mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 11:07:29 +00:00
Compare commits
113 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +45,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
@@ -58,26 +60,43 @@ 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.5.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.0"
|
||||
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}"
|
||||
|
||||
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.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-alpha06'
|
||||
}
|
||||
|
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
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
@@ -16,6 +16,9 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Retrofit classes
|
||||
-keep,allowobfuscation class com.topjohnwu.magisk.data.network.*
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
@@ -35,6 +38,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,18 @@ 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 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 +25,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
|
||||
|
||||
|
@@ -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 get(c: Class<*>): Class<*>? {
|
||||
return map.getOrElse(c) { null } //as? Class<T>
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
@@ -15,26 +16,29 @@ import org.xmlpull.v1.XmlPullParserException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
public class Config {
|
||||
import static com.topjohnwu.magisk.ConfigLeanback.getPrefs;
|
||||
import static com.topjohnwu.magisk.utils.XAndroidKt.getPackageName;
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public final class Config {
|
||||
|
||||
private static final ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
public static int magiskVersionCode = -1;
|
||||
private static boolean magiskHide;
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString = "";
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString;
|
||||
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 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;
|
||||
public static String managerLink = "";
|
||||
public static String managerNoteLink = "";
|
||||
public static String uninstallerLink = "";
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
@@ -98,25 +102,76 @@ public class Config {
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
|
||||
private static boolean magiskHide = false;
|
||||
|
||||
public static void loadMagiskInfo() {
|
||||
try {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
|
||||
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
|
||||
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} 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");
|
||||
getPrefs().edit().apply();
|
||||
Context context = ConfigLeanback.getProtectedContext();
|
||||
File xml = new File(context.getFilesDir().getParent() + "/shared_prefs",
|
||||
getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
private static final int PREF_INT = 0;
|
||||
private static final int PREF_STR_INT = 1;
|
||||
private static final int PREF_BOOL = 2;
|
||||
private static final int PREF_STR = 3;
|
||||
private static final int DB_INT = 4;
|
||||
private static final int DB_BOOL = 5;
|
||||
private static final int DB_STR = 6;
|
||||
|
||||
private static int getConfigType(String key) {
|
||||
switch (key) {
|
||||
case Key.REPO_ORDER:
|
||||
return PREF_INT;
|
||||
|
||||
case Key.SU_REQUEST_TIMEOUT:
|
||||
case Key.SU_AUTO_RESPONSE:
|
||||
case Key.SU_NOTIFICATION:
|
||||
case Key.UPDATE_CHANNEL:
|
||||
return PREF_STR_INT;
|
||||
|
||||
case Key.DARK_THEME:
|
||||
case Key.SU_REAUTH:
|
||||
case Key.CHECK_UPDATES:
|
||||
case Key.MAGISKHIDE:
|
||||
case Key.COREONLY:
|
||||
case Key.SHOW_SYSTEM_APP:
|
||||
return PREF_BOOL;
|
||||
|
||||
case Key.CUSTOM_CHANNEL:
|
||||
case Key.LOCALE:
|
||||
case Key.ETAG_KEY:
|
||||
return PREF_STR;
|
||||
|
||||
case Key.ROOT_ACCESS:
|
||||
case Key.SU_MNT_NS:
|
||||
case Key.SU_MULTIUSER_MODE:
|
||||
return DB_INT;
|
||||
|
||||
case Key.SU_FINGERPRINT:
|
||||
return DB_BOOL;
|
||||
|
||||
case Key.SU_MANAGER:
|
||||
return DB_STR;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = App.self.getPrefs();
|
||||
SharedPreferences pref = getPrefs();
|
||||
SharedPreferences.Editor editor = pref.edit();
|
||||
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
@@ -185,125 +240,72 @@ public class Config {
|
||||
.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));
|
||||
return (T) (Integer) getPrefs().getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(app.getPrefs(), key, getDef(key));
|
||||
return (T) (Integer) Utils.getPrefsInt(getPrefs(), key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) app.getPrefs().getBoolean(key, getDef(key));
|
||||
return (T) (Boolean) getPrefs().getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) app.getPrefs().getString(key, getDef(key));
|
||||
return (T) getPrefs().getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) app.getDB().getSettings(key, getDef(key));
|
||||
return (T) (Integer) ConfigLeanback.get(key, (Integer) getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (app.getDB().getSettings(key, getDef(key) ? 1 : 0) != 0);
|
||||
return (T) (Boolean) (ConfigLeanback.get(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) app.getDB().getStrings(key, getDef(key));
|
||||
return (T) ConfigLeanback.get(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();
|
||||
getPrefs().edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
app.getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
app.getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
app.getPrefs().edit().putString(key, (String) val).apply();
|
||||
getPrefs().edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
app.getDB().setSettings(key, (int) val);
|
||||
ConfigLeanback.put(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
app.getDB().setSettings(key, (boolean) val ? 1 : 0);
|
||||
ConfigLeanback.put(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().setStrings(key, (String) val);
|
||||
ConfigLeanback.put(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();
|
||||
getPrefs().edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
app.getDB().rmSettings(key);
|
||||
ConfigLeanback.delete(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
app.getDB().setStrings(key, null);
|
||||
ConfigLeanback.put(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
|
||||
static {
|
||||
/* Set default configurations */
|
||||
|
||||
@@ -340,7 +342,9 @@ public class Config {
|
||||
//defs.put(Key.SU_MANAGER, null);
|
||||
}
|
||||
|
||||
private static <T> T getDef(String key) {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static <T> T getDef(String key) throws IllegalArgumentException {
|
||||
Object val = defs.get(key);
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
@@ -359,36 +363,35 @@ public class Config {
|
||||
}
|
||||
|
||||
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
|
||||
App app = App.self;
|
||||
for (String key : defs.keySet()) {
|
||||
Object value = defs.get(key);
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(
|
||||
app.getDB().getSettings(key, (Integer) defs.get(key))));
|
||||
editor.putString(key, String.valueOf(ConfigLeanback.get(key, (Integer) value)));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, app.getDB().getStrings(key, (String) defs.get(key)));
|
||||
editor.putString(key, ConfigLeanback.get(key, String.valueOf(value)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = app.getDB().getSettings(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) defs.get(key) : bs != 0);
|
||||
int bs = ConfigLeanback.get(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) value : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) defs.get(key));
|
||||
editor.putInt(key, (Integer) value);
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(defs.get(key)));
|
||||
editor.putString(key, String.valueOf(value));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) defs.get(key));
|
||||
editor.putString(key, (String) value);
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) defs.get(key));
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
52
app/src/main/java/com/topjohnwu/magisk/ConfigLeanback.kt
Normal file
52
app/src/main/java/com/topjohnwu/magisk/ConfigLeanback.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.data.repository.SettingRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object ConfigLeanback {
|
||||
|
||||
@JvmStatic
|
||||
val protectedContext: Context by inject(Protected)
|
||||
@JvmStatic
|
||||
val prefs: SharedPreferences by inject()
|
||||
|
||||
private val settingRepo: SettingRepository by inject()
|
||||
private val stringRepo: StringRepository by inject()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: Int) {
|
||||
settingRepo.put(key, value).subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: Int): Int =
|
||||
settingRepo.fetch(key, defaultValue).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: String?) {
|
||||
val task = value?.let { stringRepo.put(key, it) } ?: stringRepo.delete(key)
|
||||
task.subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: String?): String =
|
||||
stringRepo.fetch(key, defaultValue.orEmpty()).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun delete(key: String) {
|
||||
settingRepo.delete(key).subscribeK()
|
||||
}
|
||||
|
||||
}
|
@@ -1,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";
|
||||
}
|
||||
|
||||
|
||||
}
|
105
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
105
app/src/main/java/com/topjohnwu/magisk/Const.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Environment
|
||||
import android.os.Process
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
const val DEBUG_TAG = "MagiskManager"
|
||||
|
||||
// APK content
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
const val SU_KEYSTORE_KEY = "su_key"
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
@JvmField
|
||||
val EXTERNAL_PATH: File =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
@JvmField
|
||||
var MAGISK_DISABLE_FILE: File = File("xxx")
|
||||
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
|
||||
// Versions
|
||||
const val UPDATE_SERVICE_VER = 1
|
||||
const val SNET_EXT_VER = 12
|
||||
|
||||
@JvmField
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
// Generic
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
|
||||
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 SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk")
|
||||
@JvmField
|
||||
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
||||
|
||||
private fun getRaw(where: String, name: String) =
|
||||
"https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s".format(where, name)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
}
|
20
app/src/main/java/com/topjohnwu/magisk/Constants.kt
Normal file
20
app/src/main/java/com/topjohnwu/magisk/Constants.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Process
|
||||
|
||||
object Constants {
|
||||
|
||||
// Paths
|
||||
val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
val USER_ID get() = Process.myUid() / 100000
|
||||
|
||||
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
|
||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
||||
|
||||
const val GITHUB_URL = "https://github.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
|
||||
|
||||
}
|
47
app/src/main/java/com/topjohnwu/magisk/KConfig.kt
Normal file
47
app/src/main/java/com/topjohnwu/magisk/KConfig.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.KConfig.UpdateChannel.STABLE
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object KConfig : PreferenceModel() {
|
||||
|
||||
override val context: Context by inject(Protected)
|
||||
override val fileName: String = "${context.packageName}_preferences"
|
||||
|
||||
private var internalUpdateChannel by preference(Config.Key.UPDATE_CHANNEL, STABLE.id.toString())
|
||||
var useCustomTabs by preference("useCustomTabs", true)
|
||||
@JvmStatic
|
||||
var customUpdateChannel by preference(Config.Key.CUSTOM_CHANNEL, "")
|
||||
|
||||
@JvmStatic
|
||||
var updateChannel: UpdateChannel
|
||||
get() = UpdateChannel.byId(internalUpdateChannel.toIntOrNull() ?: STABLE.id)
|
||||
set(value) {
|
||||
internalUpdateChannel = value.id.toString()
|
||||
}
|
||||
|
||||
internal const val DEFAULT_CHANNEL = "topjohnwu/magisk_files"
|
||||
|
||||
enum class UpdateChannel(val id: Int) {
|
||||
|
||||
STABLE(Config.Value.STABLE_CHANNEL),
|
||||
BETA(Config.Value.BETA_CHANNEL),
|
||||
CANARY(Config.Value.CANARY_CHANNEL),
|
||||
CANARY_DEBUG(Config.Value.CANARY_DEBUG_CHANNEL),
|
||||
CUSTOM(Config.Value.CUSTOM_CHANNEL);
|
||||
|
||||
companion object {
|
||||
fun byId(id: Int) = when (id) {
|
||||
Config.Value.STABLE_CHANNEL -> STABLE
|
||||
Config.Value.BETA_CHANNEL -> BETA
|
||||
Config.Value.CUSTOM_CHANNEL -> CUSTOM
|
||||
Config.Value.CANARY_CHANNEL -> CANARY
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> CANARY_DEBUG
|
||||
else -> STABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
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 = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1)
|
||||
) = 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,69 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Constants
|
||||
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 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.firstOrNull()?.toPolicy(context.packageManager) }
|
||||
.doOnError {
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Constants.USER_ID)
|
||||
}
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toPolicy(context.packageManager) }
|
||||
.toList()
|
||||
|
||||
}
|
@@ -11,13 +11,15 @@ import com.topjohnwu.magisk.model.entity.Repo;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Deprecated
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private SQLiteDatabase mDb;
|
||||
private final SQLiteDatabase mDb;
|
||||
|
||||
@Deprecated
|
||||
public RepoDatabaseHelper(Context context) {
|
||||
super(context, "repo.db", null, DATABASE_VER);
|
||||
mDb = getWritableDatabase();
|
||||
@@ -46,32 +48,38 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Repo getRepo(String id) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[]{id}, null, null, null)) {
|
||||
if (c.moveToNext()) {
|
||||
return new Repo(c);
|
||||
}
|
||||
@@ -79,10 +87,12 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Cursor getRepoCursor() {
|
||||
String orderBy = null;
|
||||
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
|
||||
@@ -95,6 +105,7 @@ public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Set<String> getRepoIDSet() {
|
||||
HashSet<String> set = new HashSet<>(300);
|
||||
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||
|
@@ -0,0 +1,21 @@
|
||||
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<Insert> {
|
||||
values(key to value.toString())
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: Int = -1) = query<Select> {
|
||||
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<Insert> {
|
||||
values(key 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,163 @@
|
||||
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 StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
|
||||
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 StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.appendln(orderField)
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
}
|
||||
}
|
||||
|
||||
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(",") { "\"$it\"" }
|
||||
private var _values: Map<String, String> = mapOf()
|
||||
|
||||
fun values(vararg pairs: Pair<String, String>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, String>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln("($keys) VALUES($values)")
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
|
||||
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,63 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.model.entity.MagiskConfig
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Streaming
|
||||
import retrofit2.http.Url
|
||||
|
||||
|
||||
interface GithubRawApiServices {
|
||||
|
||||
//region topjohnwu/magisk_files
|
||||
|
||||
@GET("$MAGISK_FILES/master/stable.json")
|
||||
fun fetchConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
||||
fun fetchCanaryConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
||||
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET
|
||||
fun fetchCustomConfig(@Url url: String): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Constants.BOOTCTL_REVISION): Single<ResponseBody>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
fun fetchFile(@Url url: String): Single<ResponseBody>
|
||||
|
||||
|
||||
companion object {
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
|
||||
|
||||
private const val MAGISK_FILES = KConfig.DEFAULT_CHANNEL
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,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,43 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
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 ${Constants.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { Timber.i(it.toString()); it }
|
||||
|
||||
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,130 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
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.model.entity.Version
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.magisk.utils.writeToFile
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
||||
class MagiskRepository(
|
||||
private val context: Context,
|
||||
private val apiRaw: GithubRawApiServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchMagisk() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.magisk.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
|
||||
|
||||
fun fetchManager() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.app.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_APK) }
|
||||
|
||||
fun fetchUninstaller() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
|
||||
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
|
||||
|
||||
fun fetchSafetynet() = apiRaw
|
||||
.fetchSafetynet()
|
||||
.map { it.writeToFile(context, FILE_SAFETY_NET_APK) }
|
||||
|
||||
fun fetchBootctl() = apiRaw
|
||||
.fetchBootctl()
|
||||
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
|
||||
|
||||
|
||||
fun fetchConfig() = when (KConfig.updateChannel) {
|
||||
KConfig.UpdateChannel.STABLE -> apiRaw.fetchConfig()
|
||||
KConfig.UpdateChannel.BETA -> apiRaw.fetchBetaConfig()
|
||||
KConfig.UpdateChannel.CANARY -> apiRaw.fetchCanaryConfig()
|
||||
KConfig.UpdateChannel.CANARY_DEBUG -> apiRaw.fetchCanaryDebugConfig()
|
||||
KConfig.UpdateChannel.CUSTOM -> apiRaw.fetchCustomConfig(KConfig.customUpdateChannel)
|
||||
}
|
||||
.doOnSuccess {
|
||||
Config.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
|
||||
Config.magiskLink = it.magisk.link
|
||||
Config.magiskNoteLink = it.magisk.note
|
||||
Config.magiskMD5 = it.magisk.hash
|
||||
Config.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
|
||||
Config.remoteManagerVersionString = it.app.version
|
||||
Config.managerLink = it.app.link
|
||||
Config.managerNoteLink = it.app.note
|
||||
Config.uninstallerLink = it.uninstaller.link
|
||||
}
|
||||
|
||||
|
||||
fun fetchMagiskVersion(): Single<Version> = Single.zip(
|
||||
fetchMagiskVersionName(),
|
||||
fetchMagiskVersionCode(),
|
||||
BiFunction { versionName, versionCode ->
|
||||
Version(versionName, versionCode)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
fun 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()
|
||||
|
||||
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.substring(0 until it.indexOf(":")) }
|
||||
.onErrorReturn { "Unknown" }
|
||||
|
||||
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.toIntOrNull() ?: -1 }
|
||||
.onErrorReturn { -1 }
|
||||
|
||||
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
|
||||
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
|
||||
|
||||
private val Boolean.state get() = if (this) "add" else "rm"
|
||||
|
||||
companion object {
|
||||
const val FILE_MAGISK_ZIP = "magisk.zip"
|
||||
const val FILE_MAGISK_APK = "magisk.apk"
|
||||
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
|
||||
const val FILE_SAFETY_NET_APK = "safetynet.apk"
|
||||
const val FILE_BOOTCTL_SH = "bootctl"
|
||||
|
||||
private val blacklist = listOf(
|
||||
let { val app: App by inject(); app }.packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
"com.chrome.dev",
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
|
||||
class SettingRepository(private val settingsDao: SettingsDao) {
|
||||
|
||||
fun fetch(key: String, default: Int) = settingsDao.fetch(key, default)
|
||||
fun put(key: String, value: Int) = settingsDao.put(key, value)
|
||||
fun delete(key: String) = settingsDao.delete(key)
|
||||
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
|
||||
class StringRepository(private val stringDao: StringDao) {
|
||||
|
||||
fun fetch(key: String, default: String) = stringDao.fetch(key, default)
|
||||
fun put(key: String, value: String) = stringDao.put(key, value)
|
||||
fun delete(key: String) = stringDao.delete(key)
|
||||
|
||||
}
|
@@ -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,69 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.Constants
|
||||
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(), Constants.GITHUB_RAW_API_URL) }
|
||||
}
|
||||
|
||||
val networkingModule = module {}
|
||||
fun createOkHttpClient(): OkHttpClient {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.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.*
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {}
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { AppRepository(get()) }
|
||||
single { SettingRepository(get()) }
|
||||
single { StringRepository(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()) }
|
||||
|
@@ -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,11 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskApp(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String
|
||||
)
|
@@ -0,0 +1,10 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskConfig(
|
||||
val app: MagiskApp,
|
||||
val uninstaller: MagiskLink,
|
||||
val magisk: MagiskFlashable
|
||||
)
|
@@ -0,0 +1,13 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskFlashable(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String,
|
||||
@Json(name = "md5") val hash: String
|
||||
)
|
@@ -0,0 +1,8 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskLink(
|
||||
val link: String
|
||||
)
|
@@ -0,0 +1,58 @@
|
||||
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
|
||||
).mapValues { it.toString() }
|
||||
|
||||
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.Constants
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import io.reactivex.Single
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
|
||||
interface MagiskModule : Parcelable {
|
||||
val id: String
|
||||
val name: String
|
||||
val author: String
|
||||
val version: String
|
||||
val versionCode: String
|
||||
val description: String
|
||||
}
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repository(
|
||||
@PrimaryKey @NonNull
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val lastUpdate: Long
|
||||
) : MagiskModule
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
override val id: String,
|
||||
override val name: String,
|
||||
override val author: String,
|
||||
override val version: String,
|
||||
override val versionCode: String,
|
||||
override val description: String,
|
||||
val path: String
|
||||
) : MagiskModule
|
||||
|
||||
@AnyThread
|
||||
fun File.toModule(): Single<Module> {
|
||||
val path = "${Constants.MAGISK_PATH}/$name"
|
||||
return "dos2unix < $path/module.prop".su()
|
||||
.map { it.first().toModule(path) }
|
||||
}
|
||||
|
||||
fun Map<String, String>.toModule(path: String): Module {
|
||||
return Module(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
path = path
|
||||
)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun ResponseBody.toRepository(lastUpdate: Long) = string()
|
||||
.split(Regex("\\n"))
|
||||
.map { it.split("=", limit = 2) }
|
||||
.filter { it.size == 2 }
|
||||
.map { Pair(it[0], it[1]) }
|
||||
.toMap()
|
||||
.toRepository(lastUpdate)
|
||||
|
||||
@AnyThread
|
||||
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
|
||||
id = get("id").orEmpty(),
|
||||
name = get("name").orEmpty(),
|
||||
author = get("author").orEmpty(),
|
||||
version = get("version").orEmpty(),
|
||||
versionCode = get("versionCode").orEmpty(),
|
||||
description = get("description").orEmpty(),
|
||||
lastUpdate = lastUpdate
|
||||
)
|
@@ -0,0 +1,96 @@
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = getAsInteger("uid")
|
||||
val packageName = getAsString("package_name")
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = getAsInteger("policy"),
|
||||
until = getAsInteger("until").toLong(),
|
||||
logging = getAsInteger("logging") != 0,
|
||||
notification = getAsInteger("notification") != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
||||
fun MagiskPolicy.toContentValues() = ContentValues().apply {
|
||||
put("uid", uid)
|
||||
put("uid", uid)
|
||||
put("package_name", packageName)
|
||||
put("policy", policy)
|
||||
put("until", until)
|
||||
put("logging", if (logging) 1 else 0)
|
||||
put("notification", if (notification) 1 else 0)
|
||||
}*/
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to if (logging) 1 else 0,
|
||||
"notification" to if (notification) 1 else 0
|
||||
).mapValues { it.value.toString() }
|
||||
|
||||
@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();
|
||||
}
|
@@ -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.dlString(getPropUrl()).split("\\n");
|
||||
try {
|
||||
parseProps(props);
|
||||
} catch (NumberFormatException e) {
|
||||
|
@@ -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,3 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
data class Version(val version: String, val versionCode: Int)
|
@@ -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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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,51 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
|
||||
abstract class PreferenceModel(
|
||||
private val commitPrefs: Boolean = false
|
||||
) {
|
||||
|
||||
protected abstract val fileName: String
|
||||
protected abstract val context: Context
|
||||
|
||||
internal val prefs get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Boolean> = BooleanProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Float> = FloatProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Int> = IntProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Long> = LongProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, String> = StringProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Set<String>> = 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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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 runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: 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,11 +6,13 @@ 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.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.inject
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
@@ -18,6 +20,8 @@ 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)
|
||||
@@ -62,12 +65,12 @@ 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))
|
||||
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 -> {
|
||||
|
@@ -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,31 @@
|
||||
package com.topjohnwu.magisk.model.update
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.model.entity.MagiskConfig
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
|
||||
class UpdateCheckService : DelegateWorker() {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
||||
override fun doWork(): ListenableWorker.Result {
|
||||
val config = runCatching { magiskRepo.fetchConfig().blockingGet() }
|
||||
config.getOrNull()?.let { checkUpdates(it) }
|
||||
return when {
|
||||
config.isFailure -> ListenableWorker.Result.failure()
|
||||
else -> ListenableWorker.Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdates(config: MagiskConfig) {
|
||||
when {
|
||||
BuildConfig.VERSION_CODE < config.app.versionCode.toIntOrNull() ?: -1 -> Notifications.managerUpdate()
|
||||
Config.magiskVersionCode < config.magisk.versionCode.toIntOrNull() ?: -1 -> Notifications.magiskUpdate()
|
||||
}
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
@@ -31,18 +31,27 @@ import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import io.reactivex.Single;
|
||||
|
||||
@Deprecated
|
||||
public class UpdateRepos {
|
||||
private static final DateFormat DATE_FORMAT;
|
||||
|
||||
private final App app = App.self;
|
||||
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"));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private final RepoDatabaseHelper repoDB;
|
||||
private Set<String> cached;
|
||||
private Queue<Pair<String, Date>> moduleQueue;
|
||||
|
||||
public UpdateRepos(@NonNull RepoDatabaseHelper repoDatabase) {
|
||||
repoDB = repoDatabase;
|
||||
}
|
||||
|
||||
private void runTasks(Runnable task) {
|
||||
Future[] futures = new Future[App.THREAD_POOL.getMaximumPoolSize() - 1];
|
||||
for (int i = 0; i < futures.length; ++i) {
|
||||
@@ -54,7 +63,8 @@ public class UpdateRepos {
|
||||
f.get();
|
||||
} catch (InterruptedException e) {
|
||||
continue;
|
||||
} catch (ExecutionException ignored) {}
|
||||
} catch (ExecutionException ignored) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -116,17 +126,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 +144,7 @@ public class UpdateRepos {
|
||||
}
|
||||
|
||||
private void fullReload() {
|
||||
Cursor c = app.getRepoDB().getRawCursor();
|
||||
Cursor c = repoDB.getRawCursor();
|
||||
runTasks(() -> {
|
||||
while (true) {
|
||||
Repo repo;
|
||||
@@ -145,32 +155,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);
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,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()
|
||||
}
|
||||
|
||||
|
@@ -5,15 +5,15 @@ import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
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.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 +21,7 @@ open class SplashActivity : AppCompatActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Shell.getShell {
|
||||
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
|
||||
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.unsupport_magisk_title)
|
||||
.setMessage(R.string.unsupport_magisk_message)
|
||||
@@ -48,9 +48,6 @@ open class SplashActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Dynamic detect all locales
|
||||
LocaleManager.loadAvailableLocales(R.string.app_changelog)
|
||||
|
||||
// Set default configs
|
||||
Config.initialize()
|
||||
|
||||
@@ -59,7 +56,7 @@ open class SplashActivity : AppCompatActivity() {
|
||||
|
||||
// Schedule periodic update checks
|
||||
Utils.scheduleUpdateCheck()
|
||||
CheckUpdates.check()
|
||||
//CheckUpdates.check()
|
||||
|
||||
// Setup shortcuts
|
||||
Shortcuts.setup(this)
|
||||
@@ -67,13 +64,14 @@ open class SplashActivity : AppCompatActivity() {
|
||||
// Magisk working as expected
|
||||
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
|
||||
// Load modules
|
||||
Utils.loadModules(false)
|
||||
//Utils.loadModules(false)
|
||||
// Load repos
|
||||
if (Networking.checkNetworkStatus(this))
|
||||
UpdateRepos().exec()
|
||||
if (Networking.checkNetworkStatus(this)) {
|
||||
get<UpdateRepos>().exec().subscribeK()
|
||||
}
|
||||
}
|
||||
|
||||
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,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,85 @@
|
||||
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.Config
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
|
||||
import com.topjohnwu.magisk.data.repository.SettingRepository
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
protected val repoDatabase: RepoDatabaseHelper by inject()
|
||||
protected val prefs: SharedPreferences by inject()
|
||||
protected val app: App by inject()
|
||||
protected val settingRepo: SettingRepository 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 setCustomUpdateChannel(userRepo: String) {
|
||||
KConfig.customUpdateChannel = userRepo
|
||||
}
|
||||
|
||||
protected fun getChannelCompat(channel: Int): KConfig.UpdateChannel {
|
||||
return when (channel) {
|
||||
Config.Value.STABLE_CHANNEL,
|
||||
Config.Value.DEFAULT_CHANNEL -> KConfig.UpdateChannel.STABLE
|
||||
Config.Value.BETA_CHANNEL -> KConfig.UpdateChannel.BETA
|
||||
Config.Value.CANARY_CHANNEL -> KConfig.UpdateChannel.CANARY
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> KConfig.UpdateChannel.CANARY_DEBUG
|
||||
Config.Value.CUSTOM_CHANNEL -> KConfig.UpdateChannel.CUSTOM
|
||||
else -> KConfig.updateChannel
|
||||
}
|
||||
}
|
||||
}
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
||||
}
|
@@ -1,23 +1,26 @@
|
||||
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.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)
|
||||
@@ -83,8 +86,6 @@ class HomeViewModel(
|
||||
private var shownDialog = false
|
||||
|
||||
init {
|
||||
Event.register(this)
|
||||
|
||||
isForceEncryption.addOnPropertyChangedCallback {
|
||||
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
|
||||
}
|
||||
@@ -95,13 +96,6 @@ class HomeViewModel(
|
||||
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,23 +154,29 @@ 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.fetchConfig()
|
||||
.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 {
|
||||
it.app.let {
|
||||
Config.remoteManagerVersionCode = it.versionCode.toIntOrNull() ?: -1
|
||||
Config.remoteManagerVersionString = it.version
|
||||
}
|
||||
it.magisk.let {
|
||||
Config.remoteMagiskVersionCode = it.versionCode.toIntOrNull() ?: -1
|
||||
Config.remoteMagiskVersionString = it.version
|
||||
}
|
||||
updateSelf()
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
hasRoot.value = Shell.rootAccess()
|
||||
|
||||
if (Networking.checkNetworkStatus(context)) {
|
||||
CheckUpdates.check()
|
||||
} else {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelf() {
|
||||
|
@@ -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,14 @@ 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.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,7 +27,7 @@ import java.util.*
|
||||
|
||||
class LogViewModel(
|
||||
private val resources: Resources,
|
||||
private val database: MagiskDB
|
||||
private val logRepo: LogRepository
|
||||
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
|
||||
|
||||
val items = DiffObservableList(ComparableRvItem.callback)
|
||||
@@ -58,9 +57,10 @@ class LogViewModel(
|
||||
else -> ""
|
||||
}
|
||||
|
||||
fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true }
|
||||
.subscribeK()
|
||||
.add()
|
||||
fun refresh() {
|
||||
fetchLogs().subscribeK { logItem.update(it) }
|
||||
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
val now = Calendar.getInstance()
|
||||
@@ -88,35 +88,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
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -56,7 +56,7 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
Config.get<Int>(Config.Key.REPO_ORDER)!!
|
||||
) { d, which ->
|
||||
Config.set(Config.Key.REPO_ORDER, which)
|
||||
viewModel.updateRepos()
|
||||
viewModel.refresh(false)
|
||||
d.dismiss()
|
||||
}.show()
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
|
||||
|
||||
fun download(install: Boolean) {
|
||||
context.runWithExternalRW {
|
||||
val intent = Intent(activity, ClassMap.get<Any>(DownloadModuleService::class.java))
|
||||
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) //hmm, service starts itself in foreground, this seems unnecessary
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.ui.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -11,11 +12,11 @@ import android.widget.Toast;
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.KConfig;
|
||||
import com.topjohnwu.magisk.KConfig.UpdateChannel;
|
||||
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;
|
||||
@@ -26,7 +27,6 @@ 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;
|
||||
@@ -34,26 +34,61 @@ import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.preference.SwitchPreferenceCompat;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import kotlin.Pair;
|
||||
|
||||
public class SettingsFragment extends BasePreferenceFragment {
|
||||
public final class SettingsFragment extends BasePreferenceFragment {
|
||||
|
||||
private ListPreference updateChannel, autoRes, suNotification,
|
||||
requestTimeout, rootConfig, multiuserConfig, nsConfig;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@SuppressLint("CheckResult")
|
||||
private static void setLocalePreference(ListPreference lp) {
|
||||
lp.setSummary("Loading");
|
||||
lp.setEnabled(false);
|
||||
LocaleManager.getAvailableLocales()
|
||||
.flattenAsFlowable(locales -> locales)
|
||||
.map(locale -> new Pair<>(locale.getDisplayName(locale), LocaleManager.toLanguageTag(locale)))
|
||||
.toList()
|
||||
.map(list -> {
|
||||
CharSequence[] names = new CharSequence[list.size() + 1];
|
||||
CharSequence[] values = new CharSequence[list.size() + 1];
|
||||
|
||||
names[0] = LocaleManager.getString(LocaleManager.getDefaultLocale(), R.string.system_default);
|
||||
values[0] = "";
|
||||
int i = 1;
|
||||
for (Pair<String, String> item : list) {
|
||||
names[i] = item.getFirst();
|
||||
values[i++] = item.getSecond();
|
||||
}
|
||||
return new Pair<>(names, values);
|
||||
})
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(it -> {
|
||||
lp.setEnabled(true);
|
||||
lp.setEntries(it.getFirst());
|
||||
lp.setEntryValues(it.getSecond());
|
||||
lp.setSummary(LocaleManager.getLocale().getDisplayName(LocaleManager.getLocale()));
|
||||
}, Throwable::printStackTrace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
public final void onStart() {
|
||||
super.onStart();
|
||||
setHasOptionsMenu(true);
|
||||
requireActivity().setTitle(R.string.settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
public final void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
getPreferenceManager().setStorageDeviceProtected();
|
||||
setPreferencesFromResource(R.xml.app_settings, rootKey);
|
||||
|
||||
boolean showSuperuser = Utils.showSuperUser();
|
||||
app.getPrefs().edit()
|
||||
getPrefs().edit()
|
||||
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
|
||||
.apply();
|
||||
|
||||
@@ -73,14 +108,17 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
return true;
|
||||
});
|
||||
findPreference("clear").setOnPreferenceClickListener(pref -> {
|
||||
app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
|
||||
app.getRepoDB().clearRepo();
|
||||
getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
|
||||
getRepoDatabase().clearRepo();
|
||||
//getModuleRepo().deleteAllCached().subscribeOn(Schedulers.io()).subscribe(() -> {
|
||||
//}, throwable -> {
|
||||
//});
|
||||
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.loadModules();
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT);
|
||||
return true;
|
||||
});
|
||||
@@ -96,26 +134,30 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
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) {
|
||||
|
||||
final UpdateChannel previousUpdateChannel = KConfig.getUpdateChannel();
|
||||
final UpdateChannel updateChannel = getChannelCompat(channel);
|
||||
|
||||
KConfig.setUpdateChannel(updateChannel);
|
||||
|
||||
if (updateChannel == UpdateChannel.CUSTOM) {
|
||||
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, ""));
|
||||
url.setText(KConfig.getCustomUpdateChannel());
|
||||
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))
|
||||
.setPositiveButton(R.string.ok, (d, i) -> setCustomUpdateChannel(url.getText().toString()))
|
||||
.setNegativeButton(R.string.close, (d, i) -> KConfig.setUpdateChannel(previousUpdateChannel))
|
||||
.setOnCancelListener(d -> KConfig.setUpdateChannel(previousUpdateChannel))
|
||||
.show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
setLocalePreference((ListPreference) findPreference(Config.Key.LOCALE));
|
||||
|
||||
/* We only show canary channels if user is already on canary channel
|
||||
* or the user have already chosen canary channel */
|
||||
if (!Utils.isCanary() &&
|
||||
@@ -147,11 +189,12 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
if (Shell.rootAccess() && Const.USER_ID == 0) {
|
||||
if (app.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
||||
if (getApp().getPackageName().equals(BuildConfig.APPLICATION_ID)) {
|
||||
generalCatagory.removePreference(restoreManager);
|
||||
} else {
|
||||
if (!Networking.checkNetworkStatus(app))
|
||||
if (!Networking.checkNetworkStatus(requireContext())) {
|
||||
generalCatagory.removePreference(restoreManager);
|
||||
}
|
||||
generalCatagory.removePreference(hideManager);
|
||||
}
|
||||
} else {
|
||||
@@ -169,28 +212,15 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
public final 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));
|
||||
getSettingRepo().put(key, Utils.getPrefsInt(prefs, key))
|
||||
.subscribe(() -> {
|
||||
}, Throwable::printStackTrace);
|
||||
break;
|
||||
case Config.Key.DARK_THEME:
|
||||
requireActivity().recreate();
|
||||
@@ -199,7 +229,8 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
if (prefs.getBoolean(key, false)) {
|
||||
try {
|
||||
Const.MAGISK_DISABLE_FILE.createNewFile();
|
||||
} catch (IOException ignored) {}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
} else {
|
||||
Const.MAGISK_DISABLE_FILE.delete();
|
||||
}
|
||||
@@ -213,12 +244,12 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
break;
|
||||
case Config.Key.LOCALE:
|
||||
LocaleManager.setLocale(app);
|
||||
LocaleManager.setLocale(getApp());
|
||||
requireActivity().recreate();
|
||||
break;
|
||||
case Config.Key.UPDATE_CHANNEL:
|
||||
case Config.Key.CUSTOM_CHANNEL:
|
||||
CheckUpdates.check();
|
||||
//CheckUpdates.check();
|
||||
break;
|
||||
case Config.Key.CHECK_UPDATES:
|
||||
Utils.scheduleUpdateCheck();
|
||||
@@ -228,7 +259,7 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(Preference preference) {
|
||||
public final boolean onPreferenceTreeClick(Preference preference) {
|
||||
String key = preference.getKey();
|
||||
switch (key) {
|
||||
case Config.Key.SU_FINGERPRINT:
|
||||
@@ -253,27 +284,27 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
break;
|
||||
case Config.Key.ROOT_ACCESS:
|
||||
rootConfig.setSummary(getResources()
|
||||
.getStringArray(R.array.su_access)[(int)Config.get(key)]);
|
||||
.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)]);
|
||||
.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)]);
|
||||
.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)));
|
||||
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)]);
|
||||
.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)]);
|
||||
.getStringArray(R.array.namespace_summary)[(int) Config.get(key)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -287,14 +318,4 @@ public class SettingsFragment extends BasePreferenceFragment {
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -24,7 +25,7 @@ import io.reactivex.Single
|
||||
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
|
||||
@@ -50,9 +51,9 @@ class SuperuserViewModel(
|
||||
}
|
||||
|
||||
fun updatePolicies() {
|
||||
Single.fromCallable { database.policyList }
|
||||
appRepo.fetchAll()
|
||||
.flattenAsFlowable { it }
|
||||
.map { PolicyRvItem(it, it.info.loadIcon(packageManager)) }
|
||||
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
|
||||
.toList()
|
||||
.applySchedulers()
|
||||
.applyViewModel(this)
|
||||
@@ -85,28 +86,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 +130,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.get<Int>(Config.Key.SU_AUTO_RESPONSE)) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
handler?.handleAction(Policy.DENY, 0)
|
||||
return true
|
||||
@@ -168,7 +184,6 @@ class SuRequestViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
createUICallback()
|
||||
showUI()
|
||||
return true
|
||||
}
|
||||
|
@@ -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
|
||||
@@ -115,7 +113,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 +124,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 +135,7 @@ fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListe
|
||||
listener.onChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@BindingAdapter("onTouch")
|
||||
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
|
||||
@@ -155,8 +153,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 +160,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)
|
||||
}
|
||||
|
||||
|
@@ -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,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.get<String>(Config.Key.LOCALE)
|
||||
locale = when {
|
||||
localeConfig.isNullOrEmpty() -> 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)
|
||||
}
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
package com.topjohnwu.magisk.utils;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Process;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.model.entity.Policy;
|
||||
import com.topjohnwu.magisk.model.entity.SuLogEntry;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class SuLogger {
|
||||
|
||||
public static void handleLogs(Intent intent) {
|
||||
|
||||
int fromUid = intent.getIntExtra("from.uid", -1);
|
||||
if (fromUid < 0) return;
|
||||
if (fromUid == Process.myUid()) return;
|
||||
|
||||
App app = App.self;
|
||||
PackageManager pm = app.getPackageManager();
|
||||
Policy policy;
|
||||
|
||||
boolean notify;
|
||||
Bundle data = intent.getExtras();
|
||||
if (data.containsKey("notify")) {
|
||||
notify = data.getBoolean("notify");
|
||||
try {
|
||||
policy = new Policy(fromUid, pm);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Doesn't report whether notify or not, check database ourselves
|
||||
policy = app.getDB().getPolicy(fromUid);
|
||||
if (policy == null)
|
||||
return;
|
||||
notify = policy.notification;
|
||||
}
|
||||
|
||||
policy.policy = data.getInt("policy", -1);
|
||||
if (policy.policy < 0)
|
||||
return;
|
||||
|
||||
if (notify)
|
||||
handleNotify(policy);
|
||||
|
||||
SuLogEntry log = new SuLogEntry(policy);
|
||||
|
||||
int toUid = intent.getIntExtra("to.uid", -1);
|
||||
if (toUid < 0) return;
|
||||
int pid = intent.getIntExtra("pid", -1);
|
||||
if (pid < 0) return;
|
||||
String command = intent.getStringExtra("command");
|
||||
if (command == null) return;
|
||||
log.toUid = toUid;
|
||||
log.fromPid = pid;
|
||||
log.command = command;
|
||||
log.date = new Date();
|
||||
app.getDB().addLog(log);
|
||||
}
|
||||
|
||||
private static void handleNotify(Policy policy) {
|
||||
if (policy.notification &&
|
||||
(int) Config.get(Config.Key.SU_NOTIFICATION) == Config.Value.NOTIFICATION_TOAST) {
|
||||
Utils.toast(App.self.getString(policy.policy == Policy.ALLOW ?
|
||||
R.string.su_allow_toast : R.string.su_deny_toast, policy.appName),
|
||||
Toast.LENGTH_SHORT);
|
||||
}
|
||||
}
|
||||
|
||||
public static void handleNotify(Intent intent) {
|
||||
int fromUid = intent.getIntExtra("from.uid", -1);
|
||||
if (fromUid < 0) return;
|
||||
if (fromUid == Process.myUid()) return;
|
||||
try {
|
||||
Policy policy = new Policy(fromUid, App.self.getPackageManager());
|
||||
policy.policy = intent.getIntExtra("policy", -1);
|
||||
if (policy.policy >= 0)
|
||||
handleNotify(policy);
|
||||
} catch (PackageManager.NameNotFoundException ignored) {}
|
||||
}
|
||||
}
|
94
app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.kt
Normal file
94
app/src/main/java/com/topjohnwu/magisk/utils/SuLogger.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import java.util.*
|
||||
|
||||
object SuLogger {
|
||||
|
||||
@JvmStatic
|
||||
fun handleLogs(intent: Intent) {
|
||||
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
|
||||
val pm: PackageManager by inject()
|
||||
|
||||
val notify: Boolean
|
||||
val data = intent.extras
|
||||
val policy: MagiskPolicy = if (data!!.containsKey("notify")) {
|
||||
notify = data.getBoolean("notify")
|
||||
runCatching {
|
||||
fromUid.toPolicy(pm)
|
||||
}.getOrElse { return }
|
||||
} else {
|
||||
// Doesn't report whether notify or not, check database ourselves
|
||||
val appRepo: AppRepository by inject()
|
||||
val policy = appRepo.fetch(fromUid).blockingGet() ?: return
|
||||
notify = policy.notification
|
||||
policy
|
||||
}.copy(policy = data.getInt("policy", -1))
|
||||
|
||||
if (policy.policy < 0)
|
||||
return
|
||||
|
||||
if (notify)
|
||||
handleNotify(policy)
|
||||
|
||||
val toUid = intent.getIntExtra("to.uid", -1)
|
||||
if (toUid < 0) return
|
||||
|
||||
val pid = intent.getIntExtra("pid", -1)
|
||||
if (pid < 0) return
|
||||
|
||||
val command = intent.getStringExtra("command") ?: return
|
||||
val log = policy.toLog(
|
||||
toUid = toUid,
|
||||
fromPid = pid,
|
||||
command = command,
|
||||
date = Date()
|
||||
)
|
||||
|
||||
val logRepo: LogRepository by inject()
|
||||
logRepo.put(log).blockingGet()?.printStackTrace()
|
||||
}
|
||||
|
||||
private fun handleNotify(policy: MagiskPolicy) {
|
||||
if (policy.notification && Config.get<Any>(Config.Key.SU_NOTIFICATION) as Int == Config.Value.NOTIFICATION_TOAST) {
|
||||
Utils.toast(
|
||||
App.self.getString(
|
||||
if (policy.policy == Policy.ALLOW)
|
||||
R.string.su_allow_toast
|
||||
else
|
||||
R.string.su_deny_toast, policy.appName
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun handleNotify(intent: Intent) {
|
||||
val fromUid = intent.getIntExtra("from.uid", -1)
|
||||
if (fromUid < 0) return
|
||||
if (fromUid == Process.myUid()) return
|
||||
runCatching {
|
||||
val packageManager: PackageManager by inject()
|
||||
val policy = fromUid.toPolicy(packageManager)
|
||||
.copy(policy = intent.getIntExtra("policy", -1))
|
||||
if (policy.policy >= 0)
|
||||
handleNotify(policy)
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,24 +7,16 @@ import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.BuildConfig;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.magisk.model.entity.Module;
|
||||
import com.topjohnwu.magisk.model.entity.OldModule;
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
@@ -35,6 +27,14 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.work.Constraints;
|
||||
import androidx.work.ExistingPeriodicWorkPolicy;
|
||||
import androidx.work.ListenableWorker;
|
||||
import androidx.work.NetworkType;
|
||||
import androidx.work.PeriodicWorkRequest;
|
||||
import androidx.work.WorkManager;
|
||||
|
||||
public class Utils {
|
||||
|
||||
public static void toast(CharSequence msg, int duration) {
|
||||
@@ -72,7 +72,7 @@ public class Utils {
|
||||
if (info.labelRes > 0) {
|
||||
Resources res = pm.getResourcesForApplication(info);
|
||||
Configuration config = new Configuration();
|
||||
config.setLocale(LocaleManager.locale);
|
||||
config.setLocale(LocaleManager.getLocale());
|
||||
res.updateConfiguration(config, res.getDisplayMetrics());
|
||||
return res.getString(info.labelRes);
|
||||
}
|
||||
@@ -86,28 +86,19 @@ public class Utils {
|
||||
.replace("#", "").replace("@", "").replace("\\", "_");
|
||||
}
|
||||
|
||||
public static void loadModules() {
|
||||
loadModules(true);
|
||||
}
|
||||
|
||||
public static void loadModules(boolean async) {
|
||||
Event.reset(Event.MODULE_LOAD_DONE);
|
||||
Runnable run = () -> {
|
||||
Map<String, Module> moduleMap = new ValueSortedMap<>();
|
||||
SuFile path = new SuFile(Const.MAGISK_PATH);
|
||||
SuFile[] modules = path.listFiles(
|
||||
(file, name) -> !name.equals("lost+found") && !name.equals(".core"));
|
||||
for (SuFile file : modules) {
|
||||
if (file.isFile()) continue;
|
||||
Module module = new Module(Const.MAGISK_PATH + "/" + file.getName());
|
||||
moduleMap.put(module.getId(), module);
|
||||
}
|
||||
Event.trigger(Event.MODULE_LOAD_DONE, moduleMap);
|
||||
};
|
||||
if (async)
|
||||
App.THREAD_POOL.execute(run);
|
||||
else
|
||||
run.run();
|
||||
@WorkerThread
|
||||
public static Map<String, OldModule> loadModulesLeanback() {
|
||||
final Map<String, OldModule> moduleMap = new ValueSortedMap<>();
|
||||
final SuFile path = new SuFile(Const.MAGISK_PATH);
|
||||
final SuFile[] modules = path.listFiles((file, name) ->
|
||||
!name.equals("lost+found") && !name.equals(".core")
|
||||
);
|
||||
for (SuFile file : modules) {
|
||||
if (file.isFile()) continue;
|
||||
OldModule module = new OldModule(Const.MAGISK_PATH + "/" + file.getName());
|
||||
moduleMap.put(module.getId(), module);
|
||||
}
|
||||
return moduleMap;
|
||||
}
|
||||
|
||||
public static boolean showSuperUser() {
|
||||
@@ -120,13 +111,17 @@ public class Utils {
|
||||
return BuildConfig.VERSION_NAME.contains("-");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static void scheduleUpdateCheck() {
|
||||
if (Config.get(Config.Key.CHECK_UPDATES)) {
|
||||
Constraints constraints = new Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
//ensures that notification doesn't pop up every time user starts the app
|
||||
.setRequiresDeviceIdle(true)
|
||||
.build();
|
||||
Class<? extends ListenableWorker> service = (Class<? extends ListenableWorker>) ClassMap.get(UpdateCheckService.class);
|
||||
PeriodicWorkRequest request = new PeriodicWorkRequest
|
||||
.Builder(ClassMap.get(UpdateCheckService.class), 12, TimeUnit.HOURS)
|
||||
.Builder(service, 12, TimeUnit.HOURS)
|
||||
.setConstraints(constraints)
|
||||
.build();
|
||||
WorkManager.getInstance().enqueueUniquePeriodicWork(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -9,6 +10,7 @@ import android.content.pm.PackageManager.*
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import com.topjohnwu.magisk.App
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
val packageName: String
|
||||
@@ -80,3 +82,22 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.readUri(uri: Uri) = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun ApplicationInfo.findAppLabel(pm: PackageManager): String {
|
||||
return pm.getApplicationLabel(this)?.toString().orEmpty()
|
||||
}
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun File.provide(): Uri {
|
||||
val context: Context by inject()
|
||||
return FileProvider.getUriForFile(context, "com.topjohnwu.magisk.fileprovider", this)
|
||||
}
|
||||
|
||||
fun File.mv(destination: File) {
|
||||
inputStream().copyTo(destination)
|
||||
deleteRecursively()
|
||||
}
|
||||
|
||||
fun String.toFile() = File(this)
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
38
app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt
Normal file
38
app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toFile
|
||||
import org.kamranzafar.jtar.TarInputStream
|
||||
import org.kamranzafar.jtar.TarOutputStream
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.copyTo(file: File) = toFile().copyTo(file)
|
||||
fun InputStream.copyTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
fun File.tarInputStream() = TarInputStream(inputStream())
|
||||
fun File.tarOutputStream() = TarOutputStream(this)
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,18 +3,15 @@ package com.topjohnwu.magisk.utils
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
import org.koin.core.scope.Scope
|
||||
|
||||
fun getKoin() = GlobalContext.get().koin
|
||||
|
||||
inline fun <reified T : Any> inject(
|
||||
qualifier: Qualifier? = null,
|
||||
scope: Scope? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { get<T>(qualifier, scope, parameters) }
|
||||
) = lazy { get<T>(qualifier, parameters) }
|
||||
|
||||
inline fun <reified T : Any> get(
|
||||
qualifier: Qualifier? = null,
|
||||
scope: Scope? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): T = getKoin().get(qualifier, scope, parameters)
|
||||
): T = getKoin().get(qualifier, parameters)
|
54
app/src/main/java/com/topjohnwu/magisk/utils/XNetwork.kt
Normal file
54
app/src/main/java/com/topjohnwu/magisk/utils/XNetwork.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.File
|
||||
|
||||
fun ResponseBody.writeToFile(context: Context, fileName: String): File {
|
||||
val file = File(context.cacheDir, fileName)
|
||||
withStreams(byteStream(), file.outputStream()) { inStream, outStream ->
|
||||
inStream.copyTo(outStream)
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun ResponseBody.writeToString() = string()
|
||||
|
||||
fun String.launch() = if (KConfig.useCustomTabs) {
|
||||
launchWithCustomTabs()
|
||||
} else {
|
||||
launchWithIntent()
|
||||
}
|
||||
|
||||
|
||||
private fun String.launchWithCustomTabs() {
|
||||
val context: Context by inject()
|
||||
val primaryColor = ContextCompat.getColor(context, R.color.colorPrimary)
|
||||
val secondaryColor = ContextCompat.getColor(context, R.color.colorSecondary)
|
||||
|
||||
CustomTabsIntent.Builder()
|
||||
.enableUrlBarHiding()
|
||||
.setShowTitle(true)
|
||||
.setToolbarColor(primaryColor)
|
||||
.setSecondaryToolbarColor(secondaryColor)
|
||||
.build()
|
||||
.apply { intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK }
|
||||
.launchUrl(context, this.toUri())
|
||||
}
|
||||
|
||||
private fun String.launchWithIntent() {
|
||||
val context: Context by inject()
|
||||
|
||||
Intent(Intent.ACTION_VIEW)
|
||||
.apply {
|
||||
data = toUri()
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
.startActivity(context)
|
||||
}
|
19
app/src/main/java/com/topjohnwu/magisk/utils/XSU.kt
Normal file
19
app/src/main/java/com/topjohnwu/magisk/utils/XSU.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
|
||||
fun reboot(recovery: Boolean = false): Shell.Result {
|
||||
val command = StringBuilder("/system/bin/reboot")
|
||||
.appendIf(recovery) {
|
||||
append(" recovery")
|
||||
}
|
||||
.toString()
|
||||
|
||||
return Shell.su(command).exec()
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
@@ -12,7 +12,12 @@ fun String.replaceRandomWithSpecial(): String {
|
||||
return replace(random, specialChars.random())
|
||||
}
|
||||
|
||||
fun StringBuilder.appendIf(condition: Boolean, builder: StringBuilder.() -> Unit) =
|
||||
if (condition) apply(builder) else this
|
||||
|
||||
fun Int.res(vararg args: Any): String {
|
||||
val resources: Resources by inject()
|
||||
return resources.getString(this, *args)
|
||||
}
|
||||
}
|
||||
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
@@ -1,18 +1,20 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
val now get() = System.currentTimeMillis()
|
||||
|
||||
fun Long.toTime(format: SimpleDateFormat) = format.format(this).orEmpty()
|
||||
fun String.toTime(format: SimpleDateFormat) = try {
|
||||
fun Long.toTime(format: DateFormat) = format.format(this).orEmpty()
|
||||
fun String.toTime(format: DateFormat) = try {
|
||||
format.parse(this)?.time ?: -1
|
||||
} catch (e: ParseException) {
|
||||
-1L
|
||||
}
|
||||
|
||||
private val locale get() = Locale.getDefault()
|
||||
val timeFormatFull by lazy { SimpleDateFormat("YYYY/MM/DD_HH:mm:ss", locale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("YYYY-MM-DD'T'HH:mm:ss'Z'", locale) }
|
||||
private val locale get() = LocaleManager.locale
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", locale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", locale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", LocaleManager.locale) }
|
14
app/src/main/java/com/topjohnwu/magisk/utils/XView.kt
Normal file
14
app/src/main/java/com/topjohnwu/magisk/utils/XView.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
fun View.setOnViewReadyListener(callback: () -> Unit) = addOnGlobalLayoutListener(true, callback)
|
||||
|
||||
fun View.addOnGlobalLayoutListener(oneShot: Boolean = false, callback: () -> Unit) =
|
||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (oneShot) viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
callback()
|
||||
}
|
||||
})
|
@@ -1,40 +0,0 @@
|
||||
package com.topjohnwu.magisk.view;
|
||||
|
||||
public abstract class Expandable {
|
||||
|
||||
private boolean mExpanded = false;
|
||||
|
||||
public final boolean isExpanded() {
|
||||
return mExpanded;
|
||||
}
|
||||
|
||||
public final void setExpanded(boolean expanded) {
|
||||
mExpanded = expanded;
|
||||
onSetExpanded(expanded);
|
||||
}
|
||||
|
||||
public final void expand() {
|
||||
if (mExpanded)
|
||||
return;
|
||||
onExpand();
|
||||
mExpanded = true;
|
||||
}
|
||||
|
||||
public final void collapse() {
|
||||
if (!mExpanded)
|
||||
return;
|
||||
onCollapse();
|
||||
mExpanded = false;
|
||||
}
|
||||
|
||||
protected abstract void onExpand();
|
||||
|
||||
protected abstract void onCollapse();
|
||||
|
||||
protected void onSetExpanded(boolean expanded) {
|
||||
if (expanded)
|
||||
onExpand();
|
||||
else
|
||||
onCollapse();
|
||||
}
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
package com.topjohnwu.magisk.view;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
public class ExpandableViewHolder extends Expandable {
|
||||
|
||||
private ViewGroup expandLayout;
|
||||
private ValueAnimator expandAnimator, collapseAnimator;
|
||||
private int expandHeight = 0;
|
||||
|
||||
public ExpandableViewHolder(ViewGroup viewGroup) {
|
||||
expandLayout = viewGroup;
|
||||
setExpanded(false);
|
||||
expandLayout.getViewTreeObserver().addOnPreDrawListener(
|
||||
new ViewTreeObserver.OnPreDrawListener() {
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
if (expandHeight == 0) {
|
||||
expandLayout.measure(0, 0);
|
||||
expandHeight = expandLayout.getMeasuredHeight();
|
||||
}
|
||||
|
||||
expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
expandAnimator = slideAnimator(0, expandHeight);
|
||||
collapseAnimator = slideAnimator(expandHeight, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExpand() {
|
||||
expandLayout.setVisibility(View.VISIBLE);
|
||||
expandAnimator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCollapse() {
|
||||
collapseAnimator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetExpanded(boolean expanded) {
|
||||
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||
layoutParams.height = expanded ? expandHeight : 0;
|
||||
expandLayout.setLayoutParams(layoutParams);
|
||||
}
|
||||
|
||||
private ValueAnimator slideAnimator(int start, int end) {
|
||||
ValueAnimator animator = ValueAnimator.ofInt(start, end);
|
||||
|
||||
animator.addUpdateListener(valueAnimator -> {
|
||||
int value = (Integer) valueAnimator.getAnimatedValue();
|
||||
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||
layoutParams.height = value;
|
||||
expandLayout.setLayoutParams(layoutParams);
|
||||
});
|
||||
return animator;
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ package com.topjohnwu.magisk.view;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.topjohnwu.magisk.R;
|
||||
import com.topjohnwu.net.Networking;
|
||||
@@ -50,7 +51,13 @@ public class MarkDownWindow {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
alert.setTitle(title);
|
||||
View mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null);
|
||||
markwon.setMarkdown(mv.findViewById(R.id.md_txt), md);
|
||||
TextView tv = mv.findViewById(R.id.md_txt);
|
||||
try {
|
||||
markwon.setMarkdown(tv, md);
|
||||
} catch (ExceptionInInitializerError e) {
|
||||
//Nothing we can do about this error other than show error message
|
||||
tv.setText(R.string.download_file_error);
|
||||
}
|
||||
alert.setView(mv);
|
||||
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
|
||||
alert.show();
|
||||
|
@@ -89,15 +89,12 @@
|
||||
|
||||
<androidx.appcompat.widget.AppCompatSpinner
|
||||
android:id="@+id/timeout"
|
||||
itemBinding="@{viewModel.itemBinding}"
|
||||
items="@{viewModel.items}"
|
||||
onTouch="@{() -> viewModel.spinnerTouched()}"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:selectedItemPosition="@={viewModel.selectedItemPosition}" />
|
||||
itemDropDownLayout="@{android.R.layout.simple_spinner_dropdown_item}"
|
||||
android:onClick="@{() -> viewModel.spinnerPressed()}"
|
||||
android:adapter="@{viewModel.adapter}"
|
||||
android:selection="@={viewModel.selectedItemPosition}" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/warning"
|
||||
|
@@ -1,25 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:paddingBottom="5dp">
|
||||
android:padding="@dimen/margin_generic">
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:labelFor="@id/custom_url"
|
||||
android:text="@string/settings_update_custom_msg"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
android:hint="@string/settings_update_custom_msg"
|
||||
app:hintEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/custom_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/custom_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
</LinearLayout>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user