Compare commits

...

42 Commits

Author SHA1 Message Date
topjohnwu
12bbc7fd6b Update v7.3.1 changelog 2019-06-17 22:15:38 -07:00
topjohnwu
bf9ac8252b Cleanup UpdateInfo 2019-06-16 16:47:30 -07:00
topjohnwu
4a3f5dc619 Cleanups 2019-06-16 14:35:51 -07:00
Viktor De Pasquale
ca156befbd Fixed mapping generic pairs to policy crashing when no policy is found
The policy (app) is now deleted when found invalid (uninstalled)
2019-06-16 16:50:08 -04:00
Viktor De Pasquale
4db41e2ac4 Added attempted fix for parsing data off default thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
982a43fce1 Moved diff computation of policy list to the background thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
dd76a74e1c Fixed fast scroll button crashing while scrolling to undefined position 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
70cb52b2c7 Fixed fast scroll button being visible when log is empty 2019-06-16 16:50:08 -04:00
topjohnwu
5c7f69acaa Separate SAR and legacy implementation 2019-06-16 12:45:32 -07:00
topjohnwu
f1d9015e5f Move load kernel info out of class 2019-06-15 22:25:09 -07:00
topjohnwu
e8d900c58e Fix typo 2019-06-15 18:12:12 -07:00
topjohnwu
a6241ae912 Fix magiskboot unpack option parsing 2019-06-15 16:15:12 -07:00
topjohnwu
4a697ca2ec Add 7.3.0 changelog 2019-06-14 22:39:31 -07:00
topjohnwu
58bec7f2c9 Update dependencies 2019-06-14 22:39:31 -07:00
Wenlin Shen
213f84985c Update Chinese (Traditional) translate
Update wordings to improve readability.
2019-06-13 21:33:24 -07:00
Viktor De Pasquale
074b1f8c61 Added one-click scroll to the bottom 2019-06-12 16:08:02 +02:00
topjohnwu
326eee8c83 Migrate a lot of classes to Kotlin 2019-06-12 03:29:38 -07:00
topjohnwu
00bff4912e Use svc for reboot if feasible
Close #1488
2019-06-12 00:55:21 -07:00
Viktor De Pasquale
0ce1720516 Fixed magisk log screen lines having multiple lines 2019-06-11 21:52:03 -07:00
osm0sis
ee407472cf magiskboot: allow forcing no recompression on ramdisk.cpio
- when input image had a compressed ramdisk magiskboot had no way to force the repack with the uncompressed ramdisk.cpio since it does not formally recognize cpio as its own format, so add a switch to support forcing repacking to any possible ramdisk format regardless of input image
2019-06-10 21:57:39 -07:00
osm0sis
f341f3b2dd magiskboot: accept forcing recognized but unsupported format compression
- when input image had a different supported format (e.g. gzip) magiskboot would not accept a manually compressed ramdisk or kernel in an unsupported format (e.g. lzop) despite being able to recognize it, so instead would double compress using whatever the input format was, breaking the image with, in effect, a ramdisk.cpio.lzo.gz
2019-06-10 21:56:51 -07:00
Ian Macdonald
8513946e09 'magiskboot hexpatch' will exit with status 1 when nothing patched. 2019-06-10 21:41:40 -07:00
nonnymoose
8ebd9c8927 Use original file type when creating device nodes 2019-06-10 21:41:17 -07:00
topjohnwu
1d54c5144e Fix background update checks 2019-06-10 21:25:42 -07:00
topjohnwu
e40d4318fa Let Kotlin target Java 8 2019-06-10 21:22:07 -07:00
topjohnwu
7756e10779 Rewrite configs with Kotlin delagate properties 2019-06-10 04:37:56 -07:00
topjohnwu
3e58d502d0 Update SettingsFragment to Kotlin 2019-06-09 03:04:37 -07:00
topjohnwu
1c8846dc57 Make PreferenceModel an interface 2019-06-08 16:30:12 -07:00
topjohnwu
2f320c7239 Update ClassMap 2019-06-08 15:34:15 -07:00
topjohnwu
e799918ab6 Update update check service 2019-06-08 15:28:59 -07:00
topjohnwu
86c4928e0f Fix locale settings
AppCompatActivity changed its impl again...
2019-06-08 02:11:10 -07:00
topjohnwu
0293eb5c51 Never refetch magisk version dynamically 2019-06-08 01:44:02 -07:00
topjohnwu
1ee75b6aa6 Download snet package without legacy impl 2019-06-08 01:39:22 -07:00
topjohnwu
4b30b224b5 Remove separate constant class 2019-06-08 00:41:03 -07:00
topjohnwu
16b232d2a3 Enable okhttp logging in debug only 2019-06-07 02:03:17 -07:00
topjohnwu
3f3b1f5b1d Sort policy with app name 2019-06-07 01:24:54 -07:00
topjohnwu
cec017b7bf More MagidkDB fixes 2019-06-07 01:05:54 -07:00
topjohnwu
3123cc1059 Update AndroidX dependencies 2019-06-07 00:27:07 -07:00
topjohnwu
caa9df86bc Switch to R8 friendly room-runtime 2019-06-07 00:17:00 -07:00
topjohnwu
f417389a7a Fix magisk database code in app 2019-06-06 00:39:24 -07:00
topjohnwu
662a5c8ea6 Upgrade Retrofit 2.6.0 2019-06-05 23:41:51 -07:00
topjohnwu
7edfbfb764 Upgrade SDK 2019-06-05 21:33:09 -07:00
125 changed files with 2465 additions and 2851 deletions

View File

@@ -43,6 +43,10 @@ android {
exclude '/kotlin/**'
exclude '/kotlinx/**'
}
kotlinOptions {
jvmTarget = '1.8'
}
}
androidExtensions {
@@ -74,12 +78,12 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = "2.5.0"
def vRetrofit = "2.6.0"
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = "3.12.0"
def vOkHttp = "3.12.3"
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
@@ -90,13 +94,21 @@ dependencies {
implementation "se.ansman.kotshi:api:${vKotshi}"
kapt "se.ansman.kotshi:compiler:${vKotshi}"
modules {
module('androidx.room:room-runtime') {
replacedBy('com.github.topjohnwu:room-runtime')
}
}
def vRoom = "2.1.0"
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
implementation 'androidx.cardview:cardview:1.0.0'
implementation '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'
implementation 'com.google.android.material:material:1.1.0-alpha07'
}

View File

@@ -16,9 +16,6 @@
# 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

View File

@@ -10,6 +10,9 @@ import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import androidx.room.Room
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkDatabase_Impl
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.RootUtils
@@ -107,6 +110,12 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
Shell.Config.addInitializers(RootUtils::class.java)
Shell.Config.setTimeout(2)
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
else -> null
}
}
}
@Deprecated("")

View File

@@ -21,7 +21,7 @@ object ClassMap {
)
@JvmStatic
operator fun get(c: Class<*>): Class<*>? {
return map.getOrElse(c) { null } //as? Class<T>
operator fun <T : Class<*>>get(c: Class<*>): T {
return map.getOrElse(c) { throw IllegalArgumentException() } as T
}
}

View File

@@ -1,399 +0,0 @@
package com.topjohnwu.magisk;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Xml;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import static com.topjohnwu.magisk.ConfigLeanback.getPrefs;
import static com.topjohnwu.magisk.utils.XAndroidKt.getPackageName;
public final class Config {
private static final ArrayMap<String, Object> defs = new ArrayMap<>();
public static int magiskVersionCode = -1;
// Current status
public static String magiskVersionString = "";
// Update Info
public static String remoteMagiskVersionString = "";
public static int remoteMagiskVersionCode = -1;
public static String magiskLink = "";
public static String magiskNoteLink = "";
public static String magiskMD5 = "";
public static String remoteManagerVersionString = "";
public static int remoteManagerVersionCode = -1;
public static String managerLink = "";
public static String managerNoteLink = "";
public static String uninstallerLink = "";
// Install flags
public static boolean keepVerity = false;
public static boolean keepEnc = false;
public static boolean recovery = false;
public static int suLogTimeout = 14;
public static class Key {
// su configs
public static final String ROOT_ACCESS = "root_access";
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
public static final String SU_MNT_NS = "mnt_ns";
public static final String SU_MANAGER = "requester";
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
public static final String SU_AUTO_RESPONSE = "su_auto_response";
public static final String SU_NOTIFICATION = "su_notification";
public static final String SU_REAUTH = "su_reauth";
public static final String SU_FINGERPRINT = "su_fingerprint";
// prefs
public static final String CHECK_UPDATES = "check_update";
public static final String UPDATE_CHANNEL = "update_channel";
public static final String CUSTOM_CHANNEL = "custom_channel";
public static final String LOCALE = "locale";
public static final String DARK_THEME = "dark_theme";
public static final String ETAG_KEY = "ETag";
public static final String REPO_ORDER = "repo_order";
public static final String SHOW_SYSTEM_APP = "show_system";
// system state
public static final String UPDATE_SERVICE_VER = "update_service_version";
public static final String MAGISKHIDE = "magiskhide";
public static final String COREONLY = "disable";
}
public static class Value {
public static final int DEFAULT_CHANNEL = -1;
public static final int STABLE_CHANNEL = 0;
public static final int BETA_CHANNEL = 1;
public static final int CUSTOM_CHANNEL = 2;
public static final int CANARY_CHANNEL = 3;
public static final int CANARY_DEBUG_CHANNEL = 4;
public static final int ROOT_ACCESS_DISABLED = 0;
public static final int ROOT_ACCESS_APPS_ONLY = 1;
public static final int ROOT_ACCESS_ADB_ONLY = 2;
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
public static final int MULTIUSER_MODE_USER = 2;
public static final int NAMESPACE_MODE_GLOBAL = 0;
public static final int NAMESPACE_MODE_REQUESTER = 1;
public static final int NAMESPACE_MODE_ISOLATE = 2;
public static final int NO_NOTIFICATION = 0;
public static final int NOTIFICATION_TOAST = 1;
public static final int SU_PROMPT = 0;
public static final int SU_AUTO_DENY = 1;
public static final int SU_AUTO_ALLOW = 2;
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
public static final int ORDER_NAME = 0;
public static final int ORDER_DATE = 1;
}
private static boolean magiskHide = false;
public static void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
} catch (NumberFormatException ignored) {
}
}
public static void export() {
// Flush prefs to disk
getPrefs().edit().apply();
Context context = ConfigLeanback.getProtectedContext();
File xml = new File(context.getFilesDir().getParent() + "/shared_prefs",
getPackageName() + "_preferences.xml");
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
}
private static final int PREF_INT = 0;
private static final int PREF_STR_INT = 1;
private static final int PREF_BOOL = 2;
private static final int PREF_STR = 3;
private static final int DB_INT = 4;
private static final int DB_BOOL = 5;
private static final int DB_STR = 6;
private static int getConfigType(String key) {
switch (key) {
case Key.REPO_ORDER:
return PREF_INT;
case Key.SU_REQUEST_TIMEOUT:
case Key.SU_AUTO_RESPONSE:
case Key.SU_NOTIFICATION:
case Key.UPDATE_CHANNEL:
return PREF_STR_INT;
case Key.DARK_THEME:
case Key.SU_REAUTH:
case Key.CHECK_UPDATES:
case Key.MAGISKHIDE:
case Key.COREONLY:
case Key.SHOW_SYSTEM_APP:
return PREF_BOOL;
case Key.CUSTOM_CHANNEL:
case Key.LOCALE:
case Key.ETAG_KEY:
return PREF_STR;
case Key.ROOT_ACCESS:
case Key.SU_MNT_NS:
case Key.SU_MULTIUSER_MODE:
return DB_INT;
case Key.SU_FINGERPRINT:
return DB_BOOL;
case Key.SU_MANAGER:
return DB_STR;
default:
throw new IllegalArgumentException();
}
}
public static void initialize() {
SharedPreferences pref = getPrefs();
SharedPreferences.Editor editor = pref.edit();
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
if (config.exists()) {
try {
SuFileInputStream is = new SuFileInputStream(config);
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(is, "UTF-8");
parser.nextTag();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG)
continue;
String key = parser.getAttributeValue(null, "name");
String value = parser.getAttributeValue(null, "value");
switch (parser.getName()) {
case "string":
parser.require(XmlPullParser.START_TAG, null, "string");
editor.putString(key, parser.nextText());
parser.require(XmlPullParser.END_TAG, null, "string");
break;
case "boolean":
parser.require(XmlPullParser.START_TAG, null, "boolean");
editor.putBoolean(key, Boolean.parseBoolean(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "boolean");
break;
case "int":
parser.require(XmlPullParser.START_TAG, null, "int");
editor.putInt(key, Integer.parseInt(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "int");
break;
case "long":
parser.require(XmlPullParser.START_TAG, null, "long");
editor.putLong(key, Long.parseLong(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "long");
break;
case "float":
parser.require(XmlPullParser.START_TAG, null, "int");
editor.putFloat(key, Float.parseFloat(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "int");
break;
default:
parser.next();
}
}
} catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
editor.remove(Key.ETAG_KEY);
editor.apply();
editor = pref.edit();
config.delete();
}
// Set defaults if not set
setDefs(pref, editor);
// These settings are from actual device state
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
.apply();
}
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
switch (getConfigType(key)) {
case PREF_INT:
return (T) (Integer) getPrefs().getInt(key, getDef(key));
case PREF_STR_INT:
return (T) (Integer) Utils.getPrefsInt(getPrefs(), key, getDef(key));
case PREF_BOOL:
return (T) (Boolean) getPrefs().getBoolean(key, getDef(key));
case PREF_STR:
return (T) getPrefs().getString(key, getDef(key));
case DB_INT:
return (T) (Integer) ConfigLeanback.get(key, (Integer) getDef(key));
case DB_BOOL:
return (T) (Boolean) (ConfigLeanback.get(key, getDef(key) ? 1 : 0) != 0);
case DB_STR:
return (T) ConfigLeanback.get(key, getDef(key));
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return null;
}
public static void set(String key, Object val) {
switch (getConfigType(key)) {
case PREF_INT:
getPrefs().edit().putInt(key, (int) val).apply();
break;
case PREF_STR_INT:
getPrefs().edit().putString(key, String.valueOf(val)).apply();
break;
case PREF_BOOL:
getPrefs().edit().putBoolean(key, (boolean) val).apply();
break;
case PREF_STR:
getPrefs().edit().putString(key, (String) val).apply();
break;
case DB_INT:
ConfigLeanback.put(key, (int) val);
break;
case DB_BOOL:
ConfigLeanback.put(key, (boolean) val ? 1 : 0);
break;
case DB_STR:
ConfigLeanback.put(key, (String) val);
break;
}
}
public static void remove(String key) {
switch (getConfigType(key)) {
case PREF_INT:
case PREF_STR_INT:
case PREF_BOOL:
case PREF_STR:
getPrefs().edit().remove(key).apply();
break;
case DB_BOOL:
case DB_INT:
ConfigLeanback.delete(key);
break;
case DB_STR:
ConfigLeanback.put(key, null);
break;
}
}
static {
/* Set default configurations */
// prefs int
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
// prefs string int
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
// prefs bool
defs.put(Key.CHECK_UPDATES, true);
defs.put(Key.DARK_THEME, true);
//defs.put(Key.SU_REAUTH, false);
//defs.put(Key.SHOW_SYSTEM_APP, false);
// prefs string
defs.put(Key.CUSTOM_CHANNEL, "");
defs.put(Key.LOCALE, "");
//defs.put(Key.ETAG_KEY, null);
// db int
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
// db bool
//defs.put(Key.SU_FINGERPRINT, false);
// db strings
//defs.put(Key.SU_MANAGER, null);
}
@SuppressWarnings("unchecked")
@Nullable
private static <T> T getDef(String key) throws IllegalArgumentException {
Object val = defs.get(key);
switch (getConfigType(key)) {
case PREF_INT:
case DB_INT:
case PREF_STR_INT:
return val != null ? (T) val : (T) (Integer) 0;
case DB_BOOL:
case PREF_BOOL:
return val != null ? (T) val : (T) (Boolean) false;
case DB_STR:
case PREF_STR:
return (T) val;
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return null;
}
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
for (String key : defs.keySet()) {
Object value = defs.get(key);
int type = getConfigType(key);
switch (type) {
case DB_INT:
editor.putString(key, String.valueOf(ConfigLeanback.get(key, (Integer) value)));
continue;
case DB_STR:
editor.putString(key, ConfigLeanback.get(key, String.valueOf(value)));
continue;
case DB_BOOL:
int bs = ConfigLeanback.get(key, -1);
editor.putBoolean(key, bs < 0 ? (Boolean) value : bs != 0);
continue;
}
if (pref.contains(key))
continue;
switch (type) {
case PREF_INT:
editor.putInt(key, (Integer) value);
break;
case PREF_STR_INT:
editor.putString(key, String.valueOf(value));
break;
case PREF_STR:
editor.putString(key, (String) value);
break;
case PREF_BOOL:
editor.putBoolean(key, (Boolean) value);
break;
}
}
}
}

View File

@@ -0,0 +1,196 @@
package com.topjohnwu.magisk
import android.content.Context
import android.util.Xml
import androidx.core.content.edit
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import org.xmlpull.v1.XmlPullParser
import java.io.File
object Config : PreferenceModel, DBConfig {
override val stringDao: StringDao by inject()
override val settingsDao: SettingsDao by inject()
override val context: Context by inject(Protected)
object Key {
// db configs
const val ROOT_ACCESS = "root_access"
const val SU_MULTIUSER_MODE = "multiuser_mode"
const val SU_MNT_NS = "mnt_ns"
const val SU_MANAGER = "requester"
const val SU_FINGERPRINT = "su_fingerprint"
// prefs
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
const val SU_AUTO_RESPONSE = "su_auto_response"
const val SU_NOTIFICATION = "su_notification"
const val SU_REAUTH = "su_reauth"
const val CHECK_UPDATES = "check_update"
const val UPDATE_CHANNEL = "update_channel"
const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale"
const val DARK_THEME = "dark_theme"
const val ETAG_KEY = "ETag"
const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system"
// system state
const val MAGISKHIDE = "magiskhide"
const val COREONLY = "disable"
}
object Value {
// Update channels
const val DEFAULT_CHANNEL = -1
const val STABLE_CHANNEL = 0
const val BETA_CHANNEL = 1
const val CUSTOM_CHANNEL = 2
const val CANARY_CHANNEL = 3
const val CANARY_DEBUG_CHANNEL = 4
// root access mode
const val ROOT_ACCESS_DISABLED = 0
const val ROOT_ACCESS_APPS_ONLY = 1
const val ROOT_ACCESS_ADB_ONLY = 2
const val ROOT_ACCESS_APPS_AND_ADB = 3
// su multiuser
const val MULTIUSER_MODE_OWNER_ONLY = 0
const val MULTIUSER_MODE_OWNER_MANAGED = 1
const val MULTIUSER_MODE_USER = 2
// su mnt ns
const val NAMESPACE_MODE_GLOBAL = 0
const val NAMESPACE_MODE_REQUESTER = 1
const val NAMESPACE_MODE_ISOLATE = 2
// su notification
const val NO_NOTIFICATION = 0
const val NOTIFICATION_TOAST = 1
// su auto response
const val SU_PROMPT = 0
const val SU_AUTO_DENY = 1
const val SU_AUTO_ALLOW = 2
// su timeout
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
// repo order
const val ORDER_NAME = 0
const val ORDER_DATE = 1
}
private val defaultChannel =
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
else Value.DEFAULT_CHANNEL
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
var darkTheme by preference(Key.DARK_THEME, true)
var suReAuth by preference(Key.SU_REAUTH, false)
var checkUpdate by preference(Key.CHECK_UPDATES, true)
@JvmStatic
var magiskHide by preference(Key.MAGISKHIDE, true)
var coreOnly by preference(Key.COREONLY, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var locale by preference(Key.LOCALE, "")
@JvmStatic
var etagKey by preference(Key.ETAG_KEY, "")
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
@JvmStatic
var suManager by dbStrings(Key.SU_MANAGER, "")
fun initialize() = prefs.edit {
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
if (config.exists()) runCatching {
val input = SuFileInputStream(config).buffered()
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, "UTF-8")
parser.nextTag()
parser.require(XmlPullParser.START_TAG, null, "map")
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.eventType != XmlPullParser.START_TAG)
continue
val key: String = parser.getAttributeValue(null, "name")
val value: String = parser.getAttributeValue(null, "value")
when (parser.name) {
"string" -> {
parser.require(XmlPullParser.START_TAG, null, "string")
putString(key, parser.nextText())
parser.require(XmlPullParser.END_TAG, null, "string")
}
"boolean" -> {
parser.require(XmlPullParser.START_TAG, null, "boolean")
putBoolean(key, value.toBoolean())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "boolean")
}
"int" -> {
parser.require(XmlPullParser.START_TAG, null, "int")
putInt(key, value.toInt())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "int")
}
"long" -> {
parser.require(XmlPullParser.START_TAG, null, "long")
putLong(key, value.toLong())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "long")
}
"float" -> {
parser.require(XmlPullParser.START_TAG, null, "int")
putFloat(key, value.toFloat())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "int")
}
else -> parser.next()
}
}
config.delete()
}
remove(Key.ETAG_KEY)
if (!prefs.contains(Key.UPDATE_CHANNEL))
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
// Get actual state
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
// Write database configs
putString(Key.ROOT_ACCESS, rootMode.toString())
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
}
@JvmStatic
fun export() {
// Flush prefs to disk
prefs.edit().apply()
val xml = File("${get<Context>(Protected).filesDir.parent}/shared_prefs",
"${packageName}_preferences.xml")
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
}
}

View File

@@ -1,52 +0,0 @@
package com.topjohnwu.magisk
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.data.repository.SettingRepository
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.utils.inject
object ConfigLeanback {
@JvmStatic
val protectedContext: Context by inject(Protected)
@JvmStatic
val prefs: SharedPreferences by inject()
private val settingRepo: SettingRepository by inject()
private val stringRepo: StringRepository by inject()
@JvmStatic
@AnyThread
fun put(key: String, value: Int) {
settingRepo.put(key, value).subscribeK()
}
@JvmStatic
@WorkerThread
fun get(key: String, defaultValue: Int): Int =
settingRepo.fetch(key, defaultValue).blockingGet()
@JvmStatic
@AnyThread
fun put(key: String, value: String?) {
val task = value?.let { stringRepo.put(key, it) } ?: stringRepo.delete(key)
task.subscribeK()
}
@JvmStatic
@WorkerThread
fun get(key: String, defaultValue: String?): String =
stringRepo.fetch(key, defaultValue.orEmpty()).blockingGet()
@JvmStatic
@AnyThread
fun delete(key: String) {
settingRepo.delete(key).subscribeK()
}
}

View File

@@ -9,33 +9,27 @@ 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)
val EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)!!
@JvmField
var MAGISK_DISABLE_FILE: File = File("xxx")
var MAGISK_DISABLE_FILE = File("xxx")
const val TMP_FOLDER_PATH = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
// Versions
const val UPDATE_SERVICE_VER = 1
const val SNET_EXT_VER = 12
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
// Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
@JvmField
val USER_ID = Process.myUid() / 100000
// Generic
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
init {
EXTERNAL_PATH.mkdirs()
}
@@ -72,12 +66,11 @@ object Const {
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")
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
private fun getRaw(where: String, name: String) =
"https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s".format(where, name)
"${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name"
}
object Key {

View File

@@ -1,20 +0,0 @@
package com.topjohnwu.magisk
import android.os.Process
object Constants {
// Paths
val MAGISK_PATH = "/sbin/.magisk/img"
val MAGISK_LOG = "/cache/magisk.log"
val USER_ID get() = Process.myUid() / 100000
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
const val GITHUB_URL = "https://github.com/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.model.entity.UpdateInfo;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
public final class Info {
public static int magiskVersionCode = -1;
@NonNull
public static String magiskVersionString = "";
public static UpdateInfo remote = new UpdateInfo();
public static boolean keepVerity = false;
public static boolean keepEnc = false;
public static boolean recovery = false;
public static void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
Config.setMagiskHide(Shell.su("magiskhide --status").exec().isSuccess());
} catch (NumberFormatException ignored) {
}
}
}

View File

@@ -1,47 +0,0 @@
package com.topjohnwu.magisk
import android.content.Context
import com.topjohnwu.magisk.KConfig.UpdateChannel.STABLE
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.inject
object KConfig : PreferenceModel() {
override val context: Context by inject(Protected)
override val fileName: String = "${context.packageName}_preferences"
private var internalUpdateChannel by preference(Config.Key.UPDATE_CHANNEL, STABLE.id.toString())
var useCustomTabs by preference("useCustomTabs", true)
@JvmStatic
var customUpdateChannel by preference(Config.Key.CUSTOM_CHANNEL, "")
@JvmStatic
var updateChannel: UpdateChannel
get() = UpdateChannel.byId(internalUpdateChannel.toIntOrNull() ?: STABLE.id)
set(value) {
internalUpdateChannel = value.id.toString()
}
internal const val DEFAULT_CHANNEL = "topjohnwu/magisk_files"
enum class UpdateChannel(val id: Int) {
STABLE(Config.Value.STABLE_CHANNEL),
BETA(Config.Value.BETA_CHANNEL),
CANARY(Config.Value.CANARY_CHANNEL),
CANARY_DEBUG(Config.Value.CANARY_DEBUG_CHANNEL),
CUSTOM(Config.Value.CUSTOM_CHANNEL);
companion object {
fun byId(id: Int) = when (id) {
Config.Value.STABLE_CHANNEL -> STABLE
Config.Value.BETA_CHANNEL -> BETA
Config.Value.CUSTOM_CHANNEL -> CUSTOM
Config.Value.CANARY_CHANNEL -> CANARY
Config.Value.CANARY_DEBUG_CHANNEL -> CANARY_DEBUG
else -> STABLE
}
}
}
}

View File

@@ -1,6 +1,5 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.toLog
@@ -12,7 +11,7 @@ class LogDao : BaseDao() {
override val table = DatabaseDefinition.Table.LOG
fun deleteOutdated(
suTimeout: Long = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1)
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
) = query<Delete> {
condition {
lessThan("time", suTimeout.toString())

View File

@@ -2,12 +2,13 @@ package com.topjohnwu.magisk.data.database
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.utils.now
import timber.log.Timber
import java.util.concurrent.TimeUnit
@@ -47,12 +48,7 @@ class PolicyDao(
condition {
equals("uid", uid)
}
}.map { it.firstOrNull()?.toPolicy(context.packageManager) }
.doOnError {
if (it is PackageManager.NameNotFoundException) {
delete(uid).subscribe()
}
}
}.map { it.first().toPolicySafe() }
fun update(policy: MagiskPolicy) = query<Replace> {
values(policy.toMap())
@@ -60,10 +56,26 @@ class PolicyDao(
fun fetchAll() = query<Select> {
condition {
equals("uid/100000", Constants.USER_ID)
equals("uid/100000", Const.USER_ID)
}
}.flattenAsFlowable { it }
.map { it.toPolicy(context.packageManager) }
.toList()
}.map { it.mapNotNull { it.toPolicySafe() } }
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
val taskResult = runCatching { toPolicy(context.packageManager) }
val result = taskResult.getOrNull()
val exception = taskResult.exceptionOrNull()
Timber.e(exception)
when (exception) {
is PackageManager.NameNotFoundException -> {
val uid = getOrElse("uid") { null } ?: return null
delete(uid).subscribe()
}
}
return result
}
}

View File

@@ -1,118 +0,0 @@
package com.topjohnwu.magisk.data.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.model.entity.Repo;
import java.util.HashSet;
import java.util.Set;
@Deprecated
public class RepoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 5;
private static final String TABLE_NAME = "repos";
private final SQLiteDatabase mDb;
@Deprecated
public RepoDatabaseHelper(Context context) {
super(context, "repo.db", null, DATABASE_VER);
mDb = getWritableDatabase();
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, DATABASE_VER);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != newVersion) {
// Nuke old DB and create new table
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
Config.remove(Config.Key.ETAG_KEY);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, 0, DATABASE_VER);
}
@Deprecated
public void clearRepo() {
mDb.delete(TABLE_NAME, null, null);
}
@Deprecated
public void removeRepo(String id) {
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
}
@Deprecated
public void removeRepo(Repo repo) {
removeRepo(repo.getId());
}
@Deprecated
public void removeRepo(Iterable<String> list) {
for (String id : list) {
if (id == null) continue;
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
}
}
@Deprecated
public void addRepo(Repo repo) {
mDb.replace(TABLE_NAME, null, repo.getContentValues());
}
@Deprecated
public Repo getRepo(String id) {
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[]{id}, null, null, null)) {
if (c.moveToNext()) {
return new Repo(c);
}
}
return null;
}
@Deprecated
public Cursor getRawCursor() {
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
}
@Deprecated
public Cursor getRepoCursor() {
String orderBy = null;
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
case Config.Value.ORDER_NAME:
orderBy = "name COLLATE NOCASE";
break;
case Config.Value.ORDER_DATE:
orderBy = "last_update DESC";
}
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
}
@Deprecated
public Set<String> getRepoIDSet() {
HashSet<String> set = new HashSet<>(300);
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")));
}
}
return set;
}
}

View File

@@ -0,0 +1,109 @@
package com.topjohnwu.magisk.data.database
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.content.edit
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.Repo
import java.util.*
@Deprecated("")
class RepoDatabaseHelper
constructor(context: Context) : SQLiteOpenHelper(context, "repo.db", null, DATABASE_VER) {
private val mDb: SQLiteDatabase = writableDatabase
val rawCursor: Cursor
@Deprecated("")
get() = mDb.query(TABLE_NAME, null, null, null, null, null, null)
val repoCursor: Cursor
@Deprecated("")
get() {
var orderBy: String? = null
when (Config.repoOrder) {
Config.Value.ORDER_NAME -> orderBy = "name COLLATE NOCASE"
Config.Value.ORDER_DATE -> orderBy = "last_update DESC"
}
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy)
}
val repoIDSet: Set<String>
@Deprecated("")
get() {
val set = HashSet<String>(300)
mDb.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")))
}
}
return set
}
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DATABASE_VER)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion != newVersion) {
// Nuke old DB and create new table
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))")
Config.prefs.edit {
remove(Config.Key.ETAG_KEY)
}
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, 0, DATABASE_VER)
}
@Deprecated("")
fun clearRepo() {
mDb.delete(TABLE_NAME, null, null)
}
@Deprecated("")
fun removeRepo(id: String) {
mDb.delete(TABLE_NAME, "id=?", arrayOf(id))
}
@Deprecated("")
fun removeRepo(repo: Repo) {
removeRepo(repo.id)
}
@Deprecated("")
fun removeRepo(list: Iterable<String>) {
list.forEach {
mDb.delete(TABLE_NAME, "id=?", arrayOf(it))
}
}
@Deprecated("")
fun addRepo(repo: Repo) {
mDb.replace(TABLE_NAME, null, repo.contentValues)
}
@Deprecated("")
fun getRepo(id: String): Repo? {
mDb.query(TABLE_NAME, null, "id=?", arrayOf(id), null, null, null).use { c ->
if (c.moveToNext()) {
return Repo(c)
}
}
return null
}
companion object {
private val DATABASE_VER = 5
private val TABLE_NAME = "repos"
}
}

View File

@@ -10,11 +10,12 @@ class SettingsDao : BaseDao() {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: Int) = query<Insert> {
values(key to value.toString())
fun put(key: String, value: Int) = query<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
fun fetch(key: String, default: Int = -1) = query<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }

View File

@@ -10,8 +10,8 @@ class StringDao : BaseDao() {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: String) = query<Insert> {
values(key to value)
fun put(key: String, value: String) = query<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
fun fetch(key: String, default: String = "") = query<Select> {

View File

@@ -32,11 +32,7 @@ class Delete : MagiskQueryBuilder {
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln(condition)
.toString()
return listOf(requestType, table, condition).joinToString(" ")
}
}
@@ -65,13 +61,7 @@ class Select : MagiskQueryBuilder {
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln(condition)
.appendln(orderField)
.toString()
.replace("\n", " ")
return listOf(requestType, table, condition, orderField).joinToString(" ")
}
}
@@ -84,23 +74,25 @@ open class Insert : MagiskQueryBuilder {
override lateinit var table: String
private val keys get() = _values.keys.joinToString(",")
private val values get() = _values.values.joinToString(",") { "\"$it\"" }
private var _values: Map<String, String> = mapOf()
private val values get() = _values.values.joinToString(",") {
when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
else -> "\"$it\""
}
}
private var _values: Map<String, Any> = mapOf()
fun values(vararg pairs: Pair<String, String>) {
fun values(vararg pairs: Pair<String, Any>) {
_values = pairs.toMap()
}
fun values(values: Map<String, String>) {
fun values(values: Map<String, Any>) {
_values = values
}
override fun toString(): String {
return StringBuilder()
.appendln(requestType)
.appendln(table)
.appendln("($keys) VALUES($values)")
.toString()
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
}
}

View File

@@ -1,8 +1,7 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.KConfig
import com.topjohnwu.magisk.model.entity.MagiskConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.model.entity.UpdateInfo
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
@@ -16,27 +15,27 @@ interface GithubRawApiServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchConfig(): Single<MagiskConfig>
fun fetchStableUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaConfig(): Single<MagiskConfig>
fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
fun fetchCanaryConfig(): Single<MagiskConfig>
fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET
fun fetchCustomConfig(@Url url: String): Single<MagiskConfig>
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming
fun fetchBootctl(@Path(REVISION) revision: String = Constants.BOOTCTL_REVISION): Single<ResponseBody>
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
//endregion
@@ -55,7 +54,7 @@ interface GithubRawApiServices {
private const val FILE = "file"
private const val MAGISK_FILES = KConfig.DEFAULT_CHANNEL
private const val MAGISK_FILES = "topjohnwu/magisk_files"
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
}

View File

@@ -0,0 +1,95 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.utils.trimEmptyToNull
import io.reactivex.schedulers.Schedulers
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface DBConfig {
val settingsDao: SettingsDao
val stringDao: StringDao
fun dbSettings(
name: String,
default: Int
) = DBSettingsValue(name, default)
fun dbSettings(
name: String,
default: Boolean
) = DBBoolSettings(name, default)
fun dbStrings(
name: String,
default: String
) = DBStringsValue(name, default)
}
class DBSettingsValue(
private val name: String,
private val default: Int
) : ReadWriteProperty<DBConfig, Int> {
private var value: Int? = null
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null)
value = thisRef.settingsDao.fetch(getKey(property), default).blockingGet()
return value!!
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
synchronized(this) {
this.value = value
}
thisRef.settingsDao.put(getKey(property), value)
.subscribeOn(Schedulers.io())
.subscribe()
}
}
class DBBoolSettings(
name: String,
default: Boolean
) : ReadWriteProperty<DBConfig, Boolean> {
val base = DBSettingsValue(name, if (default) 1 else 0)
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
= base.getValue(thisRef, property) != 0
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
base.setValue(thisRef, property, if (value) 1 else 0)
}
class DBStringsValue(
private val name: String,
private val default: String
) : ReadWriteProperty<DBConfig, String> {
private var value: String? = null
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null)
value = thisRef.stringDao.fetch(getKey(property), default).blockingGet()
return value!!
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
synchronized(this) {
this.value = value
}
thisRef.stringDao.put(getKey(property), value)
.subscribeOn(Schedulers.io())
.subscribe()
}
}

View File

@@ -1,14 +1,12 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.LogDao
import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.superuser.Shell
import timber.log.Timber
import java.util.concurrent.TimeUnit
@@ -20,9 +18,8 @@ class LogRepository(
.map { it.sortByDescending { it.date.time }; it }
.map { it.wrap() }
fun fetchMagiskLogs() = "tail -n 5000 ${Constants.MAGISK_LOG}".suRaw()
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() }
.map { Timber.i(it.toString()); it }
fun clearLogs() = logDao.deleteAll()
fun clearOutdated() = logDao.deleteOutdated()

View File

@@ -4,20 +4,17 @@ import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.KConfig
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.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,
@@ -25,55 +22,42 @@ class MagiskRepository(
private val packageManager: PackageManager
) {
fun fetchMagisk() = fetchConfig()
fun fetchMagisk() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.magisk.link) }
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
fun fetchManager() = fetchConfig()
fun fetchManager() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.app.link) }
.map { it.writeToFile(context, FILE_MAGISK_APK) }
fun fetchUninstaller() = fetchConfig()
fun fetchUninstaller() = fetchUpdate()
.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 fetchSafetynet() = apiRaw.fetchSafetynet()
fun fetchBootctl() = apiRaw
.fetchBootctl()
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
fun fetchConfig() = when (KConfig.updateChannel) {
KConfig.UpdateChannel.STABLE -> apiRaw.fetchConfig()
KConfig.UpdateChannel.BETA -> apiRaw.fetchBetaConfig()
KConfig.UpdateChannel.CANARY -> apiRaw.fetchCanaryConfig()
KConfig.UpdateChannel.CANARY_DEBUG -> apiRaw.fetchCanaryDebugConfig()
KConfig.UpdateChannel.CUSTOM -> apiRaw.fetchCustomConfig(KConfig.customUpdateChannel)
}
.doOnSuccess {
Config.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
Config.magiskLink = it.magisk.link
Config.magiskNoteLink = it.magisk.note
Config.magiskMD5 = it.magisk.hash
Config.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
Config.remoteManagerVersionString = it.app.version
Config.managerLink = it.app.link
Config.managerNoteLink = it.app.note
Config.uninstallerLink = it.uninstaller.link
}
fun fetchMagiskVersion(): Single<Version> = Single.zip(
fetchMagiskVersionName(),
fetchMagiskVersionCode(),
BiFunction { versionName, versionCode ->
Version(versionName, versionCode)
fun fetchUpdate() = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
if (it.magisk.versionCode < Info.magiskVersionCode
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
apiRaw.fetchBetaUpdate()
} else {
Single.just(it)
}
)
}.map { Info.remote = it; it }
fun fetchApps() =
Single.fromCallable { packageManager.getInstalledApplications(0) }
@@ -93,16 +77,6 @@ class MagiskRepository(
.map { HideTarget(it) }
.toList()
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
.map { it.first() }
.map { it.substring(0 until it.indexOf(":")) }
.onErrorReturn { "Unknown" }
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
.map { it.first() }
.map { it.toIntOrNull() ?: -1 }
.onErrorReturn { -1 }
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
class SettingRepository(private val settingsDao: SettingsDao) {
fun fetch(key: String, default: Int) = settingsDao.fetch(key, default)
fun put(key: String, value: Int) = settingsDao.put(key, value)
fun delete(key: String) = settingsDao.delete(key)
}

View File

@@ -1,11 +0,0 @@
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)
}

View File

@@ -2,7 +2,8 @@ package com.topjohnwu.magisk.di
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -19,17 +20,20 @@ val networkingModule = module {
single { createConverterFactory() }
single { createCallAdapterFactory() }
single { createRetrofit(get(), get(), get()) }
single { createApiService<GithubRawApiServices>(get(), Constants.GITHUB_RAW_API_URL) }
single { createApiService<GithubRawApiServices>(get(), Const.Url.GITHUB_RAW_API_URL) }
}
fun createOkHttpClient(): OkHttpClient {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
builder.addInterceptor(httpLoggingInterceptor)
}
return OkHttpClient.Builder()
.addInterceptor(httpLoggingInterceptor)
.build()
return builder.build()
}
fun createConverterFactory(): Converter.Factory {

View File

@@ -1,6 +1,8 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.data.repository.*
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.data.repository.MagiskRepository
import org.koin.dsl.module
@@ -8,6 +10,4 @@ val repositoryModule = module {
single { MagiskRepository(get(), get(), get()) }
single { LogRepository(get()) }
single { AppRepository(get()) }
single { SettingRepository(get()) }
single { StringRepository(get()) }
}

View File

@@ -0,0 +1,37 @@
package com.topjohnwu.magisk.model.binding
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
class BindingAdapter : BindingRecyclerViewAdapter<ComparableRvItem<*>>() {
private var recyclerView: RecyclerView? = null
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: ComparableRvItem<*>
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
when (item) {
is LenientRvItem -> {
val recycler = recyclerView ?: return
item.onBindingBound(binding)
item.onBindingBound(binding, recycler)
}
else -> item.onBindingBound(binding)
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
}

View File

@@ -6,6 +6,8 @@ import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import androidx.annotation.Nullable;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
@@ -29,8 +31,6 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import androidx.annotation.Nullable;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;

View File

@@ -5,10 +5,10 @@ import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
import androidx.annotation.NonNull;
import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskApp(
val version: String,
val versionCode: String,
val link: String,
val note: String
)

View File

@@ -1,10 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskConfig(
val app: MagiskApp,
val uninstaller: MagiskLink,
val magisk: MagiskFlashable
)

View File

@@ -1,13 +0,0 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskFlashable(
val version: String,
val versionCode: String,
val link: String,
val note: String,
@Json(name = "md5") val hash: String
)

View File

@@ -1,8 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskLink(
val link: String
)

View File

@@ -38,7 +38,6 @@ fun Map<String, String>.toLog(): MagiskLog {
fun Long.toDate() = Date(this)
fun MagiskLog.toMap() = mapOf(
"from_uid" to fromUid,
"to_uid" to toUid,
@@ -47,8 +46,8 @@ fun MagiskLog.toMap() = mapOf(
"app_name" to appName,
"command" to command,
"action" to action,
"time" to date
).mapValues { it.toString() }
"time" to date.time
)
fun MagiskPolicy.toLog(
toUid: Int,

View File

@@ -6,7 +6,7 @@ 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.Const
import com.topjohnwu.magisk.data.database.base.su
import io.reactivex.Single
import kotlinx.android.parcel.Parcelize
@@ -48,7 +48,7 @@ data class Module(
@AnyThread
fun File.toModule(): Single<Module> {
val path = "${Constants.MAGISK_PATH}/$name"
val path = "${Const.MAGISK_PATH}/$name"
return "dos2unix < $path/module.prop".su()
.map { it.first().toModule(path) }
}

View File

@@ -24,42 +24,14 @@ data class MagiskPolicy(
}
/*@Throws(PackageManager.NameNotFoundException::class)
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = getAsInteger("uid")
val packageName = getAsString("package_name")
val info = pm.getApplicationInfo(packageName, 0)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return MagiskPolicy(
uid = uid,
packageName = packageName,
policy = getAsInteger("policy"),
until = getAsInteger("until").toLong(),
logging = getAsInteger("logging") != 0,
notification = getAsInteger("notification") != 0,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}
fun MagiskPolicy.toContentValues() = ContentValues().apply {
put("uid", uid)
put("uid", uid)
put("package_name", packageName)
put("policy", policy)
put("until", until)
put("logging", if (logging) 1 else 0)
put("notification", if (notification) 1 else 0)
}*/
fun MagiskPolicy.toMap() = mapOf(
"uid" to uid,
"package_name" to packageName,
"policy" to policy,
"until" to until,
"logging" to if (logging) 1 else 0,
"notification" to if (notification) 1 else 0
).mapValues { it.value.toString() }
"logging" to logging,
"notification" to notification
)
@Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {

View File

@@ -4,10 +4,10 @@ import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.utils.Utils;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
@@ -27,7 +27,7 @@ public class Policy implements Comparable<Policy>{
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = Utils.getAppLabel(info, pm);
appName = Utils.INSTANCE.getAppLabel(info, pm);
}
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {

View File

@@ -54,7 +54,7 @@ public class Repo extends BaseModule {
}
public void update() throws IllegalRepoException {
String[] props = Utils.dlString(getPropUrl()).split("\\n");
String[] props = Utils.INSTANCE.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
@@ -103,7 +103,7 @@ public class Repo extends BaseModule {
}
public String getDownloadFilename() {
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
return Utils.INSTANCE.getLegalFilename(getName() + "-" + getVersion() + ".zip");
}
public class IllegalRepoException extends Exception {

View File

@@ -0,0 +1,33 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class UpdateInfo(
val app: ManagerJson = ManagerJson(),
val uninstaller: UninstallerJson = UninstallerJson(),
val magisk: MagiskJson = MagiskJson()
)
@JsonSerializable
data class UninstallerJson(
val link: String = ""
)
@JsonSerializable
data class MagiskJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = "",
@Json(name = "md5") val hash: String = ""
)
@JsonSerializable
data class ManagerJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = ""
)

View File

@@ -1,11 +1,26 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() {
class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
override val layoutRes: Int = R.layout.item_console
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
val view = binding.root as TextView
view.measure(0, 0)
val desiredWidth = view.measuredWidth
view.updateLayoutParams { width = desiredWidth }
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
override fun itemSameAs(other: ConsoleRvItem) = item == other.item
}

View File

@@ -0,0 +1,16 @@
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.databinding.ComparableRvItem
/**
* This item addresses issues where enclosing recycler has to be invalidated or generally
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
* */
abstract class LenientRvItem<in T> : ComparableRvItem<T>() {
open fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {}
}

View File

@@ -16,7 +16,7 @@ class BooleanProperty(
property: KProperty<*>
): Boolean {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -16,7 +16,7 @@ class FloatProperty(
property: KProperty<*>
): Float {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -16,7 +16,7 @@ class IntProperty(
property: KProperty<*>
): Int {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -16,7 +16,7 @@ class LongProperty(
property: KProperty<*>
): Long {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -1,51 +1,69 @@
package com.topjohnwu.magisk.model.preference
import android.content.Context
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
abstract class PreferenceModel(
private val commitPrefs: Boolean = false
) {
interface PreferenceModel {
protected abstract val fileName: String
protected abstract val context: Context
val context: Context
internal val prefs get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
val fileName: String
get() = "${context.packageName}_preferences"
val commitPrefs: Boolean
get() = false
val prefs: SharedPreferences
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
protected fun preference(
fun preferenceStrInt(
name: String,
default: Int,
writeDefault: Boolean = false,
commit: Boolean = commitPrefs
) = object: ReadWriteProperty<PreferenceModel, Int> {
val base = StringProperty(name, default.toString(), commit)
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
base.getValue(thisRef, property).toInt()
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
base.setValue(thisRef, property, value.toString())
}
fun preference(
name: String,
default: Boolean,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, Boolean> = BooleanProperty(name, default, commit)
) = BooleanProperty(name, default, commit)
protected fun preference(
fun preference(
name: String,
default: Float,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, Float> = FloatProperty(name, default, commit)
) = FloatProperty(name, default, commit)
protected fun preference(
fun preference(
name: String,
default: Int,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, Int> = IntProperty(name, default, commit)
) = IntProperty(name, default, commit)
protected fun preference(
fun preference(
name: String,
default: Long,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, Long> = LongProperty(name, default, commit)
) = LongProperty(name, default, commit)
protected fun preference(
fun preference(
name: String,
default: String,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, String> = StringProperty(name, default, commit)
) = StringProperty(name, default, commit)
protected fun preference(
fun preference(
name: String,
default: Set<String>,
commit: Boolean = commitPrefs
): ReadWriteProperty<PreferenceModel, Set<String>> = StringSetProperty(name, default, commit)
) = StringSetProperty(name, default, commit)
}
}

View File

@@ -16,7 +16,7 @@ class StringProperty(
property: KProperty<*>
): String {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -16,7 +16,7 @@ class StringSetProperty(
property: KProperty<*>
): Set<String> {
val prefName = name.trimEmptyToNull() ?: property.name
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(

View File

@@ -6,14 +6,14 @@ import android.content.Intent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.DownloadApp
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
@@ -64,9 +64,8 @@ open class GeneralReceiver : BroadcastReceiver() {
}
Intent.ACTION_PACKAGE_REPLACED ->
// This will only work pre-O
if (Config.get<Boolean>(Config.Key.SU_REAUTH)!!) {
if (Config.suReAuth)
appRepo.delete(getPkg(intent)).blockingGet()
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
appRepo.delete(pkg).blockingGet()
@@ -74,10 +73,11 @@ open class GeneralReceiver : BroadcastReceiver() {
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
Info.remote = Info.remote.copy(app = Info.remote.app.copy(
link = intent.getStringExtra(Const.Key.INTENT_SET_LINK) ?: ""))
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
}
Const.Key.BROADCAST_REBOOT -> RootUtils.reboot()
Const.Key.BROADCAST_REBOOT -> reboot()
}
}
}

View File

@@ -2,30 +2,29 @@ package com.topjohnwu.magisk.model.update
import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
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
import com.topjohnwu.superuser.Shell
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()
// Make sure shell initializer was ran
Shell.getShell()
return runCatching {
magiskRepo.fetchUpdate().blockingGet()
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
Notifications.managerUpdate()
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
Notifications.magiskUpdate()
ListenableWorker.Result.success()
}.getOrElse {
ListenableWorker.Result.failure()
}
}
}

View File

@@ -4,12 +4,6 @@ import android.content.Context;
import android.net.Network;
import android.net.Uri;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -17,6 +11,12 @@ import androidx.annotation.RequiresApi;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public abstract class DelegateWorker {
private ListenableWorker worker;

View File

@@ -8,8 +8,8 @@ import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.DownloadProgressListener;
import com.topjohnwu.net.Networking;
@@ -123,9 +123,9 @@ public abstract class MagiskInstaller {
File zip = new File(App.self.getCacheDir(), "magisk.zip");
if (!ShellUtils.checkSum("MD5", zip, Config.magiskMD5)) {
if (!ShellUtils.checkSum("MD5", zip, Info.remote.getMagisk().getHash())) {
console.add("- Downloading zip");
Networking.get(Config.magiskLink)
Networking.get(Info.remote.getMagisk().getLink())
.setDownloadProgressListener(new ProgressLog())
.execForFile(zip);
} else {
@@ -282,10 +282,10 @@ public abstract class MagiskInstaller {
return false;
}
if (!Shell.sh(Utils.fmt(
if (!Shell.sh(Utils.INSTANCE.fmt(
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " +
"sh update-binary sh boot_patch.sh %s",
Config.keepEnc, Config.keepVerity, Config.recovery, srcBoot))
Info.keepEnc, Info.keepVerity, Info.recovery, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
@@ -311,10 +311,10 @@ public abstract class MagiskInstaller {
}
protected boolean flashBoot() {
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, srcBoot))
if (!Shell.su(Utils.INSTANCE.fmt("direct_install %s %s", installDir, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
if (!Config.keepVerity)
if (!Info.keepVerity)
Shell.su("patch_dtbo_image").to(console, logs).exec();
return true;
}

View File

@@ -18,7 +18,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
@@ -36,12 +35,6 @@ import io.reactivex.Single;
@Deprecated
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;
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;
@@ -70,14 +63,25 @@ public class UpdateRepos {
}
}
/**
* Static instance of (Simple)DateFormat is not threadsafe so in order to make it safe it needs
* to be created beforehand on the same thread where it'll be used.
* See https://stackoverflow.com/a/18383395
*/
private static SimpleDateFormat getDateFormat() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
/* We sort repos by last push, which means that we only need to check whether the
* first page is updated to determine whether the online repo database is changed
*/
private boolean parsePage(int page) {
Request req = Networking.get(Utils.fmt(Const.Url.REPO_URL, page + 1));
Request req = Networking.get(Utils.INSTANCE.fmt(Const.Url.REPO_URL, page + 1));
if (page == 0) {
String etag = Config.get(Config.Key.ETAG_KEY);
if (etag != null)
String etag = Config.getEtagKey();
if (!etag.isEmpty())
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
}
Request.Result<JSONArray> res = req.execForJSONArray();
@@ -94,10 +98,12 @@ public class UpdateRepos {
return true;
try {
SimpleDateFormat dateFormat = getDateFormat();
for (int i = 0; i < res.getResult().length(); i++) {
JSONObject rawRepo = res.getResult().getJSONObject(i);
String id = rawRepo.getString("name");
Date date = DATE_FORMAT.parse(rawRepo.getString("pushed_at"));
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
moduleQueue.offer(new Pair<>(id, date));
}
} catch (JSONException | ParseException e) {
@@ -110,7 +116,7 @@ public class UpdateRepos {
String etag = res.getConnection().getHeaderField(Config.Key.ETAG_KEY);
if (etag != null) {
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
Config.set(Config.Key.ETAG_KEY, etag);
Config.setEtagKey(etag);
}
}

View File

@@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.model.navigation.Navigation
@@ -105,11 +106,11 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible =
Shell.rootAccess() && Config.get<Any>(Config.Key.MAGISKHIDE) as Boolean
Shell.rootAccess() && Config.magiskHide
menu.findItem(R.id.modulesFragment).isVisible =
Shell.rootAccess() && Config.magiskVersionCode >= 0
Shell.rootAccess() && Info.magiskVersionCode >= 0
menu.findItem(R.id.reposFragment).isVisible =
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Config.magiskVersionCode >= 0)
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Info.magiskVersionCode >= 0)
menu.findItem(R.id.logFragment).isVisible =
Shell.rootAccess()
menu.findItem(R.id.superuserFragment).isVisible =

View File

@@ -5,13 +5,11 @@ 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.UpdateRepos
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.get
@@ -21,7 +19,7 @@ open class SplashActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
Shell.getShell {
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
if (Info.magiskVersionCode > 0 && Info.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(R.string.unsupport_magisk_message)
@@ -35,9 +33,9 @@ open class SplashActivity : AppCompatActivity() {
}
private fun initAndStart() {
val pkg = Config.get<String>(Config.Key.SU_MANAGER)
if (pkg != null && packageName == BuildConfig.APPLICATION_ID) {
Config.remove(Config.Key.SU_MANAGER)
val pkg = Config.suManager
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
get<SettingsDao>().delete(Config.Key.SU_MANAGER)
Shell.su("pm uninstall $pkg").submit()
}
if (TextUtils.equals(pkg, packageName)) {
@@ -56,21 +54,10 @@ open class SplashActivity : AppCompatActivity() {
// Schedule periodic update checks
Utils.scheduleUpdateCheck()
//CheckUpdates.check()
// Setup shortcuts
Shortcuts.setup(this)
// Magisk working as expected
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
// Load modules
//Utils.loadModules(false)
// Load repos
if (Networking.checkNetworkStatus(this)) {
get<UpdateRepos>().exec().subscribeK()
}
}
val intent = Intent(this, ClassMap[MainActivity::class.java])
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
DONE = true

View File

@@ -1,7 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface ActivityResultListener {
fun onActivityResult(resultCode: Int, data: Intent?)
}

View File

@@ -12,20 +12,14 @@ 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,
@@ -67,19 +61,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
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
}
protected fun <T: Preference> findPref(key: CharSequence): T {
return findPreference(key) as T
}
}

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface IBaseLeanback {
fun runWithExternalRW(callback: Runnable)
fun runWithPermissions(vararg permissions: String, callback: Runnable)
fun startActivityForResult(intent: Intent, requestCode: Int, listener: ActivityResultListener)
}

View File

@@ -1,10 +1,13 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
@@ -16,6 +19,7 @@ import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.skoumal.teanity.view.TeanityActivity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.events.BackPressEvent
@@ -27,16 +31,20 @@ import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.set
import timber.log.Timber
import kotlin.reflect.KClass
typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
MagiskLeanbackActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
override val numberOfRootFragments: Int get() = baseFragments.size
override val baseFragments: List<KClass<out Fragment>> = listOf()
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
protected open val defaultPosition: Int = 0
@@ -50,14 +58,12 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
init {
val isDarkTheme = Config.get<Boolean>(Config.Key.DARK_THEME)
val theme = if (isDarkTheme) {
val theme = if (Config.darkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun applyOverrideConfiguration(config: Configuration?) {
@@ -66,6 +72,10 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
super.applyOverrideConfiguration(config)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigationController?.apply {
@@ -185,19 +195,23 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) =
if (report?.areAllPermissionsGranted() == true) {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
if (report.areAllPermissionsGranted()) {
request.onSuccess()
} else {
request.onFailure()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = request.onShowRationale(permissions.orEmpty().map { it.name })
})
.check()
permissions: MutableList<PermissionRequest>,
token: PermissionToken
) = token.continuePermissionRequest()
}).check()
}
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
@@ -207,4 +221,21 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply {
resultCallbacks.remove(requestCode)
invoke(this@MagiskActivity, resultCode, data)
}
}
fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: RequestCallback
) {
resultCallbacks[requestCode] = listener
startActivityForResult(intent, requestCode)
}
}

View File

@@ -1,64 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Intent
import androidx.collection.SparseArrayCompat
import androidx.databinding.ViewDataBinding
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.skoumal.teanity.view.TeanityActivity
import com.topjohnwu.magisk.Const
abstract class MagiskLeanbackActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityActivity<ViewModel, Binding>(), IBaseLeanback {
private val resultListeners = SparseArrayCompat<ActivityResultListener>()
@Deprecated("Permissions will be checked in a different streamlined way")
fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() })
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithExternalRW(callback: Runnable) {
runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback)
}
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithPermissions(vararg permissions: String, callback: Runnable) {
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
if (report?.areAllPermissionsGranted() == true) {
Const.EXTERNAL_PATH.mkdirs()
callback.run()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = Unit
})
.check()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultListeners.get(requestCode)?.apply {
resultListeners.remove(requestCode)
onActivityResult(resultCode, data)
}
}
override fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: ActivityResultListener
) {
resultListeners.put(requestCode, listener)
startActivityForResult(intent, requestCode)
}
}

View File

@@ -103,7 +103,7 @@ class FlashViewModel(
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = RootUtils.reboot()
fun restartPressed() = reboot()
fun backPressed() = back()

View File

@@ -28,7 +28,7 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
(findItem(R.id.app_search).actionView as? SearchView)
?.setOnQueryTextListener(this@MagiskHideFragment)
val showSystem = Config.get<Boolean>(Config.Key.SHOW_SYSTEM_APP)
val showSystem = Config.showSystemApp
findItem(R.id.show_system).isChecked = showSystem
viewModel.isShowSystem.value = showSystem
@@ -39,10 +39,8 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
if (item.itemId == R.id.show_system) {
val showSystem = !item.isChecked
item.isChecked = showSystem
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem)
Config.showSystemApp = showSystem
viewModel.isShowSystem.value = showSystem
//adapter!!.setShowSystem(showSystem)
//adapter!!.filter(search!!.query.toString())
}
return true
}
@@ -56,9 +54,4 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
viewModel.query.value = query.orEmpty()
return false
}
/*override fun onEvent(event: Int) {
//mSwipeRefreshLayout!!.isRefreshing = false
adapter!!.filter(search!!.query.toString())
}*/
}

View File

@@ -1,16 +1,20 @@
package com.topjohnwu.magisk.ui.home
import android.app.Activity
import android.os.Bundle
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.utils.ISafetyNetHelper
import com.topjohnwu.magisk.utils.copyTo
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import dalvik.system.DexClassLoader
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -21,6 +25,8 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
override val layoutRes: Int = R.layout.fragment_magisk
override val viewModel: HomeViewModel by viewModel()
val magiskRepo: MagiskRepository by inject()
lateinit var EXT_FILE: File
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
@@ -37,6 +43,11 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EXT_FILE = File("${requireActivity().filesDir.parent}/snet", "snet.apk")
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
@@ -45,7 +56,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private fun installMagisk() {
// Show Manager update first
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) {
installManager()
return
}
@@ -61,9 +72,9 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = Networking
.get(Const.Url.SNET_URL)
.getAsFile(EXT_APK) { updateSafetyNet(true) }
fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().copyTo(EXT_FILE) }
.subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) {
download()

View File

@@ -4,10 +4,7 @@ import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSubscribeUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
@@ -25,8 +22,8 @@ class HomeViewModel(
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Config.keepEnc)
val isKeepVerity = KObservableField(Config.keepVerity)
val isForceEncryption = KObservableField(Info.keepEnc)
val isKeepVerity = KObservableField(Info.keepVerity)
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
@@ -41,7 +38,7 @@ class HomeViewModel(
val magiskCurrentVersion = KObservableField("")
val magiskLatestVersion = KObservableField("")
val magiskAdditionalInfo = Observer(magiskState) {
if (Config.get<Boolean>(Config.Key.COREONLY))
if (Config.coreOnly)
R.string.core_only_enabled.res()
else
""
@@ -87,10 +84,10 @@ class HomeViewModel(
init {
isForceEncryption.addOnPropertyChangedCallback {
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
Info.keepEnc = it ?: return@addOnPropertyChangedCallback
}
isKeepVerity.addOnPropertyChangedCallback {
Config.keepVerity = it ?: return@addOnPropertyChangedCallback
Info.keepVerity = it ?: return@addOnPropertyChangedCallback
}
refresh()
@@ -154,7 +151,7 @@ class HomeViewModel(
}
fun refresh() {
magiskRepo.fetchConfig()
magiskRepo.fetchUpdate()
.applyViewModel(this)
.doOnSubscribeUi {
magiskState.value = MagiskState.LOADING
@@ -164,14 +161,6 @@ class HomeViewModel(
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()
}
@@ -181,22 +170,22 @@ class HomeViewModel(
private fun updateSelf() {
state = State.LOADED
magiskState.value = when (Config.magiskVersionCode) {
magiskState.value = when (Info.magiskVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
!in Config.remoteMagiskVersionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
version.format(Config.magiskVersionString, Config.magiskVersionCode)
version.format(Info.magiskVersionString, Info.magiskVersionCode)
} else {
""
}
magiskLatestVersion.value = version
.format(Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)
.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
managerState.value = when (Config.remoteManagerVersionCode) {
managerState.value = when (Info.remote.app.versionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
@@ -206,7 +195,7 @@ class HomeViewModel(
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
managerLatestVersion.value = version
.format(Config.remoteManagerVersionString, Config.remoteManagerVersionCode)
.format(Info.remote.app.version, Info.remote.app.versionCode)
}
private fun ensureEnv() {

View File

@@ -12,6 +12,7 @@ import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.model.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
@@ -30,6 +31,7 @@ class LogViewModel(
private val logRepo: LogRepository
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val itemsAdapter = BindingAdapter()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
@@ -41,6 +43,8 @@ class LogViewModel(
private val logItem get() = items[0] as LogRvItem
private val magiskLogItem get() = items[1] as MagiskLogRvItem
val scrollPosition = KObservableField(0)
init {
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
@@ -57,6 +61,10 @@ class LogViewModel(
else -> ""
}
fun scrollDownPressed() {
scrollPosition.value = magiskLogItem.items.size - 1
}
fun refresh() {
fetchLogs().subscribeK { logItem.update(it) }
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }

View File

@@ -16,7 +16,7 @@ import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@@ -64,19 +64,20 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.reboot -> {
RootUtils.reboot()
reboot()
return true
}
R.id.reboot_recovery -> {
Shell.su("/system/bin/reboot recovery").submit()
reboot("recovery")
return true
}
R.id.reboot_bootloader -> {
reboot("booloader")
Shell.su("/system/bin/reboot bootloader").submit()
return true
}
R.id.reboot_download -> {
Shell.su("/system/bin/reboot download").submit()
reboot("download")
return true
}
else -> return false
@@ -84,32 +85,12 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
}
private fun selectFile() {
magiskActivity.runWithExternalRW {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
magiskActivity.withExternalRW {
onSuccess {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
}
/*override fun getListeningEvents(): IntArray {
return intArrayOf(Event.MODULE_LOAD_DONE)
}
override fun onEvent(event: Int) {
updateUI(Event.getResult(event))
}*/
/*private fun updateUI(moduleMap: Map<String, Module>) {
listModules.clear()
listModules.addAll(moduleMap.values)
if (listModules.size == 0) {
emptyRv!!.visibility = View.VISIBLE
recyclerView!!.visibility = View.GONE
} else {
emptyRv!!.visibility = View.GONE
recyclerView!!.visibility = View.VISIBLE
recyclerView!!.adapter = ModulesAdapter(listModules)
}
mSwipeRefreshLayout!!.isRefreshing = false
}*/
}

View File

@@ -53,9 +53,9 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(
R.array.sorting_orders,
Config.get<Int>(Config.Key.REPO_ORDER)!!
Config.repoOrder
) { d, which ->
Config.set(Config.Key.REPO_ORDER, which)
Config.repoOrder = which
viewModel.refresh(false)
d.dismiss()
}.show()
@@ -74,20 +74,22 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
}
private fun openChangelog(item: Repo) {
MarkDownWindow.show(context, null, item.detailUrl)
MarkDownWindow.show(requireActivity(), null, item.detailUrl)
}
private fun installModule(item: Repo) {
val context = magiskActivity
fun download(install: Boolean) {
context.runWithExternalRW {
val intent = Intent(activity, ClassMap[DownloadModuleService::class.java])
.putExtra("repo", item).putExtra("install", install)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary
} else {
context.startService(intent)
context.withExternalRW {
onSuccess {
val intent = Intent(activity, ClassMap[DownloadModuleService::class.java])
.putExtra("repo", item).putExtra("install", install)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
}

View File

@@ -1,321 +0,0 @@
package com.topjohnwu.magisk.ui.settings;
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.widget.EditText;
import android.widget.Toast;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.KConfig;
import com.topjohnwu.magisk.KConfig.UpdateChannel;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment;
import com.topjohnwu.magisk.utils.DownloadApp;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.PatchAPK;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import java.io.IOException;
import java.util.Arrays;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
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 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 final void onStart() {
super.onStart();
setHasOptionsMenu(true);
requireActivity().setTitle(R.string.settings);
}
@Override
public final void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setStorageDeviceProtected();
setPreferencesFromResource(R.xml.app_settings, rootKey);
boolean showSuperuser = Utils.showSuperUser();
getPrefs().edit()
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
.apply();
PreferenceScreen prefScreen = getPreferenceScreen();
PreferenceCategory generalCatagory = (PreferenceCategory) findPreference("general");
PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk");
PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser");
Preference hideManager = findPreference("hide");
hideManager.setOnPreferenceClickListener(pref -> {
PatchAPK.hideManager();
return true;
});
Preference restoreManager = findPreference("restore");
restoreManager.setOnPreferenceClickListener(pref -> {
DownloadApp.restore();
return true;
});
findPreference("clear").setOnPreferenceClickListener(pref -> {
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.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT);
return true;
});
updateChannel = (ListPreference) findPreference(Config.Key.UPDATE_CHANNEL);
rootConfig = (ListPreference) findPreference(Config.Key.ROOT_ACCESS);
autoRes = (ListPreference) findPreference(Config.Key.SU_AUTO_RESPONSE);
requestTimeout = (ListPreference) findPreference(Config.Key.SU_REQUEST_TIMEOUT);
suNotification = (ListPreference) findPreference(Config.Key.SU_NOTIFICATION);
multiuserConfig = (ListPreference) findPreference(Config.Key.SU_MULTIUSER_MODE);
nsConfig = (ListPreference) findPreference(Config.Key.SU_MNT_NS);
SwitchPreferenceCompat reauth = (SwitchPreferenceCompat) findPreference(Config.Key.SU_REAUTH);
SwitchPreferenceCompat fingerprint = (SwitchPreferenceCompat) findPreference(Config.Key.SU_FINGERPRINT);
updateChannel.setOnPreferenceChangeListener((p, o) -> {
int channel = Integer.parseInt((String) o);
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(KConfig.getCustomUpdateChannel());
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.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() &&
(int) Config.get(Config.Key.UPDATE_CHANNEL) < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
CharSequence[] entries = updateChannel.getEntries();
updateChannel.setEntries(Arrays.copyOf(entries, entries.length - 2));
}
setSummary();
// Disable dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig);
}
// Disable re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reauth.setEnabled(false);
reauth.setChecked(false);
reauth.setSummary(R.string.android_o_not_support);
}
// Disable fingerprint option if not possible
if (!FingerprintHelper.canUseFingerprint()) {
fingerprint.setEnabled(false);
fingerprint.setChecked(false);
fingerprint.setSummary(R.string.disable_fingerprint);
}
if (Shell.rootAccess() && Const.USER_ID == 0) {
if (getApp().getPackageName().equals(BuildConfig.APPLICATION_ID)) {
generalCatagory.removePreference(restoreManager);
} else {
if (!Networking.checkNetworkStatus(requireContext())) {
generalCatagory.removePreference(restoreManager);
}
generalCatagory.removePreference(hideManager);
}
} else {
generalCatagory.removePreference(restoreManager);
generalCatagory.removePreference(hideManager);
}
if (!showSuperuser) {
prefScreen.removePreference(suCategory);
}
if (!Shell.rootAccess()) {
prefScreen.removePreference(magiskCategory);
generalCatagory.removePreference(hideManager);
}
}
@Override
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:
getSettingRepo().put(key, Utils.getPrefsInt(prefs, key))
.subscribe(() -> {
}, Throwable::printStackTrace);
break;
case Config.Key.DARK_THEME:
requireActivity().recreate();
break;
case Config.Key.COREONLY:
if (prefs.getBoolean(key, false)) {
try {
Const.MAGISK_DISABLE_FILE.createNewFile();
} catch (IOException ignored) {
}
} else {
Const.MAGISK_DISABLE_FILE.delete();
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG);
break;
case Config.Key.MAGISKHIDE:
if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit();
} else {
Shell.su("magiskhide --disable").submit();
}
break;
case Config.Key.LOCALE:
LocaleManager.setLocale(getApp());
requireActivity().recreate();
break;
case Config.Key.UPDATE_CHANNEL:
case Config.Key.CUSTOM_CHANNEL:
//CheckUpdates.check();
break;
case Config.Key.CHECK_UPDATES:
Utils.scheduleUpdateCheck();
break;
}
setSummary(key);
}
@Override
public final boolean onPreferenceTreeClick(Preference preference) {
String key = preference.getKey();
switch (key) {
case Config.Key.SU_FINGERPRINT:
boolean checked = ((SwitchPreferenceCompat) preference).isChecked();
((SwitchPreferenceCompat) preference).setChecked(!checked);
new FingerprintAuthDialog(requireActivity(), () -> {
((SwitchPreferenceCompat) preference).setChecked(checked);
Config.set(key, checked);
}).show();
break;
}
return true;
}
private void setSummary(String key) {
switch (key) {
case Config.Key.UPDATE_CHANNEL:
int ch = Config.get(key);
ch = ch < 0 ? Config.Value.STABLE_CHANNEL : ch;
updateChannel.setSummary(getResources()
.getStringArray(R.array.update_channel)[ch]);
break;
case Config.Key.ROOT_ACCESS:
rootConfig.setSummary(getResources()
.getStringArray(R.array.su_access)[(int) Config.get(key)]);
break;
case Config.Key.SU_AUTO_RESPONSE:
autoRes.setSummary(getResources()
.getStringArray(R.array.auto_response)[(int) Config.get(key)]);
break;
case Config.Key.SU_NOTIFICATION:
suNotification.setSummary(getResources()
.getStringArray(R.array.su_notification)[(int) Config.get(key)]);
break;
case Config.Key.SU_REQUEST_TIMEOUT:
requestTimeout.setSummary(
getString(R.string.request_timeout_summary, (int) Config.get(key)));
break;
case Config.Key.SU_MULTIUSER_MODE:
multiuserConfig.setSummary(getResources()
.getStringArray(R.array.multiuser_summary)[(int) Config.get(key)]);
break;
case Config.Key.SU_MNT_NS:
nsConfig.setSummary(getResources()
.getStringArray(R.array.namespace_summary)[(int) Config.get(key)]);
break;
}
}
private void setSummary() {
setSummary(Config.Key.UPDATE_CHANNEL);
setSummary(Config.Key.ROOT_ACCESS);
setSummary(Config.Key.SU_AUTO_RESPONSE);
setSummary(Config.Key.SU_NOTIFICATION);
setSummary(Config.Key.SU_REQUEST_TIMEOUT);
setSummary(Config.Key.SU_MULTIUSER_MODE);
setSummary(Config.Key.SU_MNT_NS);
}
}

View File

@@ -0,0 +1,265 @@
package com.topjohnwu.magisk.ui.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreferenceCompat
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.inject
class SettingsFragment : BasePreferenceFragment() {
private val repoDatabase: RepoDatabaseHelper by inject()
private lateinit var updateChannel: ListPreference
private lateinit var autoRes: ListPreference
private lateinit var suNotification: ListPreference
private lateinit var requestTimeout: ListPreference
private lateinit var rootConfig: ListPreference
private lateinit var multiuserConfig: ListPreference
private lateinit var nsConfig: ListPreference
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.settings)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.setStorageDeviceProtected()
setPreferencesFromResource(R.xml.app_settings, rootKey)
updateChannel = findPref(Config.Key.UPDATE_CHANNEL)
rootConfig = findPref(Config.Key.ROOT_ACCESS)
autoRes = findPref(Config.Key.SU_AUTO_RESPONSE)
requestTimeout = findPref(Config.Key.SU_REQUEST_TIMEOUT)
suNotification = findPref(Config.Key.SU_NOTIFICATION)
multiuserConfig = findPref(Config.Key.SU_MULTIUSER_MODE)
nsConfig = findPref(Config.Key.SU_MNT_NS)
val reauth = findPreference(Config.Key.SU_REAUTH) as SwitchPreferenceCompat
val fingerprint = findPreference(Config.Key.SU_FINGERPRINT) as SwitchPreferenceCompat
val generalCatagory = findPreference("general") as PreferenceCategory
val magiskCategory = findPreference("magisk") as PreferenceCategory
val suCategory = findPreference("superuser") as PreferenceCategory
val hideManager = findPreference("hide")
hideManager.setOnPreferenceClickListener {
PatchAPK.hideManager()
true
}
val restoreManager = findPreference("restore")
restoreManager.setOnPreferenceClickListener {
DownloadApp.restore()
true
}
findPreference("clear").setOnPreferenceClickListener {
prefs.edit {
remove(Config.Key.ETAG_KEY)
}
repoDatabase.clearRepo()
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
true
}
findPreference("hosts").setOnPreferenceClickListener {
Shell.su("add_hosts_module").exec()
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
true
}
updateChannel.setOnPreferenceChangeListener { _, value ->
val channel = Integer.parseInt(value as String)
val previous = Config.updateChannel
if (channel == Config.Value.CUSTOM_CHANNEL) {
val v = LayoutInflater.from(requireActivity())
.inflate(R.layout.custom_channel_dialog, null)
val url = v.findViewById<EditText>(R.id.custom_url)
url.setText(Config.customChannelUrl)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(R.string.ok) { _, _ ->
Config.customChannelUrl = url.text.toString() }
.setNegativeButton(R.string.close) { _, _ ->
Config.updateChannel = previous }
.setOnCancelListener { Config.updateChannel = previous }
.show()
}
true
}
setLocalePreference(findPreference(Config.Key.LOCALE) as ListPreference)
/* We only show canary channels if user is already on canary channel
* or the user have already chosen canary channel */
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
val entries = updateChannel.entries
updateChannel.entries = entries.copyOf(entries.size - 2)
}
setSummary()
// Disable dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig)
}
// Disable re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reauth.isEnabled = false
reauth.isChecked = false
reauth.setSummary(R.string.android_o_not_support)
}
// Disable fingerprint option if not possible
if (!FingerprintHelper.canUseFingerprint()) {
fingerprint.isEnabled = false
fingerprint.isChecked = false
fingerprint.setSummary(R.string.disable_fingerprint)
}
if (Shell.rootAccess() && Const.USER_ID == 0) {
if (app.packageName == BuildConfig.APPLICATION_ID) {
generalCatagory.removePreference(restoreManager)
} else {
if (!Networking.checkNetworkStatus(requireContext())) {
generalCatagory.removePreference(restoreManager)
}
generalCatagory.removePreference(hideManager)
}
} else {
generalCatagory.removePreference(restoreManager)
generalCatagory.removePreference(hideManager)
}
if (!Utils.showSuperUser()) {
preferenceScreen.removePreference(suCategory)
}
if (!Shell.rootAccess()) {
preferenceScreen.removePreference(magiskCategory)
generalCatagory.removePreference(hideManager)
}
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> Config.rootMode = Utils.getPrefsInt(prefs, key)
Config.Key.SU_MULTIUSER_MODE -> Config.suMultiuserMode = Utils.getPrefsInt(prefs, key)
Config.Key.SU_MNT_NS -> Config.suMntNamespaceMode = Utils.getPrefsInt(prefs, key)
Config.Key.DARK_THEME -> requireActivity().recreate()
Config.Key.COREONLY -> {
if (prefs.getBoolean(key, false)) {
runCatching {
Const.MAGISK_DISABLE_FILE.createNewFile()
}
} else {
Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
Config.Key.MAGISKHIDE -> if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit()
} else {
Shell.su("magiskhide --disable").submit()
}
Config.Key.LOCALE -> {
LocaleManager.setLocale(app)
requireActivity().recreate()
}
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck()
}
setSummary(key)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
Config.Key.SU_FINGERPRINT -> {
val checked = (preference as SwitchPreferenceCompat).isChecked
preference.isChecked = !checked
FingerprintAuthDialog(requireActivity()) {
preference.isChecked = checked
Config.suFingerprint = checked
}.show()
}
}
return true
}
private fun setLocalePreference(lp: ListPreference) {
lp.isEnabled = false
LocaleManager.availableLocales
.map {
val names = mutableListOf<String>()
val values = mutableListOf<String>()
names.add(LocaleManager.getString(
LocaleManager.defaultLocale, R.string.system_default))
values.add("")
it.forEach { locale ->
names.add(locale.getDisplayName(locale))
values.add(LocaleManager.toLanguageTag(locale))
}
Pair(names.toTypedArray(), values.toTypedArray())
}.subscribeK { (names, values) ->
lp.isEnabled = true
lp.entries = names
lp.entryValues = values
lp.summary = LocaleManager.locale.getDisplayName(LocaleManager.locale)
}
}
private fun setSummary(key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> rootConfig.summary = resources
.getStringArray(R.array.su_access)[Config.rootMode]
Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources
.getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode]
Config.Key.SU_MNT_NS -> nsConfig.summary = resources
.getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode]
Config.Key.UPDATE_CHANNEL -> {
var ch = Config.updateChannel
ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch
updateChannel.summary = resources
.getStringArray(R.array.update_channel)[ch]
}
Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources
.getStringArray(R.array.auto_response)[Config.suAutoReponse]
Config.Key.SU_NOTIFICATION -> suNotification.summary = resources
.getStringArray(R.array.su_notification)[Config.suNotification]
Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary =
getString(R.string.request_timeout_summary, Config.suDefaultTimeout)
}
}
private fun setSummary() {
setSummary(Config.Key.ROOT_ACCESS)
setSummary(Config.Key.SU_MULTIUSER_MODE)
setSummary(Config.Key.SU_MNT_NS)
setSummary(Config.Key.UPDATE_CHANNEL)
setSummary(Config.Key.SU_AUTO_RESPONSE)
setSummary(Config.Key.SU_NOTIFICATION)
setSummary(Config.Key.SU_REQUEST_TIMEOUT)
}
}

View File

@@ -55,9 +55,16 @@ class SuperuserViewModel(
.flattenAsFlowable { it }
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.item.appName.toLowerCase() },
{ it.item.packageName }
))
}
.map { it to items.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK { items.update(it) }
.subscribeK { items.update(it.first, it.second) }
.add()
}

View File

@@ -173,7 +173,7 @@ class SuRequestViewModel(
return true
}
when (Config.get<Int>(Config.Key.SU_AUTO_RESPONSE)) {
when (Config.suAutoReponse) {
Config.Value.SU_AUTO_DENY -> {
handler?.handleAction(Policy.DENY, 0)
return true
@@ -190,8 +190,7 @@ class SuRequestViewModel(
@SuppressLint("ClickableViewAccessibility")
private fun showUI() {
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
val millis = SECONDS.toMillis(seconds)
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
timer = object : CountDownTimer(millis, 1000) {
override fun onTick(remains: Long) {
denyText.value = "%s (%d)"

View File

@@ -11,8 +11,10 @@ import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R
@@ -176,4 +178,40 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
} else {
view.adapter?.removeListener()
}
}
}
@BindingAdapter("hide")
fun setHidden(view: FloatingActionButton, hide: Boolean) {
if (hide) view.hide() else view.show()
}
@BindingAdapter("scrollPosition", "scrollPositionSmooth", requireAll = false)
fun setScrollPosition(view: RecyclerView, position: Int, smoothScroll: Boolean) {
val adapterItemCount = view.adapter?.itemCount ?: -1
if (position !in 0 until adapterItemCount) {
// the position is not in adapter bounds, adapter will throw exception for invalid positions
return
}
when {
smoothScroll -> view.smoothScrollToPosition(position)
else -> view.scrollToPosition(position)
}
}
@BindingAdapter("recyclerScrollEvent")
fun setScrollListener(view: RecyclerView, listener: InverseBindingListener) {
view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// don't change this or the recycler will stop at every line, effectively disabling smooth scroll
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
listener.onChange()
}
}
})
}
@InverseBindingAdapter(attribute = "scrollPosition", event = "recyclerScrollEvent")
fun getScrollPosition(view: RecyclerView) = (view.layoutManager as? LinearLayoutManager)
?.findLastCompletelyVisibleItemPosition()
?: -1

View File

@@ -6,6 +6,7 @@ import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ui.SplashActivity;
import com.topjohnwu.magisk.view.ProgressNotification;
@@ -24,8 +25,8 @@ public class DownloadApp {
}
public static void restore() {
String name = Utils.fmt("MagiskManager v%s(%d)",
Config.remoteManagerVersionString, Config.remoteManagerVersionCode);
String name = Utils.INSTANCE.fmt("MagiskManager v%s(%d)",
Info.remote.getApp().getVersion(), Info.remote.getApp().getVersionCode());
dlInstall(name, new RestoreManager());
}
@@ -33,7 +34,7 @@ public class DownloadApp {
File apk = new File(App.self.getCacheDir(), "manager.apk");
ProgressNotification progress = new ProgressNotification(name);
listener.progress = progress;
Networking.get(Config.managerLink)
Networking.get(Info.remote.getApp().getLink())
.setExecutor(App.THREAD_POOL)
.setDownloadProgressListener(progress)
.setErrorHandler((conn, e) -> progress.dlFail())

View File

@@ -1,123 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
@TargetApi(Build.VERSION_CODES.M)
public abstract class FingerprintHelper {
private FingerprintManager manager;
private Cipher cipher;
private CancellationSignal cancel;
public static boolean useFingerprint() {
boolean fp = Config.get(Config.Key.SU_FINGERPRINT);
if (fp && !canUseFingerprint()) {
Config.set(Config.Key.SU_FINGERPRINT, false);
fp = false;
}
return fp;
}
public static boolean canUseFingerprint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return false;
KeyguardManager km = App.self.getSystemService(KeyguardManager.class);
FingerprintManager fm = App.self.getSystemService(FingerprintManager.class);
return km.isKeyguardSecure() && fm != null && fm.isHardwareDetected() && fm.hasEnrolledFingerprints();
}
protected FingerprintHelper() throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
manager = App.self.getSystemService(FingerprintManager.class);
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(Const.SU_KEYSTORE_KEY, null);
if (key == null) {
key = generateKey();
}
try {
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
// Only happens on Marshmallow
key = generateKey();
cipher.init(Cipher.ENCRYPT_MODE, key);
}
}
public abstract void onAuthenticationError(int errorCode, CharSequence errString);
public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString);
public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result);
public abstract void onAuthenticationFailed();
public void authenticate() {
cancel = new CancellationSignal();
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
manager.authenticate(cryptoObject, cancel, 0, new Callback(), null);
}
public void cancel() {
if (cancel != null)
cancel.cancel();
}
private SecretKey generateKey() throws Exception {
KeyGenerator keygen = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
Const.SU_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(false);
}
keygen.init(builder.build());
return keygen.generateKey();
}
private class Callback extends FingerprintManager.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
FingerprintHelper.this.onAuthenticationError(errorCode, errString);
}
@Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
FingerprintHelper.this.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationFailed() {
FingerprintHelper.this.onAuthenticationFailed();
}
}
}

View File

@@ -0,0 +1,119 @@
package com.topjohnwu.magisk.utils
import android.annotation.TargetApi
import android.app.KeyguardManager
import android.content.Context
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.topjohnwu.magisk.Config
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
@TargetApi(Build.VERSION_CODES.M)
abstract class FingerprintHelper @Throws(Exception::class)
protected constructor() {
private val manager: FingerprintManager?
private val cipher: Cipher
private var cancel: CancellationSignal? = null
private val context: Context by inject()
init {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
manager = context.getSystemService(FingerprintManager::class.java)
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyStore.load(null)
var key = keyStore.getKey(SU_KEYSTORE_KEY, null) as SecretKey? ?: generateKey()
runCatching {
cipher.init(Cipher.ENCRYPT_MODE, key)
}.onFailure {
// Only happens on Marshmallow
key = generateKey()
cipher.init(Cipher.ENCRYPT_MODE, key)
}
}
abstract fun onAuthenticationError(errorCode: Int, errString: CharSequence)
abstract fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence)
abstract fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult)
abstract fun onAuthenticationFailed()
fun authenticate() {
cancel = CancellationSignal()
val cryptoObject = FingerprintManager.CryptoObject(cipher)
manager!!.authenticate(cryptoObject, cancel, 0, Callback(), null)
}
fun cancel() {
if (cancel != null)
cancel!!.cancel()
}
@Throws(Exception::class)
private fun generateKey(): SecretKey {
val keygen = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(
SU_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(false)
}
keygen.init(builder.build())
return keygen.generateKey()
}
private inner class Callback : FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
this@FingerprintHelper.onAuthenticationError(errorCode, errString)
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
this@FingerprintHelper.onAuthenticationHelp(helpCode, helpString)
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
this@FingerprintHelper.onAuthenticationSucceeded(result)
}
override fun onAuthenticationFailed() {
this@FingerprintHelper.onAuthenticationFailed()
}
}
companion object {
private const val SU_KEYSTORE_KEY = "su_key"
fun useFingerprint(): Boolean {
var fp = Config.suFingerprint
if (fp && !canUseFingerprint()) {
Config.suFingerprint = false
fp = false
}
return fp
}
fun canUseFingerprint(context: Context = get()): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return false
val km = context.getSystemService(KeyguardManager::class.java)
val fm = context.getSystemService(FingerprintManager::class.java)
return km?.isKeyguardSecure ?: false &&
fm != null && fm.isHardwareDetected && fm.hasEnrolledFingerprints()
}
}
}

View File

@@ -107,9 +107,9 @@ object LocaleManager {
@JvmStatic
fun setLocale(wrapper: ContextWrapper) {
val localeConfig = Config.get<String>(Config.Key.LOCALE)
val localeConfig = Config.locale
locale = when {
localeConfig.isNullOrEmpty() -> defaultLocale
localeConfig.isEmpty() -> defaultLocale
else -> forLanguageTag(localeConfig)
}
Locale.setDefault(locale)

View File

@@ -13,7 +13,7 @@ public class Logger {
}
public static void debug(String fmt, Object... args) {
debug(Utils.fmt(fmt, args));
debug(Utils.INSTANCE.fmt(fmt, args));
}
public static void error(String line) {
@@ -21,6 +21,6 @@ public class Logger {
}
public static void error(String fmt, Object... args) {
error(Utils.fmt(fmt, args));
error(Utils.INSTANCE.fmt(fmt, args));
}
}

View File

@@ -3,6 +3,8 @@ package com.topjohnwu.magisk.utils;
import android.content.ComponentName;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
@@ -27,8 +29,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import androidx.core.app.NotificationCompat;
public class PatchAPK {
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
@@ -110,7 +110,7 @@ public class PatchAPK {
if (!Shell.su("pm install " + repack).exec().isSuccess())
return false;
Config.set(Config.Key.SU_MANAGER, pkg);
Config.setSuManager(pkg);
Config.export();
RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID,
new ComponentName(pkg, ClassMap.get(SplashActivity.class).getName()));
@@ -145,7 +145,7 @@ public class PatchAPK {
Notifications.progress(app.getString(R.string.hide_manager_title));
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build());
if(!patchAndHide())
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
Utils.INSTANCE.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID);
});
}

View File

@@ -4,8 +4,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
@@ -132,7 +132,7 @@ class RootUtils : Shell.Initializer() {
job.add(context.rawResource(R.raw.util_functions))
.add(context.rawResource(R.raw.utils))
Const.MAGISK_DISABLE_FILE = SuFile("/cache/.disable_magisk")
Config.loadMagiskInfo()
Info.loadMagiskInfo()
} else {
job.add(context.rawResource(R.raw.nonroot_utils))
}
@@ -143,9 +143,9 @@ class RootUtils : Shell.Initializer() {
"export BOOTMODE=true")
.exec()
Config.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean()
Config.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean()
Config.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean()
Info.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean()
Info.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean()
Info.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean()
return true
}
@@ -155,10 +155,5 @@ class RootUtils : Shell.Initializer() {
fun rmAndLaunch(rm: String, component: ComponentName) {
Shell.su("(rm_launch $rm ${component.flattenToString()})").exec()
}
@JvmStatic
fun reboot() {
Shell.su("/system/bin/reboot ${if (Config.recovery) "recovery" else ""}").submit()
}
}
}

View File

@@ -1,10 +1,10 @@
package com.topjohnwu.magisk.utils
import android.content.Context
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
@@ -17,6 +17,8 @@ import java.util.*
object SuLogger {
private val context: Context by inject()
@JvmStatic
fun handleLogs(intent: Intent) {
@@ -66,9 +68,9 @@ object SuLogger {
}
private fun handleNotify(policy: MagiskPolicy) {
if (policy.notification && Config.get<Any>(Config.Key.SU_NOTIFICATION) as Int == Config.Value.NOTIFICATION_TOAST) {
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
Utils.toast(
App.self.getString(
context.getString(
if (policy.policy == Policy.ALLOW)
R.string.su_allow_toast
else

View File

@@ -1,145 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri;
import android.widget.Toast;
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.OldModule;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.io.SuFile;
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) {
UiThreadHandler.run(() -> Toast.makeText(App.self, msg, duration).show());
}
public static void toast(int resId, int duration) {
UiThreadHandler.run(() -> Toast.makeText(App.self, resId, duration).show());
}
public static String dlString(String url) {
String s = Networking.get(url).execForString().getResult();
return s == null ? "" : s;
}
public static int getPrefsInt(SharedPreferences prefs, String key, int def) {
return Integer.parseInt(prefs.getString(key, String.valueOf(def)));
}
public static int getPrefsInt(SharedPreferences prefs, String key) {
return getPrefsInt(prefs, key, 0);
}
public static int dpInPx(int dp) {
float scale = App.self.getResources().getDisplayMetrics().density;
return (int) (dp * scale + 0.5);
}
public static String fmt(String fmt, Object... args) {
return String.format(Locale.US, fmt, args);
}
public static String getAppLabel(ApplicationInfo info, PackageManager pm) {
try {
if (info.labelRes > 0) {
Resources res = pm.getResourcesForApplication(info);
Configuration config = new Configuration();
config.setLocale(LocaleManager.getLocale());
res.updateConfiguration(config, res.getDisplayMetrics());
return res.getString(info.labelRes);
}
} catch (Exception ignored) {}
return info.loadLabel(pm).toString();
}
public static String getLegalFilename(CharSequence filename) {
return filename.toString().replace(" ", "_").replace("'", "").replace("\"", "")
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
.replace("#", "").replace("@", "").replace("\\", "_");
}
@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() {
return Shell.rootAccess() && (Const.USER_ID == 0 ||
(int) Config.get(Config.Key.SU_MULTIUSER_MODE) !=
Config.Value.MULTIUSER_MODE_OWNER_MANAGED);
}
public static boolean isCanary() {
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(service, 12, TimeUnit.HOURS)
.setConstraints(constraints)
.build();
WorkManager.getInstance().enqueueUniquePeriodicWork(
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
ExistingPeriodicWorkPolicy.REPLACE, request);
} else {
WorkManager.getInstance().cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID);
}
}
public static void openLink(Context context, Uri link) {
Intent intent = new Intent(Intent.ACTION_VIEW, link);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
} else {
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT);
}
}
}

View File

@@ -0,0 +1,123 @@
package com.topjohnwu.magisk.utils
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.net.Uri
import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.work.*
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.OldModule
import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
import java.util.*
import java.util.concurrent.TimeUnit
object Utils {
val isCanary: Boolean
get() = BuildConfig.VERSION_NAME.contains("-")
fun toast(msg: CharSequence, duration: Int) {
UiThreadHandler.run { Toast.makeText(get(), msg, duration).show() }
}
fun toast(resId: Int, duration: Int) {
UiThreadHandler.run { Toast.makeText(get(), resId, duration).show() }
}
fun dlString(url: String): String {
val s = Networking.get(url).execForString().result
return s ?: ""
}
fun getPrefsInt(prefs: SharedPreferences, key: String, def: Int = 0): Int {
return prefs.getString(key, def.toString())!!.toInt()
}
fun dpInPx(dp: Int): Int {
val scale = get<Resources>().displayMetrics.density
return (dp * scale + 0.5).toInt()
}
fun fmt(fmt: String, vararg args: Any): String {
return String.format(Locale.US, fmt, *args)
}
fun getAppLabel(info: ApplicationInfo, pm: PackageManager): String {
try {
if (info.labelRes > 0) {
val res = pm.getResourcesForApplication(info)
val config = Configuration()
config.setLocale(LocaleManager.locale)
res.updateConfiguration(config, res.displayMetrics)
return res.getString(info.labelRes)
}
} catch (ignored: Exception) {
}
return info.loadLabel(pm).toString()
}
fun getLegalFilename(filename: CharSequence): String {
return filename.toString().replace(" ", "_").replace("'", "").replace("\"", "")
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
.replace("#", "").replace("@", "").replace("\\", "_")
}
@WorkerThread
fun loadModulesLeanback(): Map<String, OldModule> {
val moduleMap = ValueSortedMap<String, OldModule>()
val path = SuFile(Const.MAGISK_PATH)
val modules = path.listFiles { _, name -> name != "lost+found" && name != ".core" }
for (file in modules!!) {
if (file.isFile) continue
val module = OldModule(Const.MAGISK_PATH + "/" + file.name)
moduleMap[module.id] = module
}
return moduleMap
}
fun showSuperUser(): Boolean {
return Shell.rootAccess() && (Const.USER_ID == 0
|| Config.suMultiuserMode != Config.Value.MULTIUSER_MODE_OWNER_MANAGED)
}
fun scheduleUpdateCheck() {
if (Config.checkUpdate) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresDeviceIdle(true)
.build()
val request = PeriodicWorkRequest
.Builder(ClassMap[UpdateCheckService::class.java], 12, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance().enqueueUniquePeriodicWork(
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance().cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
}
}
fun openLink(context: Context, link: Uri) {
val intent = Intent(Intent.ACTION_VIEW, link)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
toast(R.string.open_link_failed_toast, Toast.LENGTH_SHORT)
}
}
}

View File

@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.utils
import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
@@ -76,4 +77,8 @@ fun <T1> ObservableList<T1>.copyNewInputInto(
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
target.addAll(addedValues)
}
})
})
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value)
}

View File

@@ -1,12 +1,6 @@
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
@@ -19,36 +13,3 @@ fun ResponseBody.writeToFile(context: Context, fileName: String): 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)
}

View File

@@ -1,18 +1,13 @@
package com.topjohnwu.magisk.utils
import com.topjohnwu.magisk.Info
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import java.io.File
fun reboot(recovery: Boolean = false): Shell.Result {
val command = StringBuilder("/system/bin/reboot")
.appendIf(recovery) {
append(" recovery")
}
.toString()
return Shell.su(command).exec()
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
}
fun File.suOutputStream() = SuFileOutputStream(this)

View File

@@ -1,66 +0,0 @@
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;
import com.topjohnwu.net.ResponseListener;
import java.io.InputStream;
import java.util.Scanner;
import androidx.appcompat.app.AlertDialog;
import ru.noties.markwon.Markwon;
import ru.noties.markwon.html.HtmlPlugin;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.svg.SvgPlugin;
public class MarkDownWindow {
public static void show(Context activity, String title, String url) {
Networking.get(url).getAsString(new Listener(activity, title));
}
public static void show(Context activity, String title, InputStream is) {
try (Scanner s = new Scanner(is, "UTF-8")) {
s.useDelimiter("\\A");
new Listener(activity, title).onResponse(s.next());
}
}
static class Listener implements ResponseListener<String> {
Context activity;
String title;
Listener(Context a, String t) {
activity = a;
title = t;
}
@Override
public void onResponse(String md) {
Markwon markwon = Markwon.builder(activity)
.usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create(activity))
.usePlugin(SvgPlugin.create(activity.getResources()))
.build();
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
alert.setTitle(title);
View mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null);
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();
}
}
}

View File

@@ -0,0 +1,54 @@
package com.topjohnwu.magisk.view
import android.content.Context
import android.view.LayoutInflater
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.R
import com.topjohnwu.net.Networking
import com.topjohnwu.net.ResponseListener
import ru.noties.markwon.Markwon
import ru.noties.markwon.html.HtmlPlugin
import ru.noties.markwon.image.ImagesPlugin
import ru.noties.markwon.image.svg.SvgPlugin
import java.io.InputStream
import java.util.*
object MarkDownWindow {
fun show(activity: Context, title: String?, url: String) {
Networking.get(url).getAsString(Listener(activity, title))
}
fun show(activity: Context, title: String?, input: InputStream) {
Scanner(input, "UTF-8").use {
it.useDelimiter("\\A")
Listener(activity, title).onResponse(it.next())
}
}
internal class Listener(var activity: Context, var title: String?) : ResponseListener<String> {
override fun onResponse(md: String) {
val markwon = Markwon.builder(activity)
.usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create(activity))
.usePlugin(SvgPlugin.create(activity.resources))
.build()
val alert = AlertDialog.Builder(activity)
alert.setTitle(title)
val mv = LayoutInflater.from(activity).inflate(R.layout.markdown_window, null)
val tv = mv.findViewById<TextView>(R.id.md_txt)
try {
markwon.setMarkdown(tv, md)
} catch (e: ExceptionInInitializerError) {
//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, _ -> dialog.dismiss() }
alert.show()
}
}
}

View File

@@ -7,19 +7,19 @@ import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.TaskStackBuilder;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
import com.topjohnwu.magisk.ui.SplashActivity;
import com.topjohnwu.magisk.utils.Utils;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.TaskStackBuilder;
public class Notifications {
public static NotificationManagerCompat mgr = NotificationManagerCompat.from(App.self);
@@ -62,12 +62,12 @@ public class Notifications {
public static void managerUpdate() {
App app = App.self;
String name = Utils.fmt("MagiskManager v%s(%d)",
Config.remoteManagerVersionString, Config.remoteManagerVersionCode);
String name = Utils.INSTANCE.fmt("MagiskManager v%s(%d)",
Info.remote.getApp().getVersion(), Info.remote.getApp().getVersionCode());
Intent intent = new Intent(app, ClassMap.get(GeneralReceiver.class));
intent.setAction(Const.Key.BROADCAST_MANAGER_UPDATE);
intent.putExtra(Const.Key.INTENT_SET_LINK, Config.managerLink);
intent.putExtra(Const.Key.INTENT_SET_LINK, Info.remote.getApp().getLink());
intent.putExtra(Const.Key.INTENT_SET_NAME, name);
PendingIntent pendingIntent = PendingIntent.getBroadcast(app,
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);

View File

@@ -1,85 +0,0 @@
package com.topjohnwu.magisk.view;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
import android.widget.Toast;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.DownloadProgressListener;
import androidx.core.app.NotificationCompat;
public class ProgressNotification implements DownloadProgressListener {
private NotificationCompat.Builder builder;
private Notification notification;
private long prevTime;
public ProgressNotification(String title) {
builder = Notifications.progress(title);
prevTime = System.currentTimeMillis();
update();
Utils.toast(App.self.getString(R.string.downloading_toast, title), Toast.LENGTH_SHORT);
}
@Override
public void onProgress(long bytesDownloaded, long totalBytes) {
long cur = System.currentTimeMillis();
if (cur - prevTime >= 1000) {
prevTime = cur;
int progress = (int) (bytesDownloaded * 100 / totalBytes);
builder.setProgress(100, progress, false);
builder.setContentText(progress + "%");
update();
}
}
public NotificationCompat.Builder getNotificationBuilder() {
return builder;
}
public Notification getNotification() {
return notification;
}
public void update() {
notification = builder.build();
Notifications.mgr.notify(hashCode(), notification);
}
private void lastUpdate() {
notification = builder.build();
Notifications.mgr.cancel(hashCode());
Notifications.mgr.notify(notification.hashCode(), notification);
}
public void dlDone() {
dlDone(PendingIntent.getActivity(App.self, hashCode(),
new Intent(), PendingIntent.FLAG_UPDATE_CURRENT));
}
public void dlDone(PendingIntent intent) {
builder.setProgress(0, 0, false)
.setContentText(App.self.getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(intent)
.setOngoing(false)
.setAutoCancel(true);
lastUpdate();
}
public void dlFail() {
builder.setProgress(0, 0, false)
.setContentText(App.self.getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false);
lastUpdate();
}
public void dismiss() {
Notifications.mgr.cancel(hashCode());
}
}

View File

@@ -0,0 +1,73 @@
package com.topjohnwu.magisk.view
import android.app.Notification
import android.app.PendingIntent
import android.content.Intent
import android.widget.Toast
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.net.DownloadProgressListener
class ProgressNotification(title: String) : DownloadProgressListener {
val notificationBuilder: NotificationCompat.Builder = Notifications.progress(title)
lateinit var notification: Notification
private set
private var prevTime: Long = 0
init {
prevTime = System.currentTimeMillis()
update()
Utils.toast(App.self.getString(R.string.downloading_toast, title), Toast.LENGTH_SHORT)
}
override fun onProgress(bytesDownloaded: Long, totalBytes: Long) {
val cur = System.currentTimeMillis()
if (cur - prevTime >= 1000) {
prevTime = cur
val progress = (bytesDownloaded * 100 / totalBytes).toInt()
notificationBuilder.setProgress(100, progress, false)
notificationBuilder.setContentText("$progress%")
update()
}
}
fun update() {
notification = notificationBuilder.build()
Notifications.mgr.notify(hashCode(), notification)
}
private fun lastUpdate() {
Notifications.mgr.cancel(hashCode())
notification = notificationBuilder.build().apply {
Notifications.mgr.notify(hashCode(), this)
}
}
fun dlDone(intent: PendingIntent = PendingIntent.getActivity(App.self, hashCode(),
Intent(), PendingIntent.FLAG_UPDATE_CURRENT)) {
notificationBuilder.setProgress(0, 0, false)
.setContentText(App.self.getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentIntent(intent)
.setOngoing(false)
.setAutoCancel(true)
lastUpdate()
}
fun dlFail() {
notificationBuilder.setProgress(0, 0, false)
.setContentText(App.self.getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
lastUpdate()
}
fun dismiss() {
Notifications.mgr.cancel(hashCode())
}
}

View File

@@ -1,79 +0,0 @@
package com.topjohnwu.magisk.view;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ui.SplashActivity;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import java.util.ArrayList;
import androidx.annotation.RequiresApi;
public class Shortcuts {
public static void setup(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutManager manager = context.getSystemService(ShortcutManager.class);
manager.setDynamicShortcuts(getShortCuts(context));
}
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private static ArrayList<ShortcutInfo> getShortCuts(Context context) {
ArrayList<ShortcutInfo> shortCuts = new ArrayList<>();
boolean root = Shell.rootAccess();
if (Utils.showSuperUser()) {
shortCuts.add(new ShortcutInfo.Builder(context, "superuser")
.setShortLabel(context.getString(R.string.superuser))
.setIntent(new Intent(context, ClassMap.get(SplashActivity.class))
.putExtra(Const.Key.OPEN_SECTION, "superuser")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_superuser))
.setRank(0)
.build());
}
if (root && (boolean) Config.get(Config.Key.MAGISKHIDE)) {
shortCuts.add(new ShortcutInfo.Builder(context, "magiskhide")
.setShortLabel(context.getString(R.string.magiskhide))
.setIntent(new Intent(context, ClassMap.get(SplashActivity.class))
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_magiskhide))
.setRank(1)
.build());
}
if (!(boolean) Config.get(Config.Key.COREONLY) && root && Config.magiskVersionCode >= 0) {
shortCuts.add(new ShortcutInfo.Builder(context, "modules")
.setShortLabel(context.getString(R.string.modules))
.setIntent(new Intent(context, ClassMap.get(SplashActivity.class))
.putExtra(Const.Key.OPEN_SECTION, "modules")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_extension))
.setRank(3)
.build());
shortCuts.add(new ShortcutInfo.Builder(context, "downloads")
.setShortLabel(context.getString(R.string.downloads))
.setIntent(new Intent(context, ClassMap.get(SplashActivity.class))
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_cloud_download))
.setRank(2)
.build());
}
return shortCuts;
}
}

View File

@@ -0,0 +1,72 @@
package com.topjohnwu.magisk.view
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
object Shortcuts {
fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
val manager = context.getSystemService(ShortcutManager::class.java)
manager?.dynamicShortcuts = getShortCuts(context)
}
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private fun getShortCuts(context: Context): List<ShortcutInfo> {
val shortCuts = mutableListOf<ShortcutInfo>()
val root = Shell.rootAccess()
if (Utils.showSuperUser()) {
shortCuts.add(ShortcutInfo.Builder(context, "superuser")
.setShortLabel(context.getString(R.string.superuser))
.setIntent(Intent(context, ClassMap[SplashActivity::class.java])
.putExtra(Const.Key.OPEN_SECTION, "superuser")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_superuser))
.setRank(0)
.build())
}
if (root && Config.magiskHide) {
shortCuts.add(ShortcutInfo.Builder(context, "magiskhide")
.setShortLabel(context.getString(R.string.magiskhide))
.setIntent(Intent(context, ClassMap[SplashActivity::class.java])
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_magiskhide))
.setRank(1)
.build())
}
if (!Config.coreOnly && root && Info.magiskVersionCode >= 0) {
shortCuts.add(ShortcutInfo.Builder(context, "modules")
.setShortLabel(context.getString(R.string.modules))
.setIntent(Intent(context, ClassMap[SplashActivity::class.java])
.putExtra(Const.Key.OPEN_SECTION, "modules")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_extension))
.setRank(3)
.build())
shortCuts.add(ShortcutInfo.Builder(context, "downloads")
.setShortLabel(context.getString(R.string.downloads))
.setIntent(Intent(context, ClassMap[SplashActivity::class.java])
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_cloud_download))
.setRank(2)
.build())
}
return shortCuts
}
}

View File

@@ -1,48 +0,0 @@
package com.topjohnwu.magisk.view;
import android.app.Activity;
import android.net.Uri;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.StringRes;
import com.google.android.material.snackbar.Snackbar;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.XAndroidKt;
public class SnackbarMaker {
public static Snackbar make(Activity activity, CharSequence text, int duration) {
View view = activity.findViewById(android.R.id.content);
return make(view, text, duration);
}
public static Snackbar make(Activity activity, @StringRes int resId, int duration) {
return make(activity, activity.getString(resId), duration);
}
public static Snackbar make(View view, CharSequence text, int duration) {
Snackbar snack = Snackbar.make(view, text, duration);
setup(snack);
return snack;
}
public static Snackbar make(View view, @StringRes int resId, int duration) {
Snackbar snack = Snackbar.make(view, resId, duration);
setup(snack);
return snack;
}
private static void setup(Snackbar snack) {
TextView text = snack.getView().findViewById(com.google.android.material.R.id.snackbar_text);
text.setMaxLines(Integer.MAX_VALUE);
}
public static void showUri(Activity activity, Uri uri) {
make(activity, activity.getString(R.string.internal_storage,
"/Download/" + XAndroidKt.getFileName(uri)),
Snackbar.LENGTH_LONG)
.setAction(R.string.ok, (v)->{}).show();
}
}

View File

@@ -0,0 +1,46 @@
package com.topjohnwu.magisk.view
import android.app.Activity
import android.net.Uri
import android.view.View
import android.widget.TextView
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.utils.fileName
object SnackbarMaker {
fun make(activity: Activity, text: CharSequence, duration: Int): Snackbar {
val view = activity.findViewById<View>(android.R.id.content)
return make(view, text, duration)
}
fun make(activity: Activity, @StringRes resId: Int, duration: Int): Snackbar {
return make(activity, activity.getString(resId), duration)
}
fun make(view: View, text: CharSequence, duration: Int): Snackbar {
val snack = Snackbar.make(view, text, duration)
setup(snack)
return snack
}
fun make(view: View, @StringRes resId: Int, duration: Int): Snackbar {
val snack = Snackbar.make(view, resId, duration)
setup(snack)
return snack
}
private fun setup(snack: Snackbar) {
val text = snack.view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
text.maxLines = Integer.MAX_VALUE
}
fun showUri(activity: Activity, uri: Uri) {
make(activity, activity.getString(R.string.internal_storage,
"/Download/" + uri.fileName),
Snackbar.LENGTH_LONG)
.setAction(R.string.ok) { }.show()
}
}

View File

@@ -1,137 +0,0 @@
package com.topjohnwu.magisk.view.dialogs;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.appcompat.app.AlertDialog;
import com.topjohnwu.magisk.databinding.AlertDialogBinding;
public class CustomAlertDialog extends AlertDialog.Builder {
private DialogInterface.OnClickListener positiveListener;
private DialogInterface.OnClickListener negativeListener;
private DialogInterface.OnClickListener neutralListener;
protected AlertDialog dialog;
protected AlertDialogBinding binding;
{
binding = AlertDialogBinding.inflate(LayoutInflater.from(getContext()));
super.setView(binding.getRoot());
binding.message.setVisibility(View.GONE);
binding.negative.setVisibility(View.GONE);
binding.positive.setVisibility(View.GONE);
binding.neutral.setVisibility(View.GONE);
binding.buttonPanel.setVisibility(View.GONE);
}
public CustomAlertDialog(@NonNull Context context) {
super(context);
}
public CustomAlertDialog(@NonNull Context context, @StyleRes int themeResId) {
super(context, themeResId);
}
@Override
public CustomAlertDialog setView(int layoutResId) { return this; }
@Override
public CustomAlertDialog setView(View view) { return this; }
@Override
public CustomAlertDialog setMessage(@Nullable CharSequence message) {
binding.message.setVisibility(View.VISIBLE);
binding.message.setText(message);
return this;
}
@Override
public CustomAlertDialog setMessage(@StringRes int messageId) {
return setMessage(getContext().getString(messageId));
}
@Override
public CustomAlertDialog setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) {
binding.buttonPanel.setVisibility(View.VISIBLE);
binding.positive.setVisibility(View.VISIBLE);
binding.positive.setText(text);
positiveListener = listener;
binding.positive.setOnClickListener(v -> {
if (positiveListener != null) {
positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
}
dialog.dismiss();
});
return this;
}
@Override
public CustomAlertDialog setPositiveButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setPositiveButton(getContext().getString(textId), listener);
}
@Override
public CustomAlertDialog setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) {
binding.buttonPanel.setVisibility(View.VISIBLE);
binding.negative.setVisibility(View.VISIBLE);
binding.negative.setText(text);
negativeListener = listener;
binding.negative.setOnClickListener(v -> {
if (negativeListener != null) {
negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
}
dialog.dismiss();
});
return this;
}
@Override
public CustomAlertDialog setNegativeButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setNegativeButton(getContext().getString(textId), listener);
}
@Override
public CustomAlertDialog setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) {
binding.buttonPanel.setVisibility(View.VISIBLE);
binding.neutral.setVisibility(View.VISIBLE);
binding.neutral.setText(text);
neutralListener = listener;
binding.neutral.setOnClickListener(v -> {
if (neutralListener != null) {
neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
}
dialog.dismiss();
});
return this;
}
@Override
public CustomAlertDialog setNeutralButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setNeutralButton(getContext().getString(textId), listener);
}
@Override
public AlertDialog create() {
dialog = super.create();
return dialog;
}
@Override
public AlertDialog show() {
create();
dialog.show();
return dialog;
}
public void dismiss() {
dialog.dismiss();
}
}

View File

@@ -0,0 +1,113 @@
package com.topjohnwu.magisk.view.dialogs
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import android.view.View
import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import com.topjohnwu.magisk.databinding.AlertDialogBinding
open class CustomAlertDialog : AlertDialog.Builder {
private var positiveListener: DialogInterface.OnClickListener? = null
private var negativeListener: DialogInterface.OnClickListener? = null
private var neutralListener: DialogInterface.OnClickListener? = null
protected var dialog: AlertDialog? = null
protected var binding: AlertDialogBinding =
AlertDialogBinding.inflate(LayoutInflater.from(context))
init {
super.setView(binding.root)
binding.message.visibility = View.GONE
binding.negative.visibility = View.GONE
binding.positive.visibility = View.GONE
binding.neutral.visibility = View.GONE
binding.buttonPanel.visibility = View.GONE
}
constructor(context: Context) : super(context)
constructor(context: Context, @StyleRes themeResId: Int) : super(context, themeResId)
override fun setView(layoutResId: Int): CustomAlertDialog {
return this
}
override fun setView(view: View): CustomAlertDialog {
return this
}
override fun setMessage(message: CharSequence?): CustomAlertDialog {
binding.message.visibility = View.VISIBLE
binding.message.text = message
return this
}
override fun setMessage(@StringRes messageId: Int): CustomAlertDialog {
return setMessage(context.getString(messageId))
}
override fun setPositiveButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.positive.visibility = View.VISIBLE
binding.positive.text = text
positiveListener = listener
binding.positive.setOnClickListener {
positiveListener?.onClick(dialog, DialogInterface.BUTTON_POSITIVE)
dialog?.dismiss()
}
return this
}
override fun setPositiveButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setPositiveButton(context.getString(textId), listener)
}
override fun setNegativeButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.negative.visibility = View.VISIBLE
binding.negative.text = text
negativeListener = listener
binding.negative.setOnClickListener {
negativeListener?.onClick(dialog, DialogInterface.BUTTON_NEGATIVE)
dialog?.dismiss()
}
return this
}
override fun setNegativeButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setNegativeButton(context.getString(textId), listener)
}
override fun setNeutralButton(text: CharSequence, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
binding.buttonPanel.visibility = View.VISIBLE
binding.neutral.visibility = View.VISIBLE
binding.neutral.text = text
neutralListener = listener
binding.neutral.setOnClickListener {
neutralListener?.onClick(dialog, DialogInterface.BUTTON_NEUTRAL)
dialog?.dismiss()
}
return this
}
override fun setNeutralButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener?): CustomAlertDialog {
return setNeutralButton(context.getString(textId), listener)
}
override fun create(): AlertDialog {
return super.create().apply { dialog = this }
}
override fun show(): AlertDialog {
return create().apply { show() }
}
fun dismiss() {
dialog?.dismiss()
}
}

View File

@@ -1,47 +0,0 @@
package com.topjohnwu.magisk.view.dialogs;
import android.app.Activity;
import android.app.ProgressDialog;
import android.widget.Toast;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.tasks.MagiskInstaller;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.io.SuFile;
import androidx.annotation.NonNull;
public class EnvFixDialog extends CustomAlertDialog {
public EnvFixDialog(@NonNull Activity activity) {
super(activity);
setTitle(R.string.env_fix_title);
setMessage(R.string.env_fix_msg);
setCancelable(true);
setPositiveButton(R.string.yes, (d, i) -> {
ProgressDialog pd = ProgressDialog.show(activity,
activity.getString(R.string.setup_title),
activity.getString(R.string.setup_msg));
new MagiskInstaller() {
@Override
protected boolean operations() {
installDir = new SuFile("/data/adb/magisk");
Shell.su("rm -rf /data/adb/magisk/*").exec();
return extractZip() && Shell.su("fix_env").exec().isSuccess();
}
@Override
protected void onResult(boolean success) {
pd.dismiss();
Utils.toast(success ? R.string.reboot_delay_toast : R.string.setup_fail, Toast.LENGTH_LONG);
if (success)
UiThreadHandler.handler.postDelayed(RootUtils::reboot, 5000);
}
}.exec();
});
setNegativeButton(R.string.no_thanks, null);
}
}

View File

@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.view.dialogs
import android.app.Activity
import android.app.ProgressDialog
import android.widget.Toast
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.io.SuFile
class EnvFixDialog(activity: Activity) : CustomAlertDialog(activity) {
init {
setTitle(R.string.env_fix_title)
setMessage(R.string.env_fix_msg)
setCancelable(true)
setPositiveButton(R.string.yes) { _, _ ->
val pd = ProgressDialog.show(activity,
activity.getString(R.string.setup_title),
activity.getString(R.string.setup_msg))
object : MagiskInstaller() {
override fun operations(): Boolean {
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
override fun onResult(success: Boolean) {
pd.dismiss()
Utils.toast(if (success) R.string.reboot_delay_toast else R.string.setup_fail, Toast.LENGTH_LONG)
if (success)
UiThreadHandler.handler.postDelayed({ reboot() }, 5000)
}
}.exec()
}
setNegativeButton(R.string.no_thanks, null)
}
}

View File

@@ -1,106 +0,0 @@
package com.topjohnwu.magisk.view.dialogs;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.view.Gravity;
import android.widget.Toast;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
@TargetApi(Build.VERSION_CODES.M)
public class FingerprintAuthDialog extends CustomAlertDialog {
private final Runnable callback;
@Nullable
private Runnable failureCallback;
private DialogFingerprintHelper helper;
public FingerprintAuthDialog(@NonNull Activity activity, @NonNull Runnable onSuccess) {
super(activity);
callback = onSuccess;
Drawable fingerprint = activity.getResources().getDrawable(R.drawable.ic_fingerprint);
fingerprint.setBounds(0, 0, Utils.dpInPx(50), Utils.dpInPx(50));
Resources.Theme theme = activity.getTheme();
TypedArray ta = theme.obtainStyledAttributes(new int[] {R.attr.imageColorTint});
fingerprint.setTint(ta.getColor(0, Color.GRAY));
ta.recycle();
binding.message.setCompoundDrawables(null, null, null, fingerprint);
binding.message.setCompoundDrawablePadding(Utils.dpInPx(20));
binding.message.setGravity(Gravity.CENTER);
setMessage(R.string.auth_fingerprint);
setNegativeButton(R.string.close, (d, w) -> {
helper.cancel();
if (failureCallback != null) {
failureCallback.run();
}
});
setOnCancelListener(d -> {
helper.cancel();
if (failureCallback != null) {
failureCallback.run();
}
});
try {
helper = new DialogFingerprintHelper();
} catch (Exception ignored) {}
}
public FingerprintAuthDialog(@NonNull Activity activity, @NonNull Runnable onSuccess, @NonNull Runnable onFailure) {
this(activity, onSuccess);
failureCallback = onFailure;
}
@Override
public AlertDialog show() {
create();
if (helper == null) {
dialog.dismiss();
Utils.toast(R.string.auth_fail, Toast.LENGTH_SHORT);
} else {
helper.authenticate();
dialog.show();
}
return dialog;
}
class DialogFingerprintHelper extends FingerprintHelper {
DialogFingerprintHelper() throws Exception {}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
binding.message.setTextColor(Color.RED);
binding.message.setText(errString);
}
@Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
binding.message.setTextColor(Color.RED);
binding.message.setText(helpString);
}
@Override
public void onAuthenticationFailed() {
binding.message.setTextColor(Color.RED);
binding.message.setText(R.string.auth_fail);
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
dismiss();
callback.run();
}
}
}

View File

@@ -0,0 +1,88 @@
package com.topjohnwu.magisk.view.dialogs
import android.annotation.TargetApi
import android.app.Activity
import android.graphics.Color
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.view.Gravity
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.Utils
@TargetApi(Build.VERSION_CODES.M)
class FingerprintAuthDialog(activity: Activity, private val callback: () -> Unit)
: CustomAlertDialog(activity) {
private var failureCallback: (() -> Unit)? = null
private var helper: DialogFingerprintHelper? = null
init {
val fingerprint = ContextCompat.getDrawable(activity, R.drawable.ic_fingerprint)
fingerprint?.setBounds(0, 0, Utils.dpInPx(50), Utils.dpInPx(50))
val theme = activity.theme
val ta = theme.obtainStyledAttributes(intArrayOf(R.attr.imageColorTint))
fingerprint?.setTint(ta.getColor(0, Color.GRAY))
ta.recycle()
binding.message.setCompoundDrawables(null, null, null, fingerprint)
binding.message.compoundDrawablePadding = Utils.dpInPx(20)
binding.message.gravity = Gravity.CENTER
setMessage(R.string.auth_fingerprint)
setNegativeButton(R.string.close) { _, _ ->
helper?.cancel()
failureCallback?.invoke()
}
setOnCancelListener {
helper?.cancel()
failureCallback?.invoke()
}
runCatching {
helper = DialogFingerprintHelper()
}
}
constructor(activity: Activity, onSuccess: () -> Unit, onFailure: () -> Unit)
: this(activity, onSuccess) {
failureCallback = onFailure
}
override fun show(): AlertDialog {
return create().apply {
if (helper == null) {
dismiss()
Utils.toast(R.string.auth_fail, Toast.LENGTH_SHORT)
} else {
helper?.authenticate()
show()
}
}
}
internal inner class DialogFingerprintHelper @Throws(Exception::class)
constructor() : FingerprintHelper() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
binding.message.setTextColor(Color.RED)
binding.message.text = errString
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
binding.message.setTextColor(Color.RED)
binding.message.text = helpString
}
override fun onAuthenticationFailed() {
binding.message.setTextColor(Color.RED)
binding.message.setText(R.string.auth_fail)
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
dismiss()
callback()
}
}
}

Some files were not shown because too many files have changed in this diff Show More