mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 15:27:25 +00:00
Compare commits
161 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 | ||
![]() |
80855e89ec | ||
![]() |
0850401dc4 | ||
![]() |
337fda2023 | ||
![]() |
64f238191e | ||
![]() |
eb169cb133 | ||
![]() |
92789c3113 | ||
![]() |
c1c677e161 | ||
![]() |
2fe917ff82 | ||
![]() |
0e6c205732 | ||
![]() |
125ae0a173 | ||
![]() |
0245e13591 | ||
![]() |
d546733287 | ||
![]() |
c275326d59 | ||
![]() |
d4561507b8 | ||
![]() |
2624706c69 | ||
![]() |
d39d885ec2 | ||
![]() |
d83c744725 | ||
![]() |
843995cdb9 | ||
![]() |
9491ba77e0 | ||
![]() |
58a449d437 | ||
![]() |
7f55e0f05b | ||
![]() |
4f9e8d2e8a | ||
![]() |
21be2f46f3 | ||
![]() |
a6e7680212 | ||
![]() |
e79e744e08 | ||
![]() |
7abdac72a4 | ||
![]() |
90d85eaf7d | ||
![]() |
e65f9740fb | ||
![]() |
7538f89b56 | ||
![]() |
7c755a3991 | ||
![]() |
10e903c9fc | ||
![]() |
b018124226 | ||
![]() |
5d632d0d90 | ||
![]() |
4eecaea601 | ||
![]() |
63055818ec | ||
![]() |
0beb08b687 | ||
![]() |
bbc9e60a12 | ||
![]() |
6c975ecc4c | ||
![]() |
23e8a4ce4b | ||
![]() |
50134a2f9b | ||
![]() |
628b37c4fa | ||
![]() |
1b4ae70a43 | ||
![]() |
065051a360 |
@@ -19,16 +19,14 @@ android {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +43,14 @@ android {
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -58,26 +64,51 @@ dependencies {
|
||||
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
|
||||
def markwonVersion = '3.0.1'
|
||||
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 libsuVersion = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
|
||||
def vLibsu = '2.5.0'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
def koin = "2.0.0-rc-2"
|
||||
implementation "org.koin:koin-core:${koin}"
|
||||
implementation "org.koin:koin-android:${koin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${koin}"
|
||||
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}"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha06'
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
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
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
@@ -35,6 +35,7 @@
|
||||
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
}
|
||||
|
@@ -4,22 +4,21 @@ import android.annotation.SuppressLint
|
||||
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 androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
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.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
@@ -29,13 +28,6 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
|
||||
lateinit var protectedContext: Context
|
||||
|
||||
@Deprecated("Use dependency injection")
|
||||
val prefs: SharedPreferences by inject()
|
||||
@Deprecated("Use dependency injection")
|
||||
val DB: MagiskDB by inject()
|
||||
@Deprecated("Use dependency injection")
|
||||
val repoDB: RepoDatabaseHelper by inject()
|
||||
|
||||
@Volatile
|
||||
private var foreground: Activity? = null
|
||||
|
||||
@@ -118,6 +110,12 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
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("")
|
||||
|
@@ -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 com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
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.getPrefs().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.getPrefs();
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
try {
|
||||
SuFileInputStream is = new SuFileInputStream(config);
|
||||
XmlPullParser parser = Xml.newPullParser();
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
|
||||
parser.setInput(is, "UTF-8");
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.START_TAG, null, "map");
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.getEventType() != XmlPullParser.START_TAG)
|
||||
continue;
|
||||
String key = parser.getAttributeValue(null, "name");
|
||||
String value = parser.getAttributeValue(null, "value");
|
||||
switch (parser.getName()) {
|
||||
case "string":
|
||||
parser.require(XmlPullParser.START_TAG, null, "string");
|
||||
editor.putString(key, parser.nextText());
|
||||
parser.require(XmlPullParser.END_TAG, null, "string");
|
||||
break;
|
||||
case "boolean":
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean");
|
||||
editor.putBoolean(key, Boolean.parseBoolean(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean");
|
||||
break;
|
||||
case "int":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putInt(key, Integer.parseInt(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
case "long":
|
||||
parser.require(XmlPullParser.START_TAG, null, "long");
|
||||
editor.putLong(key, Long.parseLong(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "long");
|
||||
break;
|
||||
case "float":
|
||||
parser.require(XmlPullParser.START_TAG, null, "int");
|
||||
editor.putFloat(key, Float.parseFloat(value));
|
||||
parser.nextTag();
|
||||
parser.require(XmlPullParser.END_TAG, null, "int");
|
||||
break;
|
||||
default:
|
||||
parser.next();
|
||||
}
|
||||
}
|
||||
} catch (IOException | XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
editor.remove(Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
editor = pref.edit();
|
||||
config.delete();
|
||||
}
|
||||
|
||||
// Set defaults if not set
|
||||
setDefs(pref, editor);
|
||||
|
||||
// These settings are from actual device state
|
||||
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.apply();
|
||||
}
|
||||
|
||||
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.getPrefs().getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.getPrefs().getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.getDB().getSettings(key, getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.getDB().getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.getDB().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.getPrefs().edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.getPrefs().edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.getDB().setSettings(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.getDB().setSettings(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().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.getPrefs().edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.getDB().rmSettings(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().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.getDB().getSettings(key, (Integer) defs.get(key))));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.getDB().getStrings(key, (String) defs.get(key)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.getDB().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,102 +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;
|
||||
|
||||
// Generic
|
||||
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
|
||||
|
||||
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"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -12,8 +12,7 @@ val applicationModule = module {
|
||||
factory { get<Context>().resources }
|
||||
factory { get<Context>() as App }
|
||||
factory { get<Context>().packageManager }
|
||||
single(SUTimeout) {
|
||||
get<App>().protectedContext.getSharedPreferences("su_timeout", 0)
|
||||
}
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<App>().protectedContext) }
|
||||
}
|
||||
factory(Protected) { get<App>().protectedContext }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
}
|
@@ -1,12 +1,15 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val databaseModule = module {
|
||||
single { MagiskDB(get<App>().protectedContext) }
|
||||
single { LogDao() }
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { RepoDatabaseHelper(get()) }
|
||||
single { UpdateRepos(get()) }
|
||||
}
|
||||
|
@@ -2,4 +2,5 @@ package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
@@ -1,6 +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) }
|
||||
}
|
||||
|
||||
val networkingModule = module {}
|
||||
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)
|
||||
}
|
@@ -1,6 +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 {}
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { AppRepository(get()) }
|
||||
}
|
||||
|
@@ -18,7 +18,7 @@ val viewModelModules = module {
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { ModuleViewModel(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()) }
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -6,6 +6,8 @@ import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
@@ -29,8 +31,6 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class DownloadModuleService extends Service {
|
||||
|
||||
private List<ProgressNotification> notifications;
|
||||
|
@@ -5,10 +5,10 @@ import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
|
||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||
@@ -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,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();
|
||||
}
|
@@ -4,10 +4,10 @@ import android.content.ContentValues;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
|
||||
public class Policy implements Comparable<Policy>{
|
||||
public static final int INTERACTIVE = 0;
|
||||
@@ -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)
|
@@ -1,11 +1,26 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() {
|
||||
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_console
|
||||
|
||||
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
|
||||
val view = binding.root as TextView
|
||||
view.measure(0, 0)
|
||||
val desiredWidth = view.measuredWidth
|
||||
|
||||
view.updateLayoutParams { width = desiredWidth }
|
||||
|
||||
if (recyclerView.width < desiredWidth) {
|
||||
recyclerView.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
|
||||
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
|
||||
}
|
@@ -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) {}
|
||||
|
||||
}
|
@@ -1,11 +1,13 @@
|
||||
package com.topjohnwu.magisk.model.entity.recycler
|
||||
|
||||
import androidx.databinding.ObservableList
|
||||
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.SuLogEntry
|
||||
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>() {
|
||||
@@ -25,12 +27,12 @@ class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
}
|
||||
|
||||
class LogItemRvItem(
|
||||
val items: ObservableList<ComparableRvItem<*>>
|
||||
item: WrappedMagiskLog
|
||||
) : ComparableRvItem<LogItemRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log
|
||||
|
||||
val date = items.filterIsInstance<LogItemEntryRvItem>().firstOrNull()
|
||||
?.item?.dateString.orEmpty()
|
||||
val date = item.time.toTime(timeFormatMedium)
|
||||
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
|
||||
val isExpanded = KObservableField(false)
|
||||
|
||||
fun toggle() = isExpanded.toggle()
|
||||
@@ -41,7 +43,7 @@ class LogItemRvItem(
|
||||
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
|
||||
}
|
||||
|
||||
class LogItemEntryRvItem(val item: SuLogEntry) : ComparableRvItem<LogItemEntryRvItem>() {
|
||||
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_superuser_log_entry
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
|
@@ -6,12 +6,13 @@ 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.Module
|
||||
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: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
|
||||
@@ -49,18 +50,22 @@ class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
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.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.detailUrl == other.item.detailUrl
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
@@ -6,13 +6,14 @@ 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: Policy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_policy
|
||||
|
||||
@@ -25,6 +26,13 @@ class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<Poli
|
||||
|
||||
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
|
||||
@@ -32,13 +40,11 @@ class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<Poli
|
||||
}
|
||||
shouldNotify.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
item.notification = it
|
||||
rxBus.post(PolicyUpdateEvent.Notification(this@PolicyRvItem))
|
||||
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
|
||||
}
|
||||
shouldLog.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
item.logging = it
|
||||
rxBus.post(PolicyUpdateEvent.Log(this@PolicyRvItem))
|
||||
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -8,9 +9,9 @@ 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: PolicyRvItem) : RxBus.Event {
|
||||
class Notification(item: PolicyRvItem) : PolicyUpdateEvent(item)
|
||||
class Log(item: PolicyRvItem) : PolicyUpdateEvent(item)
|
||||
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
|
||||
|
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
@@ -33,7 +32,7 @@ sealed class Flashing(
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
Utils.loadModules()
|
||||
//Utils.loadModules()
|
||||
}
|
||||
super.onResult(success)
|
||||
}
|
||||
|
@@ -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) }
|
||||
}
|
||||
}
|
@@ -6,18 +6,22 @@ import android.content.Intent
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
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.RootUtils
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
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"
|
||||
@@ -32,7 +36,6 @@ open class GeneralReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null)
|
||||
return
|
||||
val mDB: MagiskDB = get()
|
||||
var action: String? = intent.action ?: return
|
||||
when (action) {
|
||||
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
||||
@@ -47,7 +50,7 @@ open class GeneralReceiver : BroadcastReceiver() {
|
||||
}
|
||||
when (action) {
|
||||
REQUEST -> {
|
||||
val i = Intent(context, ClassMap.get<Any>(SuRequestActivity::class.java))
|
||||
val i = Intent(context, ClassMap[SuRequestActivity::class.java])
|
||||
.setAction(action)
|
||||
.putExtra("socket", intent.getStringExtra("socket"))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@@ -61,20 +64,20 @@ open class GeneralReceiver : BroadcastReceiver() {
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED ->
|
||||
// This will only work pre-O
|
||||
if (Config.get<Boolean>(Config.Key.SU_REAUTH)!!) {
|
||||
mDB.deletePolicy(getPkg(intent))
|
||||
}
|
||||
if (Config.suReAuth)
|
||||
appRepo.delete(getPkg(intent)).blockingGet()
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
mDB.deletePolicy(pkg)
|
||||
Shell.su("magiskhide --rm $pkg").submit()
|
||||
appRepo.delete(pkg).blockingGet()
|
||||
"magiskhide --rm $pkg".su().blockingGet()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
|
||||
Const.Key.BROADCAST_MANAGER_UPDATE -> {
|
||||
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
|
||||
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 -> RootUtils.reboot()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,12 +4,6 @@ import android.content.Context;
|
||||
import android.net.Network;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -17,6 +11,12 @@ import androidx.annotation.RequiresApi;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ListenableWorker;
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class DelegateWorker {
|
||||
|
||||
private ListenableWorker worker;
|
||||
|
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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 final 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.getRepoDB().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.getRepoDB().addRepo(repo);
|
||||
repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.getRepoDB().removeRepo(pair.first);
|
||||
repoDB.removeRepo(pair.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -134,7 +150,7 @@ public class UpdateRepos {
|
||||
}
|
||||
|
||||
private void fullReload() {
|
||||
Cursor c = app.getRepoDB().getRawCursor();
|
||||
Cursor c = repoDB.getRawCursor();
|
||||
runTasks(() -> {
|
||||
while (true) {
|
||||
Repo repo;
|
||||
@@ -145,32 +161,31 @@ public class UpdateRepos {
|
||||
}
|
||||
try {
|
||||
repo.update();
|
||||
app.getRepoDB().addRepo(repo);
|
||||
repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.getRepoDB().removeRepo(repo);
|
||||
repoDB.removeRepo(repo);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void exec(boolean force) {
|
||||
Event.reset(Event.REPO_LOAD_DONE);
|
||||
App.THREAD_POOL.execute(() -> {
|
||||
cached = Collections.synchronizedSet(app.getRepoDB().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.getRepoDB().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);
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ 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
|
||||
@@ -48,7 +49,7 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (!SplashActivity.DONE) {
|
||||
startActivity(Intent(this, ClassMap.get<Any>(SplashActivity::class.java)))
|
||||
startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -105,11 +106,11 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
|
||||
private fun checkHideSection() {
|
||||
val menu = binding.navView.menu
|
||||
menu.findItem(R.id.magiskHideFragment).isVisible =
|
||||
Shell.rootAccess() && Config.get<Any>(Config.Key.MAGISKHIDE) as Boolean
|
||||
Shell.rootAccess() && Config.magiskHide
|
||||
menu.findItem(R.id.modulesFragment).isVisible =
|
||||
Shell.rootAccess() && Config.magiskVersionCode >= 0
|
||||
Shell.rootAccess() && Info.magiskVersionCode >= 0
|
||||
menu.findItem(R.id.reposFragment).isVisible =
|
||||
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Config.magiskVersionCode >= 0)
|
||||
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Info.magiskVersionCode >= 0)
|
||||
menu.findItem(R.id.logFragment).isVisible =
|
||||
Shell.rootAccess()
|
||||
menu.findItem(R.id.superuserFragment).isVisible =
|
||||
|
@@ -6,14 +6,12 @@ import android.text.TextUtils
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
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.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.android.get
|
||||
|
||||
open class SplashActivity : AppCompatActivity() {
|
||||
|
||||
@@ -21,7 +19,7 @@ open class SplashActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Shell.getShell {
|
||||
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
||||
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)
|
||||
@@ -35,9 +33,9 @@ open class SplashActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun initAndStart() {
|
||||
val pkg = Config.get<String>(Config.Key.SU_MANAGER)
|
||||
if (pkg != null && packageName == BuildConfig.APPLICATION_ID) {
|
||||
Config.remove(Config.Key.SU_MANAGER)
|
||||
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)) {
|
||||
@@ -48,9 +46,6 @@ open class SplashActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic detect all locales
|
||||
LocaleManager.loadAvailableLocales(R.string.app_changelog)
|
||||
|
||||
// Set default configs
|
||||
Config.initialize()
|
||||
|
||||
@@ -59,21 +54,11 @@ open class SplashActivity : AppCompatActivity() {
|
||||
|
||||
// Schedule periodic update checks
|
||||
Utils.scheduleUpdateCheck()
|
||||
CheckUpdates.check()
|
||||
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this)
|
||||
|
||||
// Magisk working as expected
|
||||
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
||||
// Load modules
|
||||
Utils.loadModules(false)
|
||||
// Load repos
|
||||
if (Networking.checkNetworkStatus(this))
|
||||
UpdateRepos().exec()
|
||||
}
|
||||
|
||||
val intent = Intent(this, ClassMap.get<Any>(MainActivity::class.java))
|
||||
val intent = Intent(this, ClassMap[MainActivity::class.java])
|
||||
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
|
||||
DONE = true
|
||||
startActivity(intent)
|
||||
|
@@ -1,7 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
interface ActivityResultListener {
|
||||
fun onActivityResult(resultCode: Int, data: Intent?)
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.base;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceGroupAdapter;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
|
||||
|
||||
public App app = App.self;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View v = super.onCreateView(inflater, container, savedInstanceState);
|
||||
app.getPrefs().registerOnSharedPreferenceChangeListener(this);
|
||||
Event.register(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
app.getPrefs().unregisterOnSharedPreferenceChangeListener(this);
|
||||
Event.unregister(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
|
||||
return new PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
Preference preference = getItem(position);
|
||||
if (preference instanceof PreferenceCategory)
|
||||
setZeroPaddingToLayoutChildren(holder.itemView);
|
||||
else {
|
||||
View iconFrame = holder.itemView.findViewById(R.id.icon_frame);
|
||||
if (iconFrame != null) {
|
||||
iconFrame.setVisibility(preference.getIcon() == null ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setZeroPaddingToLayoutChildren(View view) {
|
||||
if (!(view instanceof ViewGroup))
|
||||
return;
|
||||
ViewGroup viewGroup = (ViewGroup) view;
|
||||
int childCount = viewGroup.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
setZeroPaddingToLayoutChildren(viewGroup.getChildAt(i));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
viewGroup.setPaddingRelative(0, viewGroup.getPaddingTop(), viewGroup.getPaddingEnd(), viewGroup.getPaddingBottom());
|
||||
else
|
||||
viewGroup.setPadding(0, viewGroup.getPaddingTop(), viewGroup.getPaddingRight(), viewGroup.getPaddingBottom());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.preference.*
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.R
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val app: App by inject()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val v = super.onCreateView(inflater, container, savedInstanceState)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
return v
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> {
|
||||
return object : PreferenceGroupAdapter(preferenceScreen) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
|
||||
super.onBindViewHolder(holder, position)
|
||||
when (val preference = getItem(position)) {
|
||||
is PreferenceCategory -> setZeroPaddingToLayoutChildren(holder.itemView)
|
||||
else -> holder.itemView.findViewById<View>(R.id.icon_frame)?.isVisible =
|
||||
preference.icon != null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setZeroPaddingToLayoutChildren(view: View) {
|
||||
(view as? ViewGroup)?.children?.forEach {
|
||||
setZeroPaddingToLayoutChildren(it)
|
||||
} ?: return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
view.setPaddingRelative(0, view.paddingTop, view.paddingEnd, view.paddingBottom)
|
||||
else
|
||||
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||
}
|
||||
|
||||
protected fun <T: Preference> findPref(key: CharSequence): T {
|
||||
return findPreference(key) as T
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
|
||||
import android.content.Intent
|
||||
|
||||
interface IBaseLeanback {
|
||||
|
||||
fun runWithExternalRW(callback: Runnable)
|
||||
fun runWithPermissions(vararg permissions: String, callback: Runnable)
|
||||
fun startActivityForResult(intent: Intent, requestCode: Int, listener: ActivityResultListener)
|
||||
|
||||
}
|
@@ -1,10 +1,13 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -16,6 +19,7 @@ import com.karumi.dexter.listener.PermissionRequest
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import com.ncapdevi.fragnav.FragNavController
|
||||
import com.ncapdevi.fragnav.FragNavTransactionOptions
|
||||
import com.skoumal.teanity.view.TeanityActivity
|
||||
import com.skoumal.teanity.viewevents.ViewEvent
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
@@ -27,16 +31,20 @@ import com.topjohnwu.magisk.model.navigation.Navigator
|
||||
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import timber.log.Timber
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit
|
||||
|
||||
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||
MagiskLeanbackActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
|
||||
Navigator, FragNavController.TransactionListener {
|
||||
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
|
||||
Navigator, FragNavController.TransactionListener {
|
||||
|
||||
override val numberOfRootFragments: Int get() = baseFragments.size
|
||||
override val baseFragments: List<KClass<out Fragment>> = listOf()
|
||||
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
|
||||
|
||||
|
||||
protected open val defaultPosition: Int = 0
|
||||
|
||||
@@ -50,14 +58,12 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
|
||||
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
|
||||
|
||||
init {
|
||||
val isDarkTheme = Config.get<Boolean>(Config.Key.DARK_THEME)
|
||||
val theme = if (isDarkTheme) {
|
||||
val theme = if (Config.darkTheme) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES
|
||||
} else {
|
||||
AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
AppCompatDelegate.setDefaultNightMode(theme)
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
}
|
||||
|
||||
override fun applyOverrideConfiguration(config: Configuration?) {
|
||||
@@ -66,6 +72,10 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
|
||||
super.applyOverrideConfiguration(config)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale))
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
navigationController?.apply {
|
||||
@@ -185,19 +195,23 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
|
||||
Dexter.withActivity(this)
|
||||
.withPermissions(*permissions)
|
||||
.withListener(object : MultiplePermissionsListener {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport?) =
|
||||
if (report?.areAllPermissionsGranted() == true) {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
|
||||
if (report.areAllPermissionsGranted()) {
|
||||
request.onSuccess()
|
||||
} else {
|
||||
request.onFailure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPermissionRationaleShouldBeShown(
|
||||
permissions: MutableList<PermissionRequest>?,
|
||||
token: PermissionToken?
|
||||
) = request.onShowRationale(permissions.orEmpty().map { it.name })
|
||||
})
|
||||
.check()
|
||||
permissions: MutableList<PermissionRequest>,
|
||||
token: PermissionToken
|
||||
) = token.continuePermissionRequest()
|
||||
}).check()
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
|
||||
@@ -207,4 +221,21 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultCallbacks[requestCode]?.apply {
|
||||
resultCallbacks.remove(requestCode)
|
||||
invoke(this@MagiskActivity, resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
fun startActivityForResult(
|
||||
intent: Intent,
|
||||
requestCode: Int,
|
||||
listener: RequestCallback
|
||||
) {
|
||||
resultCallbacks[requestCode] = listener
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,64 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.base
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.karumi.dexter.Dexter
|
||||
import com.karumi.dexter.MultiplePermissionsReport
|
||||
import com.karumi.dexter.PermissionToken
|
||||
import com.karumi.dexter.listener.PermissionRequest
|
||||
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
|
||||
import com.skoumal.teanity.view.TeanityActivity
|
||||
import com.topjohnwu.magisk.Const
|
||||
|
||||
abstract class MagiskLeanbackActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
|
||||
TeanityActivity<ViewModel, Binding>(), IBaseLeanback {
|
||||
|
||||
private val resultListeners = SparseArrayCompat<ActivityResultListener>()
|
||||
|
||||
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||
fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() })
|
||||
|
||||
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||
override fun runWithExternalRW(callback: Runnable) {
|
||||
runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback)
|
||||
}
|
||||
|
||||
@Deprecated("Permissions will be checked in a different streamlined way")
|
||||
override fun runWithPermissions(vararg permissions: String, callback: Runnable) {
|
||||
Dexter.withActivity(this)
|
||||
.withPermissions(*permissions)
|
||||
.withListener(object : MultiplePermissionsListener {
|
||||
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
|
||||
if (report?.areAllPermissionsGranted() == true) {
|
||||
Const.EXTERNAL_PATH.mkdirs()
|
||||
callback.run()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPermissionRationaleShouldBeShown(
|
||||
permissions: MutableList<PermissionRequest>?,
|
||||
token: PermissionToken?
|
||||
) = Unit
|
||||
})
|
||||
.check()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
resultListeners.get(requestCode)?.apply {
|
||||
resultListeners.remove(requestCode)
|
||||
onActivityResult(resultCode, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startActivityForResult(
|
||||
intent: Intent,
|
||||
requestCode: Int,
|
||||
listener: ActivityResultListener
|
||||
) {
|
||||
resultListeners.put(requestCode, listener)
|
||||
startActivityForResult(intent, requestCode)
|
||||
}
|
||||
}
|
@@ -6,16 +6,11 @@ import com.skoumal.teanity.viewmodel.LoadingViewModel
|
||||
import com.topjohnwu.magisk.model.events.BackPressEvent
|
||||
import com.topjohnwu.magisk.model.events.PermissionEvent
|
||||
import com.topjohnwu.magisk.model.events.ViewActionEvent
|
||||
import com.topjohnwu.magisk.utils.Event
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener {
|
||||
|
||||
override fun onEvent(event: Int) = Timber.i("Event of $event was not handled")
|
||||
override fun getListeningEvents(): IntArray = intArrayOf()
|
||||
abstract class MagiskViewModel : LoadingViewModel() {
|
||||
|
||||
fun withView(action: Activity.() -> Unit) {
|
||||
ViewActionEvent(action).publish()
|
||||
|
@@ -103,7 +103,7 @@ class FlashViewModel(
|
||||
.subscribeK { SnackbarEvent(it).publish() }
|
||||
.add()
|
||||
|
||||
fun restartPressed() = RootUtils.reboot()
|
||||
fun restartPressed() = reboot()
|
||||
|
||||
fun backPressed() = back()
|
||||
|
||||
|
@@ -1,32 +1,26 @@
|
||||
package com.topjohnwu.magisk.ui.hide
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.update
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
import timber.log.Timber
|
||||
|
||||
class HideViewModel(
|
||||
private val packageManager: PackageManager,
|
||||
private val magiskRepo: MagiskRepository,
|
||||
rxBus: RxBus
|
||||
) : MagiskViewModel() {
|
||||
|
||||
@@ -55,26 +49,19 @@ class HideViewModel(
|
||||
// fetching this for every item is nonsensical, so we add .cache() so the response is all
|
||||
// the same for every single mapped item, it only actually executes the whole thing the
|
||||
// first time around.
|
||||
val hideTargets = Shell.su("magiskhide --ls").toSingle()
|
||||
.map { it.exec().out }
|
||||
.flattenAsFlowable { it }
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
.cache()
|
||||
val hideTargets = magiskRepo.fetchHideTargets().cache()
|
||||
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
magiskRepo.fetchApps()
|
||||
.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() }
|
||||
.map { HideRvItem(it, hideTargets.blockingGet()) }
|
||||
.toList()
|
||||
.map { it.sortWith(compareBy(
|
||||
{it.isHiddenState.value}, {it.item.name}, {it.packageName})); it }
|
||||
.map {
|
||||
it.sortedWith(compareBy(
|
||||
{ it.isHiddenState.value },
|
||||
{ it.item.name.toLowerCase() },
|
||||
{ it.packageName }
|
||||
))
|
||||
}
|
||||
.doOnSuccess { allItems.update(it) }
|
||||
.flatMap { queryRaw() }
|
||||
.applyViewModel(this)
|
||||
@@ -103,26 +90,9 @@ class HideViewModel(
|
||||
.toList()
|
||||
.map { it to items.calculateDiff(it) }
|
||||
|
||||
private fun toggleItem(item: HideProcessRvItem) {
|
||||
val state = if (item.isHidden.value) "add" else "rm"
|
||||
"magiskhide --%s %s %s".format(state, item.packageName, item.process)
|
||||
.let { Shell.su(it) }
|
||||
.toSingle()
|
||||
.map { it.submit() }
|
||||
.subscribeK()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val blacklist = listOf(
|
||||
App.self.packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
)
|
||||
}
|
||||
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
|
||||
.toggleHide(item.isHidden.value, item.packageName, item.process)
|
||||
.subscribeK()
|
||||
.add()
|
||||
|
||||
}
|
@@ -25,10 +25,24 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_magiskhide, menu)
|
||||
menu.apply {
|
||||
(findItem(R.id.app_search).actionView as? SearchView)
|
||||
?.setOnQueryTextListener(this@MagiskHideFragment)
|
||||
val query = viewModel.query.value
|
||||
val searchItem = menu.findItem(R.id.app_search)
|
||||
val searchView = searchItem.actionView as? SearchView
|
||||
|
||||
val showSystem = Config.get<Boolean>(Config.Key.SHOW_SYSTEM_APP)
|
||||
searchView?.run {
|
||||
setOnQueryTextListener(this@MagiskHideFragment)
|
||||
setQuery(query, false)
|
||||
}
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView?.isIconified = false
|
||||
} else {
|
||||
searchItem.collapseActionView()
|
||||
searchView?.isIconified = true
|
||||
}
|
||||
|
||||
val showSystem = Config.showSystemApp
|
||||
|
||||
findItem(R.id.show_system).isChecked = showSystem
|
||||
viewModel.isShowSystem.value = showSystem
|
||||
@@ -39,10 +53,8 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
|
||||
if (item.itemId == R.id.show_system) {
|
||||
val showSystem = !item.isChecked
|
||||
item.isChecked = showSystem
|
||||
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem)
|
||||
Config.showSystemApp = showSystem
|
||||
viewModel.isShowSystem.value = showSystem
|
||||
//adapter!!.setShowSystem(showSystem)
|
||||
//adapter!!.filter(search!!.query.toString())
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -56,9 +68,4 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
|
||||
viewModel.query.value = query.orEmpty()
|
||||
return false
|
||||
}
|
||||
|
||||
/*override fun onEvent(event: Int) {
|
||||
//mSwipeRefreshLayout!!.isRefreshing = false
|
||||
adapter!!.filter(search!!.query.toString())
|
||||
}*/
|
||||
}
|
||||
|
@@ -1,16 +1,20 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.viewevents.ViewEvent
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
|
||||
import com.topjohnwu.magisk.model.events.*
|
||||
import com.topjohnwu.magisk.ui.base.MagiskActivity
|
||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||
import com.topjohnwu.magisk.utils.ISafetyNetHelper
|
||||
import com.topjohnwu.magisk.utils.copyTo
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||
import com.topjohnwu.magisk.view.dialogs.*
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import dalvik.system.DexClassLoader
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -21,6 +25,8 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
|
||||
override val layoutRes: Int = R.layout.fragment_magisk
|
||||
override val viewModel: HomeViewModel by viewModel()
|
||||
val magiskRepo: MagiskRepository by inject()
|
||||
lateinit var EXT_FILE: File
|
||||
|
||||
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
|
||||
|
||||
@@ -37,6 +43,11 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
EXT_FILE = File("${requireActivity().filesDir.parent}/snet", "snet.apk")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
@@ -45,7 +56,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
|
||||
private fun installMagisk() {
|
||||
// Show Manager update first
|
||||
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
|
||||
if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) {
|
||||
installManager()
|
||||
return
|
||||
}
|
||||
@@ -61,9 +72,9 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
|
||||
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
|
||||
|
||||
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
|
||||
fun download() = Networking
|
||||
.get(Const.Url.SNET_URL)
|
||||
.getAsFile(EXT_APK) { updateSafetyNet(true) }
|
||||
fun download() = magiskRepo.fetchSafetynet()
|
||||
.map { it.byteStream().copyTo(EXT_FILE) }
|
||||
.subscribeK { updateSafetyNet(true) }
|
||||
|
||||
if (!requiresUserInput) {
|
||||
download()
|
||||
|
@@ -1,29 +1,29 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.content.Context
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.*
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.model.events.*
|
||||
import com.topjohnwu.magisk.model.observer.Observer
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
import com.topjohnwu.magisk.utils.*
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.magisk.utils.ISafetyNetHelper
|
||||
import com.topjohnwu.magisk.utils.packageName
|
||||
import com.topjohnwu.magisk.utils.res
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
|
||||
class HomeViewModel(
|
||||
private val context: Context
|
||||
private val magiskRepo: MagiskRepository
|
||||
) : MagiskViewModel() {
|
||||
|
||||
val isAdvancedExpanded = KObservableField(false)
|
||||
|
||||
val isForceEncryption = KObservableField(Config.keepEnc)
|
||||
val isKeepVerity = KObservableField(Config.keepVerity)
|
||||
val isForceEncryption = KObservableField(Info.keepEnc)
|
||||
val isKeepVerity = KObservableField(Info.keepVerity)
|
||||
|
||||
val magiskState = KObservableField(MagiskState.LOADING)
|
||||
val magiskStateText = Observer(magiskState) {
|
||||
@@ -38,7 +38,7 @@ class HomeViewModel(
|
||||
val magiskCurrentVersion = KObservableField("")
|
||||
val magiskLatestVersion = KObservableField("")
|
||||
val magiskAdditionalInfo = Observer(magiskState) {
|
||||
if (Config.get<Boolean>(Config.Key.COREONLY))
|
||||
if (Config.coreOnly)
|
||||
R.string.core_only_enabled.res()
|
||||
else
|
||||
""
|
||||
@@ -83,25 +83,16 @@ class HomeViewModel(
|
||||
private var shownDialog = false
|
||||
|
||||
init {
|
||||
Event.register(this)
|
||||
|
||||
isForceEncryption.addOnPropertyChangedCallback {
|
||||
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
|
||||
Info.keepEnc = it ?: return@addOnPropertyChangedCallback
|
||||
}
|
||||
isKeepVerity.addOnPropertyChangedCallback {
|
||||
Config.keepVerity = it ?: return@addOnPropertyChangedCallback
|
||||
Info.keepVerity = it ?: return@addOnPropertyChangedCallback
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun onEvent(event: Int) {
|
||||
updateSelf()
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
override fun getListeningEvents(): IntArray = intArrayOf(Event.UPDATE_CHECK_DONE)
|
||||
|
||||
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
|
||||
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
|
||||
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
|
||||
@@ -160,43 +151,41 @@ class HomeViewModel(
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
state = State.LOADING
|
||||
magiskState.value = MagiskState.LOADING
|
||||
managerState.value = MagiskState.LOADING
|
||||
ctsState.value = SafetyNetState.IDLE
|
||||
basicIntegrityState.value = SafetyNetState.IDLE
|
||||
safetyNetTitle.value = R.string.safetyNet_check_text
|
||||
Event.reset(this)
|
||||
Config.remoteMagiskVersionString = null
|
||||
Config.remoteMagiskVersionCode = -1
|
||||
magiskRepo.fetchUpdate()
|
||||
.applyViewModel(this)
|
||||
.doOnSubscribeUi {
|
||||
magiskState.value = MagiskState.LOADING
|
||||
managerState.value = MagiskState.LOADING
|
||||
ctsState.value = SafetyNetState.IDLE
|
||||
basicIntegrityState.value = SafetyNetState.IDLE
|
||||
safetyNetTitle.value = R.string.safetyNet_check_text
|
||||
}
|
||||
.subscribeK {
|
||||
updateSelf()
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
hasRoot.value = Shell.rootAccess()
|
||||
|
||||
if (Networking.checkNetworkStatus(context)) {
|
||||
CheckUpdates.check()
|
||||
} else {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf() {
|
||||
state = State.LOADED
|
||||
magiskState.value = when (Config.magiskVersionCode) {
|
||||
magiskState.value = when (Info.magiskVersionCode) {
|
||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
|
||||
!in Config.remoteMagiskVersionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
}
|
||||
|
||||
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
|
||||
version.format(Config.magiskVersionString, Config.magiskVersionCode)
|
||||
version.format(Info.magiskVersionString, Info.magiskVersionCode)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
magiskLatestVersion.value = version
|
||||
.format(Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)
|
||||
.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
|
||||
|
||||
managerState.value = when (Config.remoteManagerVersionCode) {
|
||||
managerState.value = when (Info.remote.app.versionCode) {
|
||||
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
|
||||
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
@@ -206,7 +195,7 @@ class HomeViewModel(
|
||||
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
|
||||
|
||||
managerLatestVersion.value = version
|
||||
.format(Config.remoteManagerVersionString, Config.remoteManagerVersionCode)
|
||||
.format(Info.remote.app.version, Info.remote.app.versionCode)
|
||||
}
|
||||
|
||||
private fun ensureEnv() {
|
||||
|
@@ -1,10 +1,9 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.databinding.ObservableArrayList
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.extensions.doOnSuccessUi
|
||||
import com.skoumal.teanity.extensions.doOnSubscribeUi
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
@@ -12,14 +11,15 @@ import com.skoumal.teanity.viewevents.SnackbarEvent
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.model.entity.recycler.*
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.model.binding.BindingAdapter
|
||||
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
|
||||
import com.topjohnwu.magisk.model.events.PageChangedEvent
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.zip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
import java.io.File
|
||||
@@ -28,9 +28,10 @@ import java.util.*
|
||||
|
||||
class LogViewModel(
|
||||
private val resources: Resources,
|
||||
private val database: MagiskDB
|
||||
private val logRepo: LogRepository
|
||||
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
||||
|
||||
val itemsAdapter = BindingAdapter()
|
||||
val items = DiffObservableList(ComparableRvItem.callback)
|
||||
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
|
||||
item.bind(itemBinding)
|
||||
@@ -42,6 +43,8 @@ class LogViewModel(
|
||||
private val logItem get() = items[0] as LogRvItem
|
||||
private val magiskLogItem get() = items[1] as MagiskLogRvItem
|
||||
|
||||
val scrollPosition = KObservableField(0)
|
||||
|
||||
init {
|
||||
currentPage.addOnPropertyChangedCallback {
|
||||
it ?: return@addOnPropertyChangedCallback
|
||||
@@ -58,9 +61,14 @@ class LogViewModel(
|
||||
else -> ""
|
||||
}
|
||||
|
||||
fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true }
|
||||
.subscribeK()
|
||||
.add()
|
||||
fun scrollDownPressed() {
|
||||
scrollPosition.value = magiskLogItem.items.size - 1
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
fetchLogs().subscribeK { logItem.update(it) }
|
||||
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
val now = Calendar.getInstance()
|
||||
@@ -88,35 +96,25 @@ class LogViewModel(
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun clearLogs(callback: () -> Unit) {
|
||||
Single.fromCallable { database.clearLogs() }
|
||||
.subscribeK {
|
||||
SnackbarEvent(R.string.logs_cleared).publish()
|
||||
callback()
|
||||
}
|
||||
.add()
|
||||
}
|
||||
private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs()
|
||||
.doOnSubscribeUi(callback)
|
||||
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
|
||||
.add()
|
||||
|
||||
private fun clearMagiskLogs(callback: () -> Unit) {
|
||||
Shell.su("echo -n > " + Const.MAGISK_LOG).submit {
|
||||
SnackbarEvent(R.string.logs_cleared).publish()
|
||||
callback()
|
||||
}
|
||||
}
|
||||
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs()
|
||||
.ignoreElement()
|
||||
.doOnComplete(callback)
|
||||
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
|
||||
.add()
|
||||
|
||||
private fun updateLogs() = Single.fromCallable { database.logs }
|
||||
private fun fetchLogs() = logRepo.fetchLogs()
|
||||
.flattenAsFlowable { it }
|
||||
.map { it.map { LogItemEntryRvItem(it) } }
|
||||
.map { LogItemRvItem(ObservableArrayList<ComparableRvItem<*>>().apply { addAll(it) }) }
|
||||
.map { LogItemRvItem(it) }
|
||||
.toList()
|
||||
.doOnSuccessUi { logItem.update(it) }
|
||||
|
||||
private fun updateMagiskLog() = Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").toSingle()
|
||||
.map { it.exec() }
|
||||
.map { it.out }
|
||||
private fun fetchMagiskLog() = logRepo.fetchMagiskLogs()
|
||||
.flattenAsFlowable { it }
|
||||
.map { ConsoleRvItem(it) }
|
||||
.toList()
|
||||
.doOnSuccessUi { magiskLogItem.update(it) }
|
||||
|
||||
}
|
@@ -2,16 +2,15 @@ package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
import androidx.annotation.StringRes
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.extensions.doOnSuccessUi
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
import com.topjohnwu.magisk.model.entity.Module
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
|
||||
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
|
||||
@@ -21,7 +20,6 @@ import com.topjohnwu.magisk.model.events.OpenChangelogEvent
|
||||
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
import com.topjohnwu.magisk.utils.Event
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.update
|
||||
@@ -30,8 +28,9 @@ import io.reactivex.disposables.Disposable
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
class ModuleViewModel(
|
||||
private val repoDatabase: RepoDatabaseHelper,
|
||||
private val resources: Resources
|
||||
private val resources: Resources,
|
||||
private val repoDatabase: RepoDatabaseHelper,
|
||||
private val repoUpdater: UpdateRepos
|
||||
) : MagiskViewModel() {
|
||||
|
||||
val query = KObservableField("")
|
||||
@@ -52,36 +51,23 @@ class ModuleViewModel(
|
||||
queryDisposable?.dispose()
|
||||
queryDisposable = query()
|
||||
}
|
||||
Event.register(this)
|
||||
refresh()
|
||||
}
|
||||
|
||||
override fun getListeningEvents(): IntArray {
|
||||
return intArrayOf(Event.MODULE_LOAD_DONE, Event.REPO_LOAD_DONE)
|
||||
}
|
||||
|
||||
override fun onEvent(event: Int) = when (event) {
|
||||
Event.MODULE_LOAD_DONE -> updateModules(Event.getResult(event))
|
||||
Event.REPO_LOAD_DONE -> updateRepos()
|
||||
else -> Unit
|
||||
refresh(false)
|
||||
}
|
||||
|
||||
fun fabPressed() = OpenFilePickerEvent().publish()
|
||||
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
|
||||
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
|
||||
|
||||
fun refresh() {
|
||||
state = State.LOADING
|
||||
Utils.loadModules(true)
|
||||
UpdateRepos().exec(true)
|
||||
}
|
||||
|
||||
private fun updateModules(result: Map<String, Module>) = result.values
|
||||
.map { ModuleRvItem(it) }
|
||||
.let { itemsInstalled.update(it) }
|
||||
|
||||
internal fun updateRepos() {
|
||||
Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } }
|
||||
fun refresh(force: Boolean) {
|
||||
Single.fromCallable { Utils.loadModulesLeanback() }
|
||||
.map { it.values.toList() }
|
||||
.flattenAsFlowable { it }
|
||||
.map { ModuleRvItem(it) }
|
||||
.toList()
|
||||
.map { it to itemsInstalled.calculateDiff(it) }
|
||||
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
|
||||
.flatMap { repoUpdater.exec(force) }
|
||||
.flatMap { Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } } }
|
||||
.flattenAsFlowable { it }
|
||||
.map { RepoRvItem(it) }
|
||||
.toList()
|
||||
@@ -89,7 +75,6 @@ class ModuleViewModel(
|
||||
.flatMap { queryRaw() }
|
||||
.applyViewModel(this)
|
||||
.subscribeK { itemsRemote.update(it.first, it.second) }
|
||||
.add()
|
||||
}
|
||||
|
||||
private fun query() = queryRaw()
|
||||
@@ -109,27 +94,20 @@ class ModuleViewModel(
|
||||
|
||||
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
|
||||
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
|
||||
val installedModules = filter { installed.any { item -> it.item.id == item.item.id } }
|
||||
|
||||
fun installedByID(id: String) = installed.firstOrNull { it.item.id == id }
|
||||
fun <T : ComparableRvItem<*>> List<T>.withTitle(text: Int) =
|
||||
if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this
|
||||
|
||||
fun List<RepoRvItem>.filterObsolete() = filter {
|
||||
val module = installedByID(it.item.id) ?: return@filter false
|
||||
module.item.versionCode != it.item.versionCode
|
||||
val groupedItems = groupBy { repo ->
|
||||
installed.firstOrNull { it.item.id == repo.item.id }?.let {
|
||||
if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE
|
||||
else MODULE_INSTALLED
|
||||
} ?: MODULE_REMOTE
|
||||
}
|
||||
|
||||
val resultObsolete = installedModules.filterObsolete()
|
||||
val resultInstalled = installedModules - resultObsolete
|
||||
val resultRemote = toList() - installedModules
|
||||
|
||||
fun buildList(@StringRes text: Int, list: List<RepoRvItem>): List<ComparableRvItem<*>> {
|
||||
return if (list.isEmpty()) list
|
||||
else listOf(SectionRvItem(resources.getString(text))) + list
|
||||
}
|
||||
|
||||
return buildList(R.string.update_available, resultObsolete) +
|
||||
buildList(R.string.installed, resultInstalled) +
|
||||
buildList(R.string.not_installed, resultRemote)
|
||||
return groupedItems.getOrElse(MODULE_UPDATABLE) { listOf() }.withTitle(R.string.update_available) +
|
||||
groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) +
|
||||
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed)
|
||||
}
|
||||
|
||||
private fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||
@@ -138,4 +116,10 @@ class ModuleViewModel(
|
||||
return out
|
||||
}
|
||||
|
||||
companion object {
|
||||
protected const val MODULE_INSTALLED = 0
|
||||
protected const val MODULE_REMOTE = 1
|
||||
protected const val MODULE_UPDATABLE = 2
|
||||
}
|
||||
|
||||
}
|
@@ -16,7 +16,7 @@ import com.topjohnwu.magisk.databinding.FragmentModulesBinding
|
||||
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
|
||||
import com.topjohnwu.magisk.ui.base.MagiskFragment
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.reboot
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||
|
||||
@@ -28,7 +28,7 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
|
||||
// Get the URI of the selected file
|
||||
val intent = Intent(activity, ClassMap.get<Any>(FlashActivity::class.java))
|
||||
val intent = Intent(activity, ClassMap[FlashActivity::class.java])
|
||||
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||
startActivity(intent)
|
||||
}
|
||||
@@ -64,19 +64,20 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.reboot -> {
|
||||
RootUtils.reboot()
|
||||
reboot()
|
||||
return true
|
||||
}
|
||||
R.id.reboot_recovery -> {
|
||||
Shell.su("/system/bin/reboot recovery").submit()
|
||||
reboot("recovery")
|
||||
return true
|
||||
}
|
||||
R.id.reboot_bootloader -> {
|
||||
reboot("booloader")
|
||||
Shell.su("/system/bin/reboot bootloader").submit()
|
||||
return true
|
||||
}
|
||||
R.id.reboot_download -> {
|
||||
Shell.su("/system/bin/reboot download").submit()
|
||||
reboot("download")
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
@@ -84,32 +85,12 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
|
||||
}
|
||||
|
||||
private fun selectFile() {
|
||||
magiskActivity.runWithExternalRW {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "application/zip"
|
||||
startActivityForResult(intent, Const.ID.FETCH_ZIP)
|
||||
magiskActivity.withExternalRW {
|
||||
onSuccess {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "application/zip"
|
||||
startActivityForResult(intent, Const.ID.FETCH_ZIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*override fun getListeningEvents(): IntArray {
|
||||
return intArrayOf(Event.MODULE_LOAD_DONE)
|
||||
}
|
||||
|
||||
override fun onEvent(event: Int) {
|
||||
updateUI(Event.getResult(event))
|
||||
}*/
|
||||
|
||||
/*private fun updateUI(moduleMap: Map<String, Module>) {
|
||||
listModules.clear()
|
||||
listModules.addAll(moduleMap.values)
|
||||
if (listModules.size == 0) {
|
||||
emptyRv!!.visibility = View.VISIBLE
|
||||
recyclerView!!.visibility = View.GONE
|
||||
} else {
|
||||
emptyRv!!.visibility = View.GONE
|
||||
recyclerView!!.visibility = View.VISIBLE
|
||||
recyclerView!!.adapter = ModulesAdapter(listModules)
|
||||
}
|
||||
mSwipeRefreshLayout!!.isRefreshing = false
|
||||
}*/
|
||||
}
|
||||
|
@@ -43,8 +43,23 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.menu_repo, menu)
|
||||
(menu.findItem(R.id.repo_search).actionView as? SearchView)
|
||||
?.setOnQueryTextListener(this)
|
||||
|
||||
val query = viewModel.query.value
|
||||
val searchItem = menu.findItem(R.id.repo_search)
|
||||
val searchView = searchItem.actionView as? SearchView
|
||||
|
||||
searchView?.run {
|
||||
setOnQueryTextListener(this@ReposFragment)
|
||||
setQuery(query, false)
|
||||
}
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
searchItem.expandActionView()
|
||||
searchView?.isIconified = false
|
||||
} else {
|
||||
searchItem.collapseActionView()
|
||||
searchView?.isIconified = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -53,10 +68,10 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
.setTitle(R.string.sorting_order)
|
||||
.setSingleChoiceItems(
|
||||
R.array.sorting_orders,
|
||||
Config.get<Int>(Config.Key.REPO_ORDER)!!
|
||||
Config.repoOrder
|
||||
) { d, which ->
|
||||
Config.set(Config.Key.REPO_ORDER, which)
|
||||
viewModel.updateRepos()
|
||||
Config.repoOrder = which
|
||||
viewModel.refresh(false)
|
||||
d.dismiss()
|
||||
}.show()
|
||||
}
|
||||
@@ -74,20 +89,22 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
}
|
||||
|
||||
private fun openChangelog(item: Repo) {
|
||||
MarkDownWindow.show(context, null, item.detailUrl)
|
||||
MarkDownWindow.show(requireActivity(), null, item.detailUrl)
|
||||
}
|
||||
|
||||
private fun installModule(item: Repo) {
|
||||
val context = magiskActivity
|
||||
|
||||
fun download(install: Boolean) {
|
||||
context.runWithExternalRW {
|
||||
val intent = Intent(activity, ClassMap.get<Any>(DownloadModuleService::class.java))
|
||||
.putExtra("repo", item).putExtra("install", install)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary
|
||||
} else {
|
||||
context.startService(intent)
|
||||
context.withExternalRW {
|
||||
onSuccess {
|
||||
val intent = Intent(activity, ClassMap[DownloadModuleService::class.java])
|
||||
.putExtra("repo", item).putExtra("install", install)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,300 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.tasks.CheckUpdates;
|
||||
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment;
|
||||
import com.topjohnwu.magisk.utils.DownloadApp;
|
||||
import com.topjohnwu.magisk.utils.Event;
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper;
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
import com.topjohnwu.magisk.utils.PatchAPK;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
|
||||
public class SettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private ListPreference updateChannel, autoRes, suNotification,
|
||||
requestTimeout, rootConfig, multiuserConfig, nsConfig;
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
setHasOptionsMenu(true);
|
||||
requireActivity().setTitle(R.string.settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
getPreferenceManager().setStorageDeviceProtected();
|
||||
setPreferencesFromResource(R.xml.app_settings, rootKey);
|
||||
|
||||
boolean showSuperuser = Utils.showSuperUser();
|
||||
app.getPrefs().edit()
|
||||
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
.apply();
|
||||
|
||||
PreferenceScreen prefScreen = getPreferenceScreen();
|
||||
|
||||
PreferenceCategory generalCatagory = (PreferenceCategory) findPreference("general");
|
||||
PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk");
|
||||
PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser");
|
||||
Preference hideManager = findPreference("hide");
|
||||
hideManager.setOnPreferenceClickListener(pref -> {
|
||||
PatchAPK.hideManager();
|
||||
return true;
|
||||
});
|
||||
Preference restoreManager = findPreference("restore");
|
||||
restoreManager.setOnPreferenceClickListener(pref -> {
|
||||
DownloadApp.restore();
|
||||
return true;
|
||||
});
|
||||
findPreference("clear").setOnPreferenceClickListener(pref -> {
|
||||
app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
|
||||
app.getRepoDB().clearRepo();
|
||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
|
||||
return true;
|
||||
});
|
||||
findPreference("hosts").setOnPreferenceClickListener(pref -> {
|
||||
Shell.su("add_hosts_module").exec();
|
||||
Utils.loadModules();
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT);
|
||||
return true;
|
||||
});
|
||||
|
||||
updateChannel = (ListPreference) findPreference(Config.Key.UPDATE_CHANNEL);
|
||||
rootConfig = (ListPreference) findPreference(Config.Key.ROOT_ACCESS);
|
||||
autoRes = (ListPreference) findPreference(Config.Key.SU_AUTO_RESPONSE);
|
||||
requestTimeout = (ListPreference) findPreference(Config.Key.SU_REQUEST_TIMEOUT);
|
||||
suNotification = (ListPreference) findPreference(Config.Key.SU_NOTIFICATION);
|
||||
multiuserConfig = (ListPreference) findPreference(Config.Key.SU_MULTIUSER_MODE);
|
||||
nsConfig = (ListPreference) findPreference(Config.Key.SU_MNT_NS);
|
||||
SwitchPreferenceCompat reauth = (SwitchPreferenceCompat) findPreference(Config.Key.SU_REAUTH);
|
||||
SwitchPreferenceCompat fingerprint = (SwitchPreferenceCompat) findPreference(Config.Key.SU_FINGERPRINT);
|
||||
|
||||
updateChannel.setOnPreferenceChangeListener((p, o) -> {
|
||||
int prev = Config.get(Config.Key.UPDATE_CHANNEL);
|
||||
int channel = Integer.parseInt((String) o);
|
||||
if (channel == Config.Value.CUSTOM_CHANNEL) {
|
||||
View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null);
|
||||
EditText url = v.findViewById(R.id.custom_url);
|
||||
url.setText(app.getPrefs().getString(Config.Key.CUSTOM_CHANNEL, ""));
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.settings_update_custom)
|
||||
.setView(v)
|
||||
.setPositiveButton(R.string.ok, (d, i) ->
|
||||
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
|
||||
.setNegativeButton(R.string.close, (d, i) ->
|
||||
Config.set(Config.Key.UPDATE_CHANNEL, prev))
|
||||
.setOnCancelListener(d ->
|
||||
Config.set(Config.Key.UPDATE_CHANNEL, prev))
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
/* We only show canary channels if user is already on canary channel
|
||||
* or the user have already chosen canary channel */
|
||||
if (!Utils.isCanary() &&
|
||||
(int) Config.get(Config.Key.UPDATE_CHANNEL) < Config.Value.CANARY_CHANNEL) {
|
||||
// Remove the last 2 entries
|
||||
CharSequence[] entries = updateChannel.getEntries();
|
||||
updateChannel.setEntries(Arrays.copyOf(entries, entries.length - 2));
|
||||
}
|
||||
|
||||
setSummary();
|
||||
|
||||
// Disable dangerous settings in secondary user
|
||||
if (Const.USER_ID > 0) {
|
||||
suCategory.removePreference(multiuserConfig);
|
||||
}
|
||||
|
||||
// Disable re-authentication option on Android O, it will not work
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
reauth.setEnabled(false);
|
||||
reauth.setChecked(false);
|
||||
reauth.setSummary(R.string.android_o_not_support);
|
||||
}
|
||||
|
||||
// Disable fingerprint option if not possible
|
||||
if (!FingerprintHelper.canUseFingerprint()) {
|
||||
fingerprint.setEnabled(false);
|
||||
fingerprint.setChecked(false);
|
||||
fingerprint.setSummary(R.string.disable_fingerprint);
|
||||
}
|
||||
|
||||
if (Shell.rootAccess() && Const.USER_ID == 0) {
|
||||
if (app.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
||||
generalCatagory.removePreference(restoreManager);
|
||||
} else {
|
||||
if (!Networking.checkNetworkStatus(app))
|
||||
generalCatagory.removePreference(restoreManager);
|
||||
generalCatagory.removePreference(hideManager);
|
||||
}
|
||||
} else {
|
||||
generalCatagory.removePreference(restoreManager);
|
||||
generalCatagory.removePreference(hideManager);
|
||||
}
|
||||
|
||||
if (!showSuperuser) {
|
||||
prefScreen.removePreference(suCategory);
|
||||
}
|
||||
|
||||
if (!Shell.rootAccess()) {
|
||||
prefScreen.removePreference(magiskCategory);
|
||||
generalCatagory.removePreference(hideManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void setLocalePreference(ListPreference lp) {
|
||||
CharSequence[] entries = new CharSequence[LocaleManager.locales.size() + 1];
|
||||
CharSequence[] entryValues = new CharSequence[LocaleManager.locales.size() + 1];
|
||||
entries[0] = LocaleManager.getString(LocaleManager.defaultLocale, R.string.system_default);
|
||||
entryValues[0] = "";
|
||||
int i = 1;
|
||||
for (Locale locale : LocaleManager.locales) {
|
||||
entries[i] = locale.getDisplayName(locale);
|
||||
entryValues[i++] = LocaleManager.toLanguageTag(locale);
|
||||
}
|
||||
lp.setEntries(entries);
|
||||
lp.setEntryValues(entryValues);
|
||||
lp.setSummary(LocaleManager.locale.getDisplayName(LocaleManager.locale));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
||||
switch (key) {
|
||||
case Config.Key.ROOT_ACCESS:
|
||||
case Config.Key.SU_MULTIUSER_MODE:
|
||||
case Config.Key.SU_MNT_NS:
|
||||
app.getDB().setSettings(key, Utils.getPrefsInt(prefs, key));
|
||||
break;
|
||||
case Config.Key.DARK_THEME:
|
||||
requireActivity().recreate();
|
||||
break;
|
||||
case Config.Key.COREONLY:
|
||||
if (prefs.getBoolean(key, false)) {
|
||||
try {
|
||||
Const.MAGISK_DISABLE_FILE.createNewFile();
|
||||
} catch (IOException ignored) {}
|
||||
} else {
|
||||
Const.MAGISK_DISABLE_FILE.delete();
|
||||
}
|
||||
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG);
|
||||
break;
|
||||
case Config.Key.MAGISKHIDE:
|
||||
if (prefs.getBoolean(key, false)) {
|
||||
Shell.su("magiskhide --enable").submit();
|
||||
} else {
|
||||
Shell.su("magiskhide --disable").submit();
|
||||
}
|
||||
break;
|
||||
case Config.Key.LOCALE:
|
||||
LocaleManager.setLocale(app);
|
||||
requireActivity().recreate();
|
||||
break;
|
||||
case Config.Key.UPDATE_CHANNEL:
|
||||
case Config.Key.CUSTOM_CHANNEL:
|
||||
CheckUpdates.check();
|
||||
break;
|
||||
case Config.Key.CHECK_UPDATES:
|
||||
Utils.scheduleUpdateCheck();
|
||||
break;
|
||||
}
|
||||
setSummary(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
switch (key) {
|
||||
case Config.Key.SU_FINGERPRINT:
|
||||
boolean checked = ((SwitchPreferenceCompat) preference).isChecked();
|
||||
((SwitchPreferenceCompat) preference).setChecked(!checked);
|
||||
new FingerprintAuthDialog(requireActivity(), () -> {
|
||||
((SwitchPreferenceCompat) preference).setChecked(checked);
|
||||
Config.set(key, checked);
|
||||
}).show();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setSummary(String key) {
|
||||
switch (key) {
|
||||
case Config.Key.UPDATE_CHANNEL:
|
||||
int ch = Config.get(key);
|
||||
ch = ch < 0 ? Config.Value.STABLE_CHANNEL : ch;
|
||||
updateChannel.setSummary(getResources()
|
||||
.getStringArray(R.array.update_channel)[ch]);
|
||||
break;
|
||||
case Config.Key.ROOT_ACCESS:
|
||||
rootConfig.setSummary(getResources()
|
||||
.getStringArray(R.array.su_access)[(int)Config.get(key)]);
|
||||
break;
|
||||
case Config.Key.SU_AUTO_RESPONSE:
|
||||
autoRes.setSummary(getResources()
|
||||
.getStringArray(R.array.auto_response)[(int)Config.get(key)]);
|
||||
break;
|
||||
case Config.Key.SU_NOTIFICATION:
|
||||
suNotification.setSummary(getResources()
|
||||
.getStringArray(R.array.su_notification)[(int)Config.get(key)]);
|
||||
break;
|
||||
case Config.Key.SU_REQUEST_TIMEOUT:
|
||||
requestTimeout.setSummary(
|
||||
getString(R.string.request_timeout_summary, (int)Config.get(key)));
|
||||
break;
|
||||
case Config.Key.SU_MULTIUSER_MODE:
|
||||
multiuserConfig.setSummary(getResources()
|
||||
.getStringArray(R.array.multiuser_summary)[(int)Config.get(key)]);
|
||||
break;
|
||||
case Config.Key.SU_MNT_NS:
|
||||
nsConfig.setSummary(getResources()
|
||||
.getStringArray(R.array.namespace_summary)[(int)Config.get(key)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setSummary() {
|
||||
setSummary(Config.Key.UPDATE_CHANNEL);
|
||||
setSummary(Config.Key.ROOT_ACCESS);
|
||||
setSummary(Config.Key.SU_AUTO_RESPONSE);
|
||||
setSummary(Config.Key.SU_NOTIFICATION);
|
||||
setSummary(Config.Key.SU_REQUEST_TIMEOUT);
|
||||
setSummary(Config.Key.SU_MULTIUSER_MODE);
|
||||
setSummary(Config.Key.SU_MNT_NS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(int event) {
|
||||
setLocalePreference((ListPreference) findPreference(Config.Key.LOCALE));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getListeningEvents() {
|
||||
return new int[] {Event.LOCALE_FETCH_DONE};
|
||||
}
|
||||
}
|
@@ -0,0 +1,265 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
|
||||
import com.topjohnwu.magisk.utils.*
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class SettingsFragment : BasePreferenceFragment() {
|
||||
|
||||
private val repoDatabase: RepoDatabaseHelper by inject()
|
||||
|
||||
private lateinit var updateChannel: ListPreference
|
||||
private lateinit var autoRes: ListPreference
|
||||
private lateinit var suNotification: ListPreference
|
||||
private lateinit var requestTimeout: ListPreference
|
||||
private lateinit var rootConfig: ListPreference
|
||||
private lateinit var multiuserConfig: ListPreference
|
||||
private lateinit var nsConfig: ListPreference
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
requireActivity().setTitle(R.string.settings)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.setStorageDeviceProtected()
|
||||
setPreferencesFromResource(R.xml.app_settings, rootKey)
|
||||
|
||||
updateChannel = findPref(Config.Key.UPDATE_CHANNEL)
|
||||
rootConfig = findPref(Config.Key.ROOT_ACCESS)
|
||||
autoRes = findPref(Config.Key.SU_AUTO_RESPONSE)
|
||||
requestTimeout = findPref(Config.Key.SU_REQUEST_TIMEOUT)
|
||||
suNotification = findPref(Config.Key.SU_NOTIFICATION)
|
||||
multiuserConfig = findPref(Config.Key.SU_MULTIUSER_MODE)
|
||||
nsConfig = findPref(Config.Key.SU_MNT_NS)
|
||||
val reauth = findPreference(Config.Key.SU_REAUTH) as SwitchPreferenceCompat
|
||||
val fingerprint = findPreference(Config.Key.SU_FINGERPRINT) as SwitchPreferenceCompat
|
||||
val generalCatagory = findPreference("general") as PreferenceCategory
|
||||
val magiskCategory = findPreference("magisk") as PreferenceCategory
|
||||
val suCategory = findPreference("superuser") as PreferenceCategory
|
||||
val hideManager = findPreference("hide")
|
||||
hideManager.setOnPreferenceClickListener {
|
||||
PatchAPK.hideManager()
|
||||
true
|
||||
}
|
||||
val restoreManager = findPreference("restore")
|
||||
restoreManager.setOnPreferenceClickListener {
|
||||
DownloadApp.restore()
|
||||
true
|
||||
}
|
||||
findPreference("clear").setOnPreferenceClickListener {
|
||||
prefs.edit {
|
||||
remove(Config.Key.ETAG_KEY)
|
||||
}
|
||||
repoDatabase.clearRepo()
|
||||
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
|
||||
true
|
||||
}
|
||||
findPreference("hosts").setOnPreferenceClickListener {
|
||||
Shell.su("add_hosts_module").exec()
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
true
|
||||
}
|
||||
|
||||
updateChannel.setOnPreferenceChangeListener { _, value ->
|
||||
val channel = Integer.parseInt(value as String)
|
||||
val previous = Config.updateChannel
|
||||
|
||||
if (channel == Config.Value.CUSTOM_CHANNEL) {
|
||||
val v = LayoutInflater.from(requireActivity())
|
||||
.inflate(R.layout.custom_channel_dialog, null)
|
||||
val url = v.findViewById<EditText>(R.id.custom_url)
|
||||
url.setText(Config.customChannelUrl)
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.settings_update_custom)
|
||||
.setView(v)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
Config.customChannelUrl = url.text.toString() }
|
||||
.setNegativeButton(R.string.close) { _, _ ->
|
||||
Config.updateChannel = previous }
|
||||
.setOnCancelListener { Config.updateChannel = previous }
|
||||
.show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
setLocalePreference(findPreference(Config.Key.LOCALE) as ListPreference)
|
||||
|
||||
/* We only show canary channels if user is already on canary channel
|
||||
* or the user have already chosen canary channel */
|
||||
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
|
||||
// Remove the last 2 entries
|
||||
val entries = updateChannel.entries
|
||||
updateChannel.entries = entries.copyOf(entries.size - 2)
|
||||
|
||||
}
|
||||
|
||||
setSummary()
|
||||
|
||||
// Disable dangerous settings in secondary user
|
||||
if (Const.USER_ID > 0) {
|
||||
suCategory.removePreference(multiuserConfig)
|
||||
}
|
||||
|
||||
// Disable re-authentication option on Android O, it will not work
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
reauth.isEnabled = false
|
||||
reauth.isChecked = false
|
||||
reauth.setSummary(R.string.android_o_not_support)
|
||||
}
|
||||
|
||||
// Disable fingerprint option if not possible
|
||||
if (!FingerprintHelper.canUseFingerprint()) {
|
||||
fingerprint.isEnabled = false
|
||||
fingerprint.isChecked = false
|
||||
fingerprint.setSummary(R.string.disable_fingerprint)
|
||||
}
|
||||
|
||||
if (Shell.rootAccess() && Const.USER_ID == 0) {
|
||||
if (app.packageName == BuildConfig.APPLICATION_ID) {
|
||||
generalCatagory.removePreference(restoreManager)
|
||||
} else {
|
||||
if (!Networking.checkNetworkStatus(requireContext())) {
|
||||
generalCatagory.removePreference(restoreManager)
|
||||
}
|
||||
generalCatagory.removePreference(hideManager)
|
||||
}
|
||||
} else {
|
||||
generalCatagory.removePreference(restoreManager)
|
||||
generalCatagory.removePreference(hideManager)
|
||||
}
|
||||
|
||||
if (!Utils.showSuperUser()) {
|
||||
preferenceScreen.removePreference(suCategory)
|
||||
}
|
||||
|
||||
if (!Shell.rootAccess()) {
|
||||
preferenceScreen.removePreference(magiskCategory)
|
||||
generalCatagory.removePreference(hideManager)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
|
||||
when (key) {
|
||||
Config.Key.ROOT_ACCESS -> Config.rootMode = Utils.getPrefsInt(prefs, key)
|
||||
Config.Key.SU_MULTIUSER_MODE -> Config.suMultiuserMode = Utils.getPrefsInt(prefs, key)
|
||||
Config.Key.SU_MNT_NS -> Config.suMntNamespaceMode = Utils.getPrefsInt(prefs, key)
|
||||
Config.Key.DARK_THEME -> requireActivity().recreate()
|
||||
Config.Key.COREONLY -> {
|
||||
if (prefs.getBoolean(key, false)) {
|
||||
runCatching {
|
||||
Const.MAGISK_DISABLE_FILE.createNewFile()
|
||||
}
|
||||
} else {
|
||||
Const.MAGISK_DISABLE_FILE.delete()
|
||||
}
|
||||
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
|
||||
}
|
||||
Config.Key.MAGISKHIDE -> if (prefs.getBoolean(key, false)) {
|
||||
Shell.su("magiskhide --enable").submit()
|
||||
} else {
|
||||
Shell.su("magiskhide --disable").submit()
|
||||
}
|
||||
Config.Key.LOCALE -> {
|
||||
LocaleManager.setLocale(app)
|
||||
requireActivity().recreate()
|
||||
}
|
||||
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck()
|
||||
}
|
||||
setSummary(key)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
Config.Key.SU_FINGERPRINT -> {
|
||||
val checked = (preference as SwitchPreferenceCompat).isChecked
|
||||
preference.isChecked = !checked
|
||||
FingerprintAuthDialog(requireActivity()) {
|
||||
preference.isChecked = checked
|
||||
Config.suFingerprint = checked
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun setLocalePreference(lp: ListPreference) {
|
||||
lp.isEnabled = false
|
||||
LocaleManager.availableLocales
|
||||
.map {
|
||||
val names = mutableListOf<String>()
|
||||
val values = mutableListOf<String>()
|
||||
|
||||
names.add(LocaleManager.getString(
|
||||
LocaleManager.defaultLocale, R.string.system_default))
|
||||
values.add("")
|
||||
|
||||
it.forEach { locale ->
|
||||
names.add(locale.getDisplayName(locale))
|
||||
values.add(LocaleManager.toLanguageTag(locale))
|
||||
}
|
||||
|
||||
Pair(names.toTypedArray(), values.toTypedArray())
|
||||
}.subscribeK { (names, values) ->
|
||||
lp.isEnabled = true
|
||||
lp.entries = names
|
||||
lp.entryValues = values
|
||||
lp.summary = LocaleManager.locale.getDisplayName(LocaleManager.locale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummary(key: String) {
|
||||
when (key) {
|
||||
Config.Key.ROOT_ACCESS -> rootConfig.summary = resources
|
||||
.getStringArray(R.array.su_access)[Config.rootMode]
|
||||
Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources
|
||||
.getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode]
|
||||
Config.Key.SU_MNT_NS -> nsConfig.summary = resources
|
||||
.getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode]
|
||||
Config.Key.UPDATE_CHANNEL -> {
|
||||
var ch = Config.updateChannel
|
||||
ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch
|
||||
updateChannel.summary = resources
|
||||
.getStringArray(R.array.update_channel)[ch]
|
||||
}
|
||||
Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources
|
||||
.getStringArray(R.array.auto_response)[Config.suAutoReponse]
|
||||
Config.Key.SU_NOTIFICATION -> suNotification.summary = resources
|
||||
.getStringArray(R.array.su_notification)[Config.suNotification]
|
||||
Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary =
|
||||
getString(R.string.request_timeout_summary, Config.suDefaultTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSummary() {
|
||||
setSummary(Config.Key.ROOT_ACCESS)
|
||||
setSummary(Config.Key.SU_MULTIUSER_MODE)
|
||||
setSummary(Config.Key.SU_MNT_NS)
|
||||
setSummary(Config.Key.UPDATE_CHANNEL)
|
||||
setSummary(Config.Key.SU_AUTO_RESPONSE)
|
||||
setSummary(Config.Key.SU_NOTIFICATION)
|
||||
setSummary(Config.Key.SU_REQUEST_TIMEOUT)
|
||||
}
|
||||
}
|
@@ -10,7 +10,8 @@ import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.viewevents.SnackbarEvent
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
@@ -21,10 +22,11 @@ import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.magisk.view.dialogs.CustomAlertDialog
|
||||
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
|
||||
class SuperuserViewModel(
|
||||
private val database: MagiskDB,
|
||||
private val appRepo: AppRepository,
|
||||
private val packageManager: PackageManager,
|
||||
private val resources: Resources,
|
||||
rxBus: RxBus
|
||||
@@ -37,6 +39,7 @@ class SuperuserViewModel(
|
||||
}
|
||||
|
||||
private var ignoreNext: PolicyRvItem? = null
|
||||
private var fetchTask: Disposable? = null
|
||||
|
||||
init {
|
||||
rxBus.register<PolicyEnableEvent>()
|
||||
@@ -50,14 +53,21 @@ class SuperuserViewModel(
|
||||
}
|
||||
|
||||
fun updatePolicies() {
|
||||
Single.fromCallable { database.policyList }
|
||||
if (fetchTask?.isDisposed?.not() == true) return
|
||||
fetchTask = appRepo.fetchAll()
|
||||
.flattenAsFlowable { it }
|
||||
.map { PolicyRvItem(it, it.info.loadIcon(packageManager)) }
|
||||
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
|
||||
.toList()
|
||||
.map {
|
||||
it.sortedWith(compareBy(
|
||||
{ it.item.appName.toLowerCase() },
|
||||
{ it.item.packageName }
|
||||
))
|
||||
}
|
||||
.map { it to items.calculateDiff(it) }
|
||||
.applySchedulers()
|
||||
.applyViewModel(this)
|
||||
.subscribeK { items.update(it) }
|
||||
.add()
|
||||
.subscribeK { items.update(it.first, it.second) }
|
||||
}
|
||||
|
||||
fun deletePressed(item: PolicyRvItem) {
|
||||
@@ -85,28 +95,29 @@ class SuperuserViewModel(
|
||||
|
||||
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
|
||||
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
|
||||
val textId = if (it.logging) R.string.su_snack_notif_on else R.string.su_snack_notif_off
|
||||
val textId =
|
||||
if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
|
||||
val text = resources.getString(textId).format(it.appName)
|
||||
SnackbarEvent(text).publish()
|
||||
}
|
||||
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
|
||||
val textId =
|
||||
if (it.notification) R.string.su_snack_log_on else R.string.su_snack_log_off
|
||||
if (it.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
|
||||
val text = resources.getString(textId).format(it.appName)
|
||||
SnackbarEvent(text).publish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePolicy(item: PolicyRvItem, onSuccess: (Policy) -> Unit) =
|
||||
updatePolicy(item.item)
|
||||
private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) =
|
||||
updatePolicy(item)
|
||||
.subscribeK { onSuccess(it) }
|
||||
.add()
|
||||
|
||||
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
|
||||
fun updateState() {
|
||||
item.item.policy = if (enable) Policy.ALLOW else Policy.DENY
|
||||
val app = item.item.copy(policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY)
|
||||
|
||||
updatePolicy(item.item)
|
||||
updatePolicy(app)
|
||||
.map { it.policy == Policy.ALLOW }
|
||||
.subscribeK {
|
||||
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
@@ -128,12 +139,10 @@ class SuperuserViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePolicy(policy: Policy) =
|
||||
Single.fromCallable { database.updatePolicy(policy); policy }
|
||||
.applySchedulers()
|
||||
private fun updatePolicy(policy: MagiskPolicy) =
|
||||
appRepo.update(policy).andThen(Single.just(policy))
|
||||
|
||||
private fun deletePolicy(policy: Policy) =
|
||||
Single.fromCallable { database.deletePolicy(policy); policy }
|
||||
.applySchedulers()
|
||||
private fun deletePolicy(policy: MagiskPolicy) =
|
||||
appRepo.delete(policy.uid).andThen(Single.just(policy))
|
||||
|
||||
}
|
@@ -33,7 +33,7 @@ open class SuRequestActivity : MagiskActivity<SuRequestViewModel, ActivityReques
|
||||
val action = intent.action
|
||||
|
||||
if (TextUtils.equals(action, GeneralReceiver.REQUEST)) {
|
||||
if (!viewModel.handleRequest(intent) {})
|
||||
if (!viewModel.handleRequest(intent))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
@@ -10,26 +10,31 @@ import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.CountDownTimer
|
||||
import android.text.TextUtils
|
||||
import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.MagiskDB
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.model.events.DieEvent
|
||||
import com.topjohnwu.magisk.ui.base.MagiskViewModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.SuConnector
|
||||
import com.topjohnwu.magisk.utils.now
|
||||
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit.*
|
||||
|
||||
class SuRequestViewModel(
|
||||
private val packageManager: PackageManager,
|
||||
private val database: MagiskDB,
|
||||
private val appRepo: AppRepository,
|
||||
private val timeoutPrefs: SharedPreferences,
|
||||
private val resources: Resources
|
||||
) : MagiskViewModel() {
|
||||
@@ -44,15 +49,20 @@ class SuRequestViewModel(
|
||||
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
|
||||
val selectedItemPosition = KObservableField(0)
|
||||
|
||||
val items = DiffObservableList(ComparableRvItem.callback)
|
||||
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
|
||||
private val items = DiffObservableList(ComparableRvItem.callback)
|
||||
private val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
|
||||
item.bind(binding)
|
||||
}
|
||||
|
||||
val adapter = BindingListViewAdapter<ComparableRvItem<*>>(1).apply {
|
||||
itemBinding = this@SuRequestViewModel.itemBinding
|
||||
setItems(items)
|
||||
}
|
||||
|
||||
|
||||
var handler: ActionHandler? = null
|
||||
private var timer: CountDownTimer? = null
|
||||
private var policy: Policy? = null
|
||||
private var policy: MagiskPolicy? = null
|
||||
set(value) {
|
||||
field = value
|
||||
updatePolicy(value)
|
||||
@@ -62,12 +72,16 @@ class SuRequestViewModel(
|
||||
resources.getStringArray(R.array.allow_timeout)
|
||||
.map { SpinnerRvItem(it) }
|
||||
.let { items.update(it) }
|
||||
|
||||
selectedItemPosition.addOnPropertyChangedCallback {
|
||||
Timber.e("Changed position to $it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePolicy(policy: Policy?) {
|
||||
private fun updatePolicy(policy: MagiskPolicy?) {
|
||||
policy ?: return
|
||||
|
||||
icon.value = policy.info.loadIcon(packageManager)
|
||||
icon.value = policy.applicationInfo.loadIcon(packageManager)
|
||||
title.value = policy.appName
|
||||
packageName.value = policy.packageName
|
||||
|
||||
@@ -94,7 +108,7 @@ class SuRequestViewModel(
|
||||
return false
|
||||
}
|
||||
|
||||
fun handleRequest(intent: Intent, createUICallback: () -> Unit): Boolean {
|
||||
fun handleRequest(intent: Intent): Boolean {
|
||||
val socketName = intent.getStringExtra("socket") ?: return false
|
||||
|
||||
val connector: SuConnector
|
||||
@@ -107,8 +121,9 @@ class SuRequestViewModel(
|
||||
}
|
||||
val bundle = connector.readSocketInput()
|
||||
val uid = bundle.getString("uid")?.toIntOrNull() ?: return false
|
||||
database.clearOutdated()
|
||||
policy = database.getPolicy(uid) ?: Policy(uid, packageManager)
|
||||
appRepo.deleteOutdated().blockingGet() // wrong!
|
||||
policy = runCatching { appRepo.fetch(uid).blockingGet() }
|
||||
.getOrDefault(uid.toPolicy(packageManager))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
@@ -123,25 +138,26 @@ class SuRequestViewModel(
|
||||
done()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun handleAction(action: Int) {
|
||||
val pos = selectedItemPosition.value
|
||||
timeoutPrefs.edit().putInt(policy?.packageName, pos).apply()
|
||||
timeoutPrefs.edit().putInt(policy?.packageName, pos).commit()
|
||||
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
}
|
||||
|
||||
override fun handleAction(action: Int, time: Int) {
|
||||
policy?.apply {
|
||||
policy = action
|
||||
if (time >= 0) {
|
||||
until = if (time == 0) {
|
||||
0
|
||||
} else {
|
||||
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
|
||||
}
|
||||
database.updatePolicy(this)
|
||||
val until = if (time >= 0) {
|
||||
if (time == 0) {
|
||||
0
|
||||
} else {
|
||||
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
|
||||
}
|
||||
} else {
|
||||
policy?.until ?: 0
|
||||
}
|
||||
policy = policy?.copy(policy = action, until = until)?.apply {
|
||||
appRepo.update(this).blockingGet()
|
||||
}
|
||||
policy?.policy = action
|
||||
|
||||
handleAction()
|
||||
}
|
||||
@@ -157,7 +173,7 @@ class SuRequestViewModel(
|
||||
return true
|
||||
}
|
||||
|
||||
when (Config.get<Any>(Config.Key.SU_AUTO_RESPONSE) as Int) {
|
||||
when (Config.suAutoReponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
handler?.handleAction(Policy.DENY, 0)
|
||||
return true
|
||||
@@ -168,15 +184,13 @@ class SuRequestViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
createUICallback()
|
||||
showUI()
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun showUI() {
|
||||
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
|
||||
val millis = SECONDS.toMillis(seconds)
|
||||
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
timer = object : CountDownTimer(millis, 1000) {
|
||||
override fun onTick(remains: Long) {
|
||||
denyText.value = "%s (%d)"
|
||||
|
@@ -1,8 +1,6 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
@@ -13,8 +11,10 @@ import androidx.databinding.InverseBindingAdapter
|
||||
import androidx.databinding.InverseBindingListener
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager.widget.ViewPager
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -115,7 +115,7 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("android:selectedItemPosition")
|
||||
/*@BindingAdapter("selection"*//*, "selectionAttrChanged", "adapter"*//*)
|
||||
fun setSelectedItemPosition(view: Spinner, position: Int) {
|
||||
view.setSelection(position)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ fun setSelectedItemPosition(view: Spinner, position: Int) {
|
||||
)
|
||||
fun getSelectedItemPosition(view: Spinner) = view.selectedItemPosition
|
||||
|
||||
@BindingAdapter("android:selectedItemPositionAttrChanged")
|
||||
@BindingAdapter("selectedItemPositionAttrChanged")
|
||||
fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListener) {
|
||||
view.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onNothingSelected(p0: AdapterView<*>?) {
|
||||
@@ -137,7 +137,7 @@ fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListe
|
||||
listener.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@BindingAdapter("onTouch")
|
||||
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
|
||||
@@ -155,8 +155,6 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
|
||||
Observable.timer(1, TimeUnit.SECONDS).subscribeK { callback() }
|
||||
}
|
||||
|
||||
val tag = RecyclerView::class.java.name.sumBy { it.toInt() }
|
||||
|
||||
fun RecyclerView.Adapter<*>.setListener() {
|
||||
val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
@@ -164,11 +162,12 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
|
||||
}
|
||||
}
|
||||
registerAdapterDataObserver(observer)
|
||||
view.setTag(tag, observer)
|
||||
view.setTag(R.id.recyclerScrollListener, observer)
|
||||
}
|
||||
|
||||
fun RecyclerView.Adapter<*>.removeListener() {
|
||||
val observer = view.getTag(tag) as? RecyclerView.AdapterDataObserver ?: return
|
||||
val observer =
|
||||
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
|
||||
unregisterAdapterDataObserver(observer)
|
||||
}
|
||||
|
||||
@@ -179,4 +178,40 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
|
||||
} else {
|
||||
view.adapter?.removeListener()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("hide")
|
||||
fun setHidden(view: FloatingActionButton, hide: Boolean) {
|
||||
if (hide) view.hide() else view.show()
|
||||
}
|
||||
|
||||
@BindingAdapter("scrollPosition", "scrollPositionSmooth", requireAll = false)
|
||||
fun setScrollPosition(view: RecyclerView, position: Int, smoothScroll: Boolean) {
|
||||
val adapterItemCount = view.adapter?.itemCount ?: -1
|
||||
if (position !in 0 until adapterItemCount) {
|
||||
// the position is not in adapter bounds, adapter will throw exception for invalid positions
|
||||
return
|
||||
}
|
||||
|
||||
when {
|
||||
smoothScroll -> view.smoothScrollToPosition(position)
|
||||
else -> view.scrollToPosition(position)
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("recyclerScrollEvent")
|
||||
fun setScrollListener(view: RecyclerView, listener: InverseBindingListener) {
|
||||
view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||
// don't change this or the recycler will stop at every line, effectively disabling smooth scroll
|
||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||
listener.onChange()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@InverseBindingAdapter(attribute = "scrollPosition", event = "recyclerScrollEvent")
|
||||
fun getScrollPosition(view: RecyclerView) = (view.layoutManager as? LinearLayoutManager)
|
||||
?.findLastCompletelyVisibleItemPosition()
|
||||
?: -1
|
@@ -6,6 +6,7 @@ import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Info;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.ui.SplashActivity;
|
||||
import com.topjohnwu.magisk.view.ProgressNotification;
|
||||
@@ -24,8 +25,8 @@ public class DownloadApp {
|
||||
}
|
||||
|
||||
public static void restore() {
|
||||
String name = Utils.fmt("MagiskManager v%s(%d)",
|
||||
Config.remoteManagerVersionString, Config.remoteManagerVersionCode);
|
||||
String name = Utils.INSTANCE.fmt("MagiskManager v%s(%d)",
|
||||
Info.remote.getApp().getVersion(), Info.remote.getApp().getVersionCode());
|
||||
dlInstall(name, new RestoreManager());
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ public class DownloadApp {
|
||||
File apk = new File(App.self.getCacheDir(), "manager.apk");
|
||||
ProgressNotification progress = new ProgressNotification(name);
|
||||
listener.progress = progress;
|
||||
Networking.get(Config.managerLink)
|
||||
Networking.get(Info.remote.getApp().getLink())
|
||||
.setExecutor(App.THREAD_POOL)
|
||||
.setDownloadProgressListener(progress)
|
||||
.setErrorHandler((conn, e) -> progress.dlFail())
|
||||
|
@@ -1,123 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.collection.ArraySet;
|
||||
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Set;
|
||||
|
||||
public class Event {
|
||||
|
||||
public static final int MAGISK_HIDE_DONE = 0;
|
||||
public static final int MODULE_LOAD_DONE = 1;
|
||||
public static final int REPO_LOAD_DONE = 2;
|
||||
public static final int UPDATE_CHECK_DONE = 3;
|
||||
public static final int LOCALE_FETCH_DONE = 4;
|
||||
|
||||
@IntDef({MAGISK_HIDE_DONE, MODULE_LOAD_DONE, REPO_LOAD_DONE,
|
||||
UPDATE_CHECK_DONE, LOCALE_FETCH_DONE})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface EventID {}
|
||||
|
||||
// We will not dynamically add topics, so use arrays instead of hash tables
|
||||
private static Store[] eventList = new Store[5];
|
||||
|
||||
public static void register(Listener listener, @EventID int... events) {
|
||||
for (int event : events) {
|
||||
if (eventList[event] == null)
|
||||
eventList[event] = new Store();
|
||||
eventList[event].listeners.add(listener);
|
||||
if (eventList[event].triggered) {
|
||||
listener.onEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void register(AutoListener listener) {
|
||||
register(listener, listener.getListeningEvents());
|
||||
}
|
||||
|
||||
public static void unregister(Listener listener, @EventID int... events) {
|
||||
for (int event : events) {
|
||||
if (eventList[event] == null)
|
||||
continue;
|
||||
eventList[event].listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unregister(AutoListener listener) {
|
||||
unregister(listener, listener.getListeningEvents());
|
||||
}
|
||||
|
||||
public static void trigger(@EventID int event) {
|
||||
trigger(true, event, null);
|
||||
}
|
||||
|
||||
public static void trigger(@EventID int event, Object result) {
|
||||
trigger(true, event, result);
|
||||
}
|
||||
|
||||
public static void trigger(boolean perm, @EventID int event) {
|
||||
trigger(perm, event, null);
|
||||
}
|
||||
|
||||
public static void trigger(boolean perm, @EventID int event, Object result) {
|
||||
if (eventList[event] == null)
|
||||
eventList[event] = new Store();
|
||||
if (perm) {
|
||||
eventList[event].result = result;
|
||||
eventList[event].triggered = true;
|
||||
}
|
||||
for (Listener sub : eventList[event].listeners) {
|
||||
UiThreadHandler.run(() -> sub.onEvent(event));
|
||||
}
|
||||
}
|
||||
|
||||
public static void reset(@EventID int event) {
|
||||
if (eventList[event] == null)
|
||||
return;
|
||||
eventList[event].triggered = false;
|
||||
eventList[event].result = null;
|
||||
}
|
||||
|
||||
public static void reset(AutoListener listener) {
|
||||
for (int event : listener.getListeningEvents())
|
||||
reset(event);
|
||||
}
|
||||
|
||||
public static boolean isTriggered(@EventID int event) {
|
||||
if (eventList[event] == null)
|
||||
return false;
|
||||
return eventList[event].triggered;
|
||||
}
|
||||
|
||||
public static boolean isTriggered(AutoListener listener) {
|
||||
for (int event : listener.getListeningEvents()) {
|
||||
if (!isTriggered(event))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static <T> T getResult(@EventID int event) {
|
||||
return (T) eventList[event].result;
|
||||
}
|
||||
|
||||
private static class Store {
|
||||
boolean triggered = false;
|
||||
Set<Listener> listeners = new ArraySet<>();
|
||||
Object result;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onEvent(int event);
|
||||
}
|
||||
|
||||
public interface AutoListener extends Listener {
|
||||
@EventID
|
||||
int[] getListeningEvents();
|
||||
}
|
||||
}
|
@@ -1,123 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.KeyguardManager;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException;
|
||||
import android.security.keystore.KeyProperties;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
public abstract class FingerprintHelper {
|
||||
|
||||
private FingerprintManager manager;
|
||||
private Cipher cipher;
|
||||
private CancellationSignal cancel;
|
||||
|
||||
public static boolean useFingerprint() {
|
||||
boolean fp = Config.get(Config.Key.SU_FINGERPRINT);
|
||||
if (fp && !canUseFingerprint()) {
|
||||
Config.set(Config.Key.SU_FINGERPRINT, false);
|
||||
fp = false;
|
||||
}
|
||||
return fp;
|
||||
}
|
||||
|
||||
public static boolean canUseFingerprint() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return false;
|
||||
KeyguardManager km = App.self.getSystemService(KeyguardManager.class);
|
||||
FingerprintManager fm = App.self.getSystemService(FingerprintManager.class);
|
||||
return km.isKeyguardSecure() && fm != null && fm.isHardwareDetected() && fm.hasEnrolledFingerprints();
|
||||
}
|
||||
|
||||
protected FingerprintHelper() throws Exception {
|
||||
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
|
||||
manager = App.self.getSystemService(FingerprintManager.class);
|
||||
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
|
||||
+ KeyProperties.BLOCK_MODE_CBC + "/"
|
||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
||||
keyStore.load(null);
|
||||
SecretKey key = (SecretKey) keyStore.getKey(Const.SU_KEYSTORE_KEY, null);
|
||||
if (key == null) {
|
||||
key = generateKey();
|
||||
}
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
} catch (KeyPermanentlyInvalidatedException e) {
|
||||
// Only happens on Marshmallow
|
||||
key = generateKey();
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void onAuthenticationError(int errorCode, CharSequence errString);
|
||||
|
||||
public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString);
|
||||
|
||||
public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result);
|
||||
|
||||
public abstract void onAuthenticationFailed();
|
||||
|
||||
public void authenticate() {
|
||||
cancel = new CancellationSignal();
|
||||
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
|
||||
manager.authenticate(cryptoObject, cancel, 0, new Callback(), null);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (cancel != null)
|
||||
cancel.cancel();
|
||||
}
|
||||
|
||||
private SecretKey generateKey() throws Exception {
|
||||
KeyGenerator keygen = KeyGenerator
|
||||
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
|
||||
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
|
||||
Const.SU_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
builder.setInvalidatedByBiometricEnrollment(false);
|
||||
}
|
||||
keygen.init(builder.build());
|
||||
return keygen.generateKey();
|
||||
}
|
||||
|
||||
private class Callback extends FingerprintManager.AuthenticationCallback {
|
||||
@Override
|
||||
public void onAuthenticationError(int errorCode, CharSequence errString) {
|
||||
FingerprintHelper.this.onAuthenticationError(errorCode, errString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
|
||||
FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
||||
FingerprintHelper.this.onAuthenticationSucceeded(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailed() {
|
||||
FingerprintHelper.this.onAuthenticationFailed();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.topjohnwu.magisk.Config
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
abstract class FingerprintHelper @Throws(Exception::class)
|
||||
protected constructor() {
|
||||
|
||||
private val manager: FingerprintManager?
|
||||
private val cipher: Cipher
|
||||
private var cancel: CancellationSignal? = null
|
||||
private val context: Context by inject()
|
||||
|
||||
init {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore")
|
||||
manager = context.getSystemService(FingerprintManager::class.java)
|
||||
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
|
||||
+ KeyProperties.BLOCK_MODE_CBC + "/"
|
||||
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
keyStore.load(null)
|
||||
var key = keyStore.getKey(SU_KEYSTORE_KEY, null) as SecretKey? ?: generateKey()
|
||||
runCatching {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
}.onFailure {
|
||||
// Only happens on Marshmallow
|
||||
key = generateKey()
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onAuthenticationError(errorCode: Int, errString: CharSequence)
|
||||
|
||||
abstract fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence)
|
||||
|
||||
abstract fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult)
|
||||
|
||||
abstract fun onAuthenticationFailed()
|
||||
|
||||
fun authenticate() {
|
||||
cancel = CancellationSignal()
|
||||
val cryptoObject = FingerprintManager.CryptoObject(cipher)
|
||||
manager!!.authenticate(cryptoObject, cancel, 0, Callback(), null)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (cancel != null)
|
||||
cancel!!.cancel()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun generateKey(): SecretKey {
|
||||
val keygen = KeyGenerator
|
||||
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
|
||||
val builder = KeyGenParameterSpec.Builder(
|
||||
SU_KEYSTORE_KEY,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
builder.setInvalidatedByBiometricEnrollment(false)
|
||||
}
|
||||
keygen.init(builder.build())
|
||||
return keygen.generateKey()
|
||||
}
|
||||
|
||||
private inner class Callback : FingerprintManager.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
this@FingerprintHelper.onAuthenticationError(errorCode, errString)
|
||||
}
|
||||
|
||||
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
|
||||
this@FingerprintHelper.onAuthenticationHelp(helpCode, helpString)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
|
||||
this@FingerprintHelper.onAuthenticationSucceeded(result)
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
this@FingerprintHelper.onAuthenticationFailed()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SU_KEYSTORE_KEY = "su_key"
|
||||
|
||||
fun useFingerprint(): Boolean {
|
||||
var fp = Config.suFingerprint
|
||||
if (fp && !canUseFingerprint()) {
|
||||
Config.suFingerprint = false
|
||||
fp = false
|
||||
}
|
||||
return fp
|
||||
}
|
||||
|
||||
fun canUseFingerprint(context: Context = get()): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||
return false
|
||||
val km = context.getSystemService(KeyguardManager::class.java)
|
||||
val fm = context.getSystemService(FingerprintManager::class.java)
|
||||
return km?.isKeyguardSecure ?: false &&
|
||||
fm != null && fm.isHardwareDetected && fm.hasEnrolledFingerprints()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,147 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.internal.InternalUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class LocaleManager {
|
||||
public static Locale locale = Locale.getDefault();
|
||||
public final static Locale defaultLocale = Locale.getDefault();
|
||||
public static List<Locale> locales;
|
||||
|
||||
public static Locale forLanguageTag(String tag) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(tag);
|
||||
} else {
|
||||
String[] tok = tag.split("-");
|
||||
if (tok.length == 0) {
|
||||
return new Locale("");
|
||||
}
|
||||
String language;
|
||||
switch (tok[0]) {
|
||||
case "und":
|
||||
language = ""; // Undefined
|
||||
break;
|
||||
case "fil":
|
||||
language = "tl"; // Filipino
|
||||
break;
|
||||
default:
|
||||
language = tok[0];
|
||||
}
|
||||
if ((language.length() != 2 && language.length() != 3))
|
||||
return new Locale("");
|
||||
if (tok.length == 1)
|
||||
return new Locale(language);
|
||||
String country = tok[1];
|
||||
if (country.length() != 2 && country.length() != 3)
|
||||
return new Locale(language);
|
||||
return new Locale(language, country);
|
||||
}
|
||||
}
|
||||
|
||||
public static String toLanguageTag(Locale loc) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return loc.toLanguageTag();
|
||||
} else {
|
||||
String language = loc.getLanguage();
|
||||
String country = loc.getCountry();
|
||||
String variant = loc.getVariant();
|
||||
if (language.isEmpty() || !language.matches("\\p{Alpha}{2,8}")) {
|
||||
language = "und"; // Follow the Locale#toLanguageTag() implementation
|
||||
} else if (language.equals("iw")) {
|
||||
language = "he"; // correct deprecated "Hebrew"
|
||||
} else if (language.equals("in")) {
|
||||
language = "id"; // correct deprecated "Indonesian"
|
||||
} else if (language.equals("ji")) {
|
||||
language = "yi"; // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}")) {
|
||||
country = "";
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}")) {
|
||||
variant = "";
|
||||
}
|
||||
StringBuilder tag = new StringBuilder(language);
|
||||
if (!country.isEmpty())
|
||||
tag.append('-').append(country);
|
||||
if (!variant.isEmpty())
|
||||
tag.append('-').append(variant);
|
||||
return tag.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static void setLocale(ContextWrapper wrapper) {
|
||||
String localeConfig = Config.get(Config.Key.LOCALE);
|
||||
if (TextUtils.isEmpty(localeConfig)) {
|
||||
locale = defaultLocale;
|
||||
} else {
|
||||
locale = forLanguageTag(localeConfig);
|
||||
}
|
||||
Locale.setDefault(locale);
|
||||
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(locale));
|
||||
}
|
||||
|
||||
public static Context getLocaleContext(Context context, Locale locale) {
|
||||
Configuration config = new Configuration(context.getResources().getConfiguration());
|
||||
config.setLocale(locale);
|
||||
return context.createConfigurationContext(config);
|
||||
}
|
||||
|
||||
public static Context getLocaleContext(Locale locale) {
|
||||
return getLocaleContext(App.self.getBaseContext(), locale);
|
||||
}
|
||||
|
||||
public static String getString(Locale locale, @StringRes int id) {
|
||||
return getLocaleContext(locale).getString(id);
|
||||
}
|
||||
|
||||
public static void loadAvailableLocales(@StringRes int compareId) {
|
||||
Shell.EXECUTOR.execute(() -> {
|
||||
locales = new ArrayList<>();
|
||||
HashSet<String> set = new HashSet<>();
|
||||
Resources res = App.self.getResources();
|
||||
Locale locale;
|
||||
|
||||
// Add default locale
|
||||
locales.add(Locale.ENGLISH);
|
||||
set.add(getString(Locale.ENGLISH, compareId));
|
||||
|
||||
// Add some special locales
|
||||
locales.add(Locale.TAIWAN);
|
||||
set.add(getString(Locale.TAIWAN, compareId));
|
||||
locale = new Locale("pt", "BR");
|
||||
locales.add(locale);
|
||||
set.add(getString(locale, compareId));
|
||||
|
||||
// Other locales
|
||||
for (String s : res.getAssets().getLocales()) {
|
||||
locale = forLanguageTag(s);
|
||||
if (set.add(getString(locale, compareId))) {
|
||||
locales.add(locale);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(locales, (a, b) -> a.getDisplayName(a).compareTo(b.getDisplayName(b)));
|
||||
Event.trigger(Event.LOCALE_FETCH_DONE);
|
||||
});
|
||||
}
|
||||
}
|
135
app/src/main/java/com/topjohnwu/magisk/utils/LocaleManager.kt
Normal file
135
app/src/main/java/com/topjohnwu/magisk/utils/LocaleManager.kt
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.annotation.StringRes
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.superuser.internal.InternalUtils
|
||||
import io.reactivex.Single
|
||||
import java.util.*
|
||||
|
||||
object LocaleManager {
|
||||
@JvmStatic
|
||||
var locale = Locale.getDefault()
|
||||
@JvmStatic
|
||||
val defaultLocale = Locale.getDefault()
|
||||
|
||||
@JvmStatic
|
||||
val availableLocales = Single.fromCallable {
|
||||
val compareId = R.string.app_changelog
|
||||
val res: Resources by inject()
|
||||
mutableListOf<Locale>().apply {
|
||||
// Add default locale
|
||||
add(Locale.ENGLISH)
|
||||
|
||||
// Add some special locales
|
||||
add(Locale.TAIWAN)
|
||||
add(Locale("pt", "BR"))
|
||||
|
||||
// Other locales
|
||||
val otherLocales = res.assets.locales
|
||||
.map { forLanguageTag(it) }
|
||||
.distinctBy { getString(it, compareId) }
|
||||
|
||||
listOf("", "").toTypedArray()
|
||||
|
||||
addAll(otherLocales)
|
||||
}.sortedWith(Comparator { a, b ->
|
||||
a.getDisplayName(a).toLowerCase(a)
|
||||
.compareTo(b.getDisplayName(b).toLowerCase(b))
|
||||
})
|
||||
}.cache()
|
||||
|
||||
private fun forLanguageTag(tag: String): Locale {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(tag)
|
||||
} else {
|
||||
val tok = tag.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (tok.isEmpty()) {
|
||||
return Locale("")
|
||||
}
|
||||
val language = when (tok[0]) {
|
||||
"und" -> "" // Undefined
|
||||
"fil" -> "tl" // Filipino
|
||||
else -> tok[0]
|
||||
}
|
||||
if (language.length != 2 && language.length != 3)
|
||||
return Locale("")
|
||||
if (tok.size == 1)
|
||||
return Locale(language)
|
||||
val country = tok[1]
|
||||
|
||||
return if (country.length != 2 && country.length != 3) Locale(language)
|
||||
else Locale(language, country)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toLanguageTag(loc: Locale): String {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return loc.toLanguageTag()
|
||||
} else {
|
||||
var language = loc.language
|
||||
var country = loc.country
|
||||
var variant = loc.variant
|
||||
when {
|
||||
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
|
||||
language = "und" // Follow the Locale#toLanguageTag() implementation
|
||||
language == "iw" -> language = "he" // correct deprecated "Hebrew"
|
||||
language == "in" -> language = "id" // correct deprecated "Indonesian"
|
||||
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
|
||||
country = ""
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
|
||||
variant = ""
|
||||
}
|
||||
val tag = StringBuilder(language)
|
||||
if (country.isNotEmpty())
|
||||
tag.append('-').append(country)
|
||||
if (variant.isNotEmpty())
|
||||
tag.append('-').append(variant)
|
||||
return tag.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setLocale(wrapper: ContextWrapper) {
|
||||
val localeConfig = Config.locale
|
||||
locale = when {
|
||||
localeConfig.isEmpty() -> defaultLocale
|
||||
else -> forLanguageTag(localeConfig)
|
||||
}
|
||||
Locale.setDefault(locale)
|
||||
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(locale))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLocaleContext(context: Context, locale: Locale): Context {
|
||||
val config = Configuration(context.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
return context.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getLocaleContext(locale: Locale): Context {
|
||||
return getLocaleContext(App.self.baseContext, locale)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getString(locale: Locale, @StringRes id: Int): String {
|
||||
return getLocaleContext(locale).getString(id)
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@ public class Logger {
|
||||
}
|
||||
|
||||
public static void debug(String fmt, Object... args) {
|
||||
debug(Utils.fmt(fmt, args));
|
||||
debug(Utils.INSTANCE.fmt(fmt, args));
|
||||
}
|
||||
|
||||
public static void error(String line) {
|
||||
@@ -21,6 +21,6 @@ public class Logger {
|
||||
}
|
||||
|
||||
public static void error(String fmt, Object... args) {
|
||||
error(Utils.fmt(fmt, args));
|
||||
error(Utils.INSTANCE.fmt(fmt, args));
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ package com.topjohnwu.magisk.utils;
|
||||
import android.content.ComponentName;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
@@ -27,8 +29,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
public class PatchAPK {
|
||||
|
||||
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
|
||||
@@ -110,7 +110,7 @@ public class PatchAPK {
|
||||
if (!Shell.su("pm install " + repack).exec().isSuccess())
|
||||
return false;
|
||||
|
||||
Config.set(Config.Key.SU_MANAGER, pkg);
|
||||
Config.setSuManager(pkg);
|
||||
Config.export();
|
||||
RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID,
|
||||
new ComponentName(pkg, ClassMap.get(SplashActivity.class).getName()));
|
||||
@@ -145,7 +145,7 @@ public class PatchAPK {
|
||||
Notifications.progress(app.getString(R.string.hide_manager_title));
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build());
|
||||
if(!patchAndHide())
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
|
||||
Utils.INSTANCE.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
|
||||
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID);
|
||||
});
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user