Remove app-core module

Less confusion
This commit is contained in:
topjohnwu
2019-03-08 10:19:22 -05:00
parent cf65169c99
commit de5c902fdb
37 changed files with 17 additions and 57 deletions

View File

@@ -24,9 +24,10 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':net')
implementation project(':shared')
implementation project(':app-core')
implementation project(':signing')
implementation 'ru.noties:markwon:2.0.1'
implementation 'com.caverock:androidsvg-aar:1.3'
implementation 'org.kamranzafar:jtar:2.3'
implementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.0'
def androidXVersion = "1.0.0"
@@ -40,6 +41,10 @@ dependencies {
implementation 'androidx.room:room-runtime:2.0.0'
implementation 'androidx.transition:transition:1.0.1'
def libsuVersion = '2.3.1'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
def butterKnifeVersion = '10.1.0'
implementation "com.jakewharton:butterknife-runtime:${butterKnifeVersion}"
annotationProcessor "com.jakewharton:butterknife-compiler:${butterKnifeVersion}"

View File

@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<application
android:allowBackup="true"

View File

@@ -0,0 +1,60 @@
package com.topjohnwu.magisk;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
import com.topjohnwu.magisk.database.MagiskDB;
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.RootUtils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import java.util.concurrent.ThreadPoolExecutor;
public class App extends Application {
public static App self;
public static ThreadPoolExecutor THREAD_POOL;
// Global resources
public SharedPreferences prefs;
public MagiskDB mDB;
public RepoDatabaseHelper repoDB;
static {
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER | Shell.FLAG_USE_MAGISK_BUSYBOX);
Shell.Config.verboseLogging(BuildConfig.DEBUG);
Shell.Config.addInitializers(RootUtils.class);
Shell.Config.setTimeout(2);
THREAD_POOL = (ThreadPoolExecutor) AsyncTask.THREAD_POOL_EXECUTOR;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
self = this;
Context de = this;
if (Build.VERSION.SDK_INT >= 24) {
de = createDeviceProtectedStorageContext();
de.moveSharedPreferencesFrom(this, PreferenceManager.getDefaultSharedPreferencesName(base));
}
prefs = PreferenceManager.getDefaultSharedPreferences(de);
mDB = new MagiskDB(this);
Networking.init(this);
LocaleManager.setLocale(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LocaleManager.setLocale(this);
}
}

View File

@@ -0,0 +1,374 @@
package com.topjohnwu.magisk;
import android.content.SharedPreferences;
import android.os.Bundle;
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;
public class Config {
// Current status
public static String magiskVersionString;
public static int magiskVersionCode = -1;
private static boolean magiskHide;
// Update Info
public static String remoteMagiskVersionString;
public static int remoteMagiskVersionCode = -1;
public static String magiskLink;
public static String magiskNoteLink;
public static String magiskMD5;
public static String remoteManagerVersionString;
public static int remoteManagerVersionCode = -1;
public static String managerLink;
public static String managerNoteLink;
public static String uninstallerLink;
// Install flags
public static boolean keepVerity = false;
public static boolean keepEnc = false;
public static boolean recovery = false;
public static int suLogTimeout = 14;
public static class Key {
// su configs
public static final String ROOT_ACCESS = "root_access";
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
public static final String SU_MNT_NS = "mnt_ns";
public static final String SU_MANAGER = "requester";
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
public static final String SU_AUTO_RESPONSE = "su_auto_response";
public static final String SU_NOTIFICATION = "su_notification";
public static final String SU_REAUTH = "su_reauth";
public static final String SU_FINGERPRINT = "su_fingerprint";
// prefs
public static final String CHECK_UPDATES = "check_update";
public static final String UPDATE_CHANNEL = "update_channel";
public static final String CUSTOM_CHANNEL = "custom_channel";
public static final String BOOT_FORMAT = "boot_format";
public static final String UPDATE_SERVICE_VER = "update_service_version";
public static final String MAGISKHIDE = "magiskhide";
public static final String COREONLY = "disable";
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";
}
public static class Value {
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 Bundle defs = new Bundle();
static {
/* Set default configurations */
// prefs int
defs.putInt(Key.REPO_ORDER, Value.ORDER_DATE);
// prefs string int
defs.putInt(Key.SU_REQUEST_TIMEOUT, 10);
defs.putInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
defs.putInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
defs.putInt(Key.UPDATE_CHANNEL, Value.STABLE_CHANNEL);
// prefs bool
defs.putBoolean(Key.CHECK_UPDATES, true);
// defs.putBoolean(Key.DARK_THEME, false);
// defs.putBoolean(Key.SU_REAUTH, false);
// defs.putBoolean(Key.MAGISKHIDE, false);
// defs.putBoolean(Key.COREONLY, false);
// defs.putBoolean(Key.SHOW_SYSTEM_APP, false);
// prefs string
defs.putString(Key.CUSTOM_CHANNEL, "");
defs.putString(Key.BOOT_FORMAT, ".img");
defs.putString(Key.LOCALE, "");
// defs.putString(Key.ETAG_KEY, null);
// db int
defs.putInt(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
defs.putInt(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
defs.putInt(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
// db bool
// defs.putBoolean(Key.SU_FINGERPRINT, false);
// db strings
// defs.putString(Key.SU_MANAGER, null);
}
public static void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
} catch (NumberFormatException ignored) {}
}
public static void export() {
// Flush prefs to disk
App app = App.self;
app.prefs.edit().commit();
File xml = new File(app.getFilesDir().getParent() + "/shared_prefs",
app.getPackageName() + "_preferences.xml");
Shell.su(Utils.fmt("cat %s > /data/user/0/%s", xml, Const.MANAGER_CONFIGS)).exec();
}
public static void initialize() {
SharedPreferences pref = App.self.prefs;
SharedPreferences.Editor editor = pref.edit();
SuFile config = new SuFile("/data/user/0/" + 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 to defaults if not set
setDefs(pref, editor,
Key.SU_REQUEST_TIMEOUT, Key.SU_AUTO_RESPONSE, Key.ROOT_ACCESS,
Key.SU_MNT_NS, Key.SU_NOTIFICATION, Key.DARK_THEME,
Key.CHECK_UPDATES, Key.UPDATE_CHANNEL, Key.REPO_ORDER);
// These settings are from actual device state
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
.apply();
}
private static final int PREF_INT = 0;
private static final int PREF_STR_INT = 1;
private static final int PREF_BOOL = 2;
private static final int PREF_STR = 3;
private static final int DB_INT = 4;
private static final int DB_BOOL = 5;
private static final int DB_STR = 6;
private static int getConfigType(String key) {
switch (key) {
case Key.REPO_ORDER:
return PREF_INT;
case Key.SU_REQUEST_TIMEOUT:
case Key.SU_AUTO_RESPONSE:
case Key.SU_NOTIFICATION:
case Key.UPDATE_CHANNEL:
return PREF_STR_INT;
case Key.DARK_THEME:
case Key.SU_REAUTH:
case Key.CHECK_UPDATES:
case Key.MAGISKHIDE:
case Key.COREONLY:
case Key.SHOW_SYSTEM_APP:
return PREF_BOOL;
case Key.CUSTOM_CHANNEL:
case Key.BOOT_FORMAT:
case Key.LOCALE:
case Key.ETAG_KEY:
return PREF_STR;
case Key.ROOT_ACCESS:
case Key.SU_MNT_NS:
case Key.SU_MULTIUSER_MODE:
return DB_INT;
case Key.SU_FINGERPRINT:
return DB_BOOL;
case Key.SU_MANAGER:
return DB_STR;
default:
throw new IllegalArgumentException();
}
}
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
App app = App.self;
switch (getConfigType(key)) {
case PREF_INT:
return (T) (Integer) app.prefs.getInt(key, defs.getInt(key));
case PREF_STR_INT:
return (T) (Integer) Utils.getPrefsInt(app.prefs, key, defs.getInt(key));
case PREF_BOOL:
return (T) (Boolean) app.prefs.getBoolean(key, defs.getBoolean(key));
case PREF_STR:
return (T) app.prefs.getString(key, defs.getString(key));
case DB_INT:
return (T) (Integer) app.mDB.getSettings(key, defs.getInt(key));
case DB_BOOL:
return (T) (Boolean) (app.mDB.getSettings(key, defs.getBoolean(key) ? 1 : 0) != 0);
case DB_STR:
return (T) app.mDB.getStrings(key, defs.getString(key));
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return (T) new Object();
}
public static void set(String key, Object val) {
App app = App.self;
switch (getConfigType(key)) {
case PREF_INT:
app.prefs.edit().putInt(key, (int) val).apply();
break;
case PREF_STR_INT:
app.prefs.edit().putString(key, String.valueOf(val)).apply();
break;
case PREF_BOOL:
app.prefs.edit().putBoolean(key, (boolean) val).apply();
break;
case PREF_STR:
app.prefs.edit().putString(key, (String) val).apply();
break;
case DB_INT:
app.mDB.setSettings(key, (int) val);
break;
case DB_BOOL:
app.mDB.setSettings(key, (boolean) val ? 1 : 0);
break;
case DB_STR:
app.mDB.setStrings(key, (String) val);
break;
}
}
public static void remove(String key) {
App app = App.self;
int def;
switch (getConfigType(key)) {
case PREF_INT:
case PREF_STR_INT:
case PREF_BOOL:
case PREF_STR:
app.prefs.edit().remove(key).apply();
break;
case DB_INT:
def = defs.getInt(key);
app.mDB.setSettings(key, def);
break;
case DB_BOOL:
def = defs.getBoolean(key) ? 1 : 0;
app.mDB.setSettings(key, def);
break;
case DB_STR:
app.mDB.setStrings(key, null);
break;
}
}
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor, String... keys) {
for (String key : keys) {
if (pref.contains(key))
continue;
switch (getConfigType(key)) {
case PREF_INT:
editor.putInt(key, defs.getInt(key));
break;
case DB_INT:
case PREF_STR_INT:
editor.putString(key, String.valueOf(defs.getInt(key)));
break;
case PREF_STR:
case DB_STR:
editor.putString(key, defs.getString(key));
break;
case PREF_BOOL:
case DB_BOOL:
editor.putBoolean(key, defs.getBoolean(key));
break;
}
}
}
}

View File

@@ -0,0 +1,104 @@
package com.topjohnwu.magisk;
import android.os.Environment;
import android.os.Process;
import java.io.File;
public class Const {
public static final String DEBUG_TAG = "MagiskManager";
// APK content
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
public static final String SU_KEYSTORE_KEY = "su_key";
// Paths
public static final String MAGISK_PATH = "/sbin/.magisk/img";
public static final File EXTERNAL_PATH;
public static File MAGISK_DISABLE_FILE;
static {
MAGISK_DISABLE_FILE = new File("xxx");
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
EXTERNAL_PATH.mkdirs();
}
public static final String BUSYBOX_PATH = "/sbin/.magisk/busybox";
public static final String TMP_FOLDER_PATH = "/dev/tmp";
public static final String MAGISK_LOG = "/cache/magisk.log";
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
// Versions
public static final int UPDATE_SERVICE_VER = 1;
public static final int MIN_MODULE_VER = 1500;
public static final int SNET_EXT_VER = 12;
public static final int USER_ID = Process.myUid() / 100000;
public static final class MAGISK_VER {
public static final int MIN_SUPPORT = 18000;
}
public static class ID {
public static final int UPDATE_SERVICE_ID = 1;
public static final int FETCH_ZIP = 2;
public static final int SELECT_BOOT = 3;
public static final int ONBOOT_SERVICE_ID = 6;
// notifications
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
public static final int DTBO_NOTIFICATION_ID = 7;
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
public static final String CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update";
}
public static class Url {
private static String getRaw(String where, String name) {
return String.format("https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s", where, name);
}
public static final String STABLE_URL = getRaw("master", "stable.json");
public static final String BETA_URL = getRaw("master", "beta.json");
public static final String CANARY_URL = getRaw("master", "canary_builds/release.json");
public static final String CANARY_DEBUG_URL = getRaw("master", "canary_builds/canary.json");
public static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d";
public static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
public static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
public static final String PAYPAL_URL = "https://www.paypal.me/topjohnwu";
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
public static final String SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk");
public static final String BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl");
}
public static class Key {
// others
public static final String LINK_KEY = "Link";
public static final String IF_NONE_MATCH = "If-None-Match";
// intents
public static final String FROM_SPLASH = "splash";
public static final String OPEN_SECTION = "section";
public static final String INTENT_SET_NAME = "filename";
public static final String INTENT_SET_LINK = "link";
public static final String FLASH_ACTION = "action";
public static final String FLASH_SET_BOOT = "boot";
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
public static final String BROADCAST_REBOOT = "reboot";
}
public static class Value {
public static final String FLASH_ZIP = "flash";
public static final String PATCH_BOOT = "patch";
public static final String FLASH_MAGISK = "magisk";
public static final String FLASH_INACTIVE_SLOT = "slot";
public static final String UNINSTALL = "uninstall";
}
}

View File

@@ -0,0 +1,155 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1, minMagiskVersion = -1;
protected BaseModule() {
mId = mName = mVersion = mAuthor = mDescription = "";
}
protected BaseModule(Cursor c) {
mId = nonNull(c.getString(c.getColumnIndex("id")));
mName = nonNull(c.getString(c.getColumnIndex("name")));
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
minMagiskVersion = c.getInt(c.getColumnIndex("minMagisk"));
}
protected BaseModule(Parcel p) {
mId = p.readString();
mName = p.readString();
mVersion = p.readString();
mAuthor = p.readString();
mDescription = p.readString();
mVersionCode = p.readInt();
minMagiskVersion = p.readInt();
}
@Override
public int compareTo(@NonNull BaseModule module) {
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mId);
dest.writeString(mName);
dest.writeString(mVersion);
dest.writeString(mAuthor);
dest.writeString(mDescription);
dest.writeInt(mVersionCode);
dest.writeInt(minMagiskVersion);
}
private String nonNull(String s) {
return s == null ? "" : s;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("id", mId);
values.put("name", mName);
values.put("version", mVersion);
values.put("versionCode", mVersionCode);
values.put("author", mAuthor);
values.put("description", mDescription);
values.put("minMagisk", minMagiskVersion);
return values;
}
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2)
continue;
String key = prop[0].trim();
String value = prop[1].trim();
if (key.isEmpty() || key.charAt(0) == '#')
continue;
switch (key) {
case "id":
mId = value;
break;
case "name":
mName = value;
break;
case "version":
mVersion = value;
break;
case "versionCode":
mVersionCode = Integer.parseInt(value);
break;
case "author":
mAuthor = value;
break;
case "description":
mDescription = value;
break;
case "minMagisk":
case "template":
minMagiskVersion = Integer.parseInt(value);
break;
default:
break;
}
}
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
public int getMinMagiskVersion() {
return minMagiskVersion;
}
}

View File

@@ -0,0 +1,79 @@
package com.topjohnwu.magisk.container;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
public class Module extends BaseModule {
private SuFile mRemoveFile, mDisableFile, mUpdateFile;
private boolean mEnable, mRemove, mUpdated;
public Module(String path) {
try {
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
} catch (NumberFormatException ignored) {}
mRemoveFile = new SuFile(path, "remove");
mDisableFile = new SuFile(path, "disable");
mUpdateFile = new SuFile(path, "update");
if (getId().isEmpty()) {
int sep = path.lastIndexOf('/');
setId(path.substring(sep + 1));
}
if (getName().isEmpty()) {
setName(getId());
}
mEnable = !mDisableFile.exists();
mRemove = mRemoveFile.exists();
mUpdated = mUpdateFile.exists();
}
public static final Parcelable.Creator<Module> CREATOR = new Creator<Module>() {
/* It won't be used at any place */
@Override
public Module createFromParcel(Parcel source) {
return null;
}
@Override
public Module[] newArray(int size) {
return null;
}
};
public void createDisableFile() {
mEnable = !mDisableFile.createNewFile();
}
public void removeDisableFile() {
mEnable = mDisableFile.delete();
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile() {
mRemove = mRemoveFile.createNewFile();
}
public void deleteRemoveFile() {
mRemove = !mRemoveFile.delete();
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}

View File

@@ -0,0 +1,61 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.utils.Utils;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
public static final int DENY = 1;
public static final int ALLOW = 2;
public int uid, policy = INTERACTIVE;
public long until;
public boolean logging = true, notification = true;
public String packageName, appName;
public ApplicationInfo info;
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
String[] pkgs = pm.getPackagesForUid(uid);
if (pkgs == null || pkgs.length == 0)
throw new PackageManager.NameNotFoundException();
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = Utils.getAppLabel(info, pm);
}
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
uid = values.getAsInteger("uid");
packageName = values.getAsString("package_name");
policy = values.getAsInteger("policy");
until = values.getAsInteger("until");
logging = values.getAsInteger("logging") != 0;
notification = values.getAsInteger("notification") != 0;
info = pm.getApplicationInfo(packageName, 0);
if (info.uid != uid)
throw new PackageManager.NameNotFoundException();
appName = info.loadLabel(pm).toString();
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("uid", uid);
values.put("package_name", packageName);
values.put("policy", policy);
values.put("until", until);
values.put("logging", logging ? 1 : 0);
values.put("notification", notification ? 1 : 0);
return values;
}
@Override
public int compareTo(@NonNull Policy policy) {
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
}
}

View File

@@ -0,0 +1,109 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Utils;
import java.text.DateFormat;
import java.util.Date;
public class Repo extends BaseModule {
private Date mLastUpdate;
public Repo(String id) {
setId(id);
}
public Repo(Cursor c) {
super(c);
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
}
public Repo(Parcel p) {
super(p);
mLastUpdate = new Date(p.readLong());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override
public Repo createFromParcel(Parcel source) {
return new Repo(source);
}
@Override
public Repo[] newArray(int size) {
return new Repo[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(mLastUpdate.getTime());
}
public void update() throws IllegalRepoException {
String props[] = Utils.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
}
if (getVersionCode() < 0) {
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
}
if (getMinMagiskVersion() < Const.MIN_MODULE_VER) {
Logger.debug("Repo [" + getId() + "] is outdated");
}
}
public void update(Date lastUpdate) throws IllegalRepoException {
mLastUpdate = lastUpdate;
update();
}
@Override
public ContentValues getContentValues() {
ContentValues values = super.getContentValues();
values.put("last_update", mLastUpdate.getTime());
return values;
}
public String getZipUrl() {
return String.format(Const.Url.ZIP_URL, getId());
}
public String getPropUrl() {
return String.format(Const.Url.FILE_URL, getId(), "module.prop");
}
public String getDetailUrl() {
return String.format(Const.Url.FILE_URL, getId(), "README.md");
}
public String getLastUpdateString() {
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
}
public Date getLastUpdate() {
return mLastUpdate;
}
public String getDownloadFilename() {
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
}
public class IllegalRepoException extends Exception {
IllegalRepoException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,56 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import com.topjohnwu.magisk.utils.LocaleManager;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SuLogEntry {
public int fromUid, toUid, fromPid;
public String packageName, appName, command;
public boolean action;
public Date date;
public SuLogEntry(Policy policy) {
fromUid = policy.uid;
packageName = policy.packageName;
appName = policy.appName;
action = policy.policy == Policy.ALLOW;
}
public SuLogEntry(ContentValues values) {
fromUid = values.getAsInteger("from_uid");
packageName = values.getAsString("package_name");
appName = values.getAsString("app_name");
fromPid = values.getAsInteger("from_pid");
command = values.getAsString("command");
toUid = values.getAsInteger("to_uid");
action = values.getAsInteger("action") != 0;
date = new Date(values.getAsLong("time"));
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("from_uid", fromUid);
values.put("package_name", packageName);
values.put("app_name", appName);
values.put("from_pid", fromPid);
values.put("command", command);
values.put("to_uid", toUid);
values.put("action", action ? 1 : 0);
values.put("time", date.getTime());
return values;
}
public String getDateString() {
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
}
public String getTimeString() {
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
}
}

View File

@@ -0,0 +1,64 @@
package com.topjohnwu.magisk.container;
import org.kamranzafar.jtar.TarHeader;
import java.io.File;
import java.util.Arrays;
public class TarEntry extends org.kamranzafar.jtar.TarEntry {
public TarEntry(File file, String entryName) {
super(file, entryName);
}
/*
* Workaround missing java.nio.file.attribute.PosixFilePermission
* Simply just assign a default permission to the file
* */
@Override
public void extractTarHeader(String entryName) {
int permissions = file.isDirectory() ? 000755 : 000644;
header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory(), permissions);
header.userName = new StringBuffer("");
header.groupName = header.userName;
}
/*
* Rewrite the header to GNU format
* */
@Override
public void writeEntryHeader(byte[] outbuf) {
super.writeEntryHeader(outbuf);
System.arraycopy("ustar \0".getBytes(), 0, outbuf, 257, TarHeader.USTAR_MAGICLEN);
getOctalBytes(header.mode, outbuf, 100, TarHeader.MODELEN);
getOctalBytes(header.userId, outbuf, 108, TarHeader.UIDLEN);
getOctalBytes(header.groupId, outbuf, 116, TarHeader.GIDLEN);
getOctalBytes(header.size, outbuf, 124, TarHeader.SIZELEN);
getOctalBytes(header.modTime, outbuf, 136, TarHeader.MODTIMELEN);
Arrays.fill(outbuf, 148, 148 + TarHeader.CHKSUMLEN, (byte) ' ');
Arrays.fill(outbuf, 329, 329 + TarHeader.USTAR_DEVLEN, (byte) '\0');
Arrays.fill(outbuf, 337, 337 + TarHeader.USTAR_DEVLEN, (byte) '\0');
// Recalculate checksum
getOctalBytes(computeCheckSum(outbuf), outbuf, 148, TarHeader.CHKSUMLEN);
}
/*
* Proper octal to ASCII conversion
* */
private void getOctalBytes(long value, byte[] buf, int offset, int length) {
int idx = length - 1;
buf[offset + idx] = 0;
--idx;
for (long val = value; idx >= 0; --idx) {
buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7));
val = val >> 3;
}
}
}

View File

@@ -0,0 +1,43 @@
package com.topjohnwu.magisk.container;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ValueSortedMap<K, V extends Comparable<? super V>> extends HashMap<K, V> {
private List<V> sorted = new ArrayList<>();
@NonNull
@Override
public Collection<V> values() {
if (sorted.isEmpty()) {
sorted.addAll(super.values());
Collections.sort(sorted);
}
return sorted;
}
@Override
public V put(K key, V value) {
sorted.clear();
return super.put(key, value);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
sorted.clear();
super.putAll(m);
}
@Override
public V remove(Object key) {
sorted.clear();
return super.remove(key);
}
}

View File

@@ -0,0 +1,186 @@
package com.topjohnwu.magisk.database;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.container.SuLogEntry;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class MagiskDB {
private static final String POLICY_TABLE = "policies";
private static final String LOG_TABLE = "logs";
private static final String SETTINGS_TABLE = "settings";
private static final String STRINGS_TABLE = "strings";
private PackageManager pm;
public MagiskDB(Context context) {
pm = context.getPackageManager();
}
public void deletePolicy(Policy policy) {
deletePolicy(policy.uid);
}
private List<String> rawSQL(String fmt, Object... args) {
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
}
private List<ContentValues> SQL(String fmt, Object... args) {
List<ContentValues> list = new ArrayList<>();
for (String raw : rawSQL(fmt, args)) {
ContentValues values = new ContentValues();
String[] cols = raw.split("\\|");
for (String col : cols) {
String[] pair = col.split("=", 2);
if (pair.length != 2)
continue;
values.put(pair[0], pair[1]);
}
list.add(values);
}
return list;
}
private String toSQL(ContentValues values) {
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
keys.append('(');
vals.append("VALUES(");
boolean first = true;
for (Map.Entry<String, Object> entry : values.valueSet()) {
if (!first) {
keys.append(',');
vals.append(',');
} else {
first = false;
}
keys.append(entry.getKey());
vals.append('"');
vals.append(entry.getValue());
vals.append('"');
}
keys.append(')');
vals.append(')');
keys.append(vals);
return keys.toString();
}
public void clearOutdated() {
rawSQL(
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
"DELETE FROM %s WHERE time < %d",
POLICY_TABLE, System.currentTimeMillis() / 1000,
LOG_TABLE, System.currentTimeMillis() - Config.suLogTimeout * 86400000
);
}
public void deletePolicy(String pkg) {
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
}
public void deletePolicy(int uid) {
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
}
public Policy getPolicy(int uid) {
List<ContentValues> res =
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
if (!res.isEmpty()) {
try {
return new Policy(res.get(0), pm);
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(uid);
}
}
return null;
}
public void updatePolicy(Policy policy) {
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
}
public List<Policy> getPolicyList() {
List<Policy> list = new ArrayList<>();
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
try {
list.add(new Policy(values, pm));
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(values.getAsInteger("uid"));
}
}
Collections.sort(list);
return list;
}
public List<List<SuLogEntry>> getLogs() {
List<List<SuLogEntry>> ret = new ArrayList<>();
List<SuLogEntry> list = null;
String dateString = null, newString;
for (ContentValues values : SQL("SELECT * FROM %s ORDER BY time DESC", LOG_TABLE)) {
Date date = new Date(values.getAsLong("time"));
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
if (!TextUtils.equals(dateString, newString)) {
dateString = newString;
list = new ArrayList<>();
ret.add(list);
}
list.add(new SuLogEntry(values));
}
return ret;
}
public void addLog(SuLogEntry log) {
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
}
public void clearLogs() {
rawSQL("DELETE FROM %s", LOG_TABLE);
}
public void setSettings(String key, int value) {
ContentValues data = new ContentValues();
data.put("key", key);
data.put("value", value);
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
}
public int getSettings(String key, int defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
if (res.isEmpty())
return defaultValue;
return res.get(0).getAsInteger("value");
}
public void setStrings(String key, String value) {
if (value == null) {
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
return;
}
ContentValues data = new ContentValues();
data.put("key", key);
data.put("value", value);
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
}
public String getStrings(String key, String defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
if (res.isEmpty())
return defaultValue;
return res.get(0).getAsString("value");
}
}

View File

@@ -0,0 +1,113 @@
package com.topjohnwu.magisk.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.Const;
import com.topjohnwu.magisk.container.Repo;
import java.util.HashSet;
import java.util.Set;
public class RepoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 4;
private static final String TABLE_NAME = "repos";
private SQLiteDatabase mDb;
public RepoDatabaseHelper(Context context) {
super(context, "repo.db", null, DATABASE_VER);
mDb = getWritableDatabase();
// Remove outdated repos
mDb.delete(TABLE_NAME, "minMagisk<?", new String[] { String.valueOf(Const.MIN_MODULE_VER) });
}
@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, minMagisk INT, " +
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
Config.remove(Config.Key.ETAG_KEY);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, 0, DATABASE_VER);
}
public void clearRepo() {
mDb.delete(TABLE_NAME, null, null);
}
public void removeRepo(String id) {
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
}
public void removeRepo(Repo repo) {
removeRepo(repo.getId());
}
public void removeRepo(Iterable<String> list) {
for (String id : list) {
if (id == null) continue;
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
}
}
public void addRepo(Repo repo) {
mDb.replace(TABLE_NAME, null, repo.getContentValues());
}
public Repo getRepo(String id) {
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
if (c.moveToNext()) {
return new Repo(c);
}
}
return null;
}
public Cursor getRawCursor() {
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
}
public Cursor getRepoCursor() {
String orderBy = null;
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
case Config.Value.ORDER_NAME:
orderBy = "name COLLATE NOCASE";
break;
case Config.Value.ORDER_DATE:
orderBy = "last_update DESC";
}
return mDb.query(TABLE_NAME, null, "minMagisk<=? AND minMagisk>=?",
new String[] { String.valueOf(Config.magiskVersionCode), String.valueOf(Const.MIN_MODULE_VER) },
null, null, orderBy);
}
public Set<String> getRepoIDSet() {
HashSet<String> set = new HashSet<>(300);
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")));
}
}
return set;
}
}

View File

@@ -0,0 +1,114 @@
package com.topjohnwu.magisk.tasks;
import android.os.SystemClock;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.Request;
import com.topjohnwu.net.ResponseListener;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.json.JSONException;
import org.json.JSONObject;
public class CheckUpdates {
private static Request getRequest() {
String url;
switch ((int) Config.get(Config.Key.UPDATE_CHANNEL)) {
case Config.Value.BETA_CHANNEL:
url = Const.Url.BETA_URL;
break;
case Config.Value.CUSTOM_CHANNEL:
url = Config.get(Config.Key.CUSTOM_CHANNEL);
break;
case Config.Value.CANARY_CHANNEL:
url = Const.Url.CANARY_URL;
break;
case Config.Value.CANARY_DEBUG_CHANNEL:
url = Const.Url.CANARY_DEBUG_URL;
break;
case Config.Value.STABLE_CHANNEL:
default:
url = Const.Url.STABLE_URL;
break;
}
return Networking.get(url);
}
public static void check() {
getRequest().getAsJSONObject(new UpdateListener(null));
}
public static void checkNow(Runnable cb) {
JSONObject json = getRequest().execForJSONObject().getResult();
if (json != null)
new UpdateListener(cb).onResponse(json);
}
private static class UpdateListener implements ResponseListener<JSONObject> {
private Runnable cb;
private long start;
UpdateListener(Runnable callback) {
cb = callback;
start = SystemClock.uptimeMillis();
}
private int getInt(JSONObject json, String name, int defValue) {
if (json == null)
return defValue;
try {
return json.getInt(name);
} catch (JSONException e) {
return defValue;
}
}
private String getString(JSONObject json, String name, String defValue) {
if (json == null)
return defValue;
try {
return json.getString(name);
} catch (JSONException e) {
return defValue;
}
}
private JSONObject getJson(JSONObject json, String name) {
try {
return json.getJSONObject(name);
} catch (JSONException e) {
return null;
}
}
@Override
public void onResponse(JSONObject json) {
JSONObject magisk = getJson(json, "magisk");
Config.remoteMagiskVersionString = getString(magisk, "version", null);
Config.remoteMagiskVersionCode = getInt(magisk, "versionCode", -1);
Config.magiskLink = getString(magisk, "link", null);
Config.magiskNoteLink = getString(magisk, "note", null);
Config.magiskMD5 = getString(magisk, "md5", null);
JSONObject manager = getJson(json, "app");
Config.remoteManagerVersionString = getString(manager, "version", null);
Config.remoteManagerVersionCode = getInt(manager, "versionCode", -1);
Config.managerLink = getString(manager, "link", null);
Config.managerNoteLink = getString(manager, "note", null);
JSONObject uninstaller = getJson(json, "uninstaller");
Config.uninstallerLink = getString(uninstaller, "link", null);
UiThreadHandler.handler.postAtTime(() -> Topic.publish(Topic.UPDATE_CHECK_DONE),
start + 1000 /* Add artificial delay to let UI behave correctly */);
if (cb != null)
cb.run();
}
}
}

View File

@@ -0,0 +1,85 @@
package com.topjohnwu.magisk.tasks;
import android.net.Uri;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.ZipUtils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public abstract class FlashZip {
private Uri mUri;
private File tmpFile;
private List<String> console, logs;
public FlashZip(Uri uri, List<String> out, List<String> err) {
mUri = uri;
console = out;
logs = err;
tmpFile = new File(App.self.getCacheDir(), "install.zip");
}
private boolean unzipAndCheck() throws IOException {
ZipUtils.unzip(tmpFile, tmpFile.getParentFile(), "META-INF/com/google/android", true);
return Shell.su("grep -q '#MAGISK' " + new File(tmpFile.getParentFile(), "updater-script"))
.exec().isSuccess();
}
private boolean flash() throws IOException {
console.add("- Copying zip to temp directory");
try (InputStream in = App.self.getContentResolver().openInputStream(mUri);
OutputStream out = new BufferedOutputStream(new FileOutputStream(tmpFile))) {
if (in == null) throw new FileNotFoundException();
InputStream buf= new BufferedInputStream(in);
ShellUtils.pump(buf, out);
} catch (FileNotFoundException e) {
console.add("! Invalid Uri");
throw e;
} catch (IOException e) {
console.add("! Cannot copy to cache");
throw e;
}
try {
if (!unzipAndCheck()) {
console.add("! This zip is not a Magisk Module!");
return false;
}
} catch (IOException e) {
console.add("! Unzip error");
throw e;
}
console.add("- Installing " + Utils.getNameFromUri(App.self, mUri));
return Shell.su("cd " + tmpFile.getParent(),
"BOOTMODE=true sh update-binary dummy 1 " + tmpFile)
.to(console, logs)
.exec().isSuccess();
}
public void exec() {
App.THREAD_POOL.execute(() -> {
boolean success = false;
try {
success = flash();
} catch (IOException ignored) {}
Shell.su("cd /", "rm -rf " + tmpFile.getParent() + " " + Const.TMP_FOLDER_PATH).submit();
boolean finalSuccess = success;
UiThreadHandler.run(() -> onResult(finalSuccess));
});
}
protected abstract void onResult(boolean success);
}

View File

@@ -0,0 +1,318 @@
package com.topjohnwu.magisk.tasks;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
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.container.TarEntry;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.DownloadProgressListener;
import com.topjohnwu.net.Networking;
import com.topjohnwu.signing.SignBoot;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.NOPList;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import org.kamranzafar.jtar.TarInputStream;
import org.kamranzafar.jtar.TarOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public abstract class MagiskInstaller {
private List<String> console, logs;
protected String srcBoot;
protected File installDir;
private class ProgressLog implements DownloadProgressListener {
private int prev = -1;
private int location;
@Override
public void onProgress(long bytesDownloaded, long totalBytes) {
if (prev < 0) {
location = console.size();
console.add("... 0%");
}
int curr = (int) (100 * bytesDownloaded / totalBytes);
if (prev != curr) {
prev = curr;
console.set(location, "... " + prev + "%");
}
}
}
protected MagiskInstaller() {
console = NOPList.getInstance();
logs = NOPList.getInstance();
}
public MagiskInstaller(List<String> out, List<String> err) {
console = out;
logs = err;
installDir = new File(Utils.getDEContext().getFilesDir().getParent(), "install");
Shell.sh("rm -rf " + installDir).exec();
installDir.mkdirs();
}
protected boolean findImage() {
console.add("- Detecting target image");
srcBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\"");
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image");
return false;
}
return true;
}
protected boolean findSecondaryImage() {
String slot = ShellUtils.fastCmd("echo $SLOT");
String target = (TextUtils.equals(slot, "_a") ? "_b" : "_a");
console.add("- Target slot: " + target);
console.add("- Detecting target image");
srcBoot = ShellUtils.fastCmd(
"SLOT=" + target,
"find_boot_image",
"SLOT=" + slot,
"echo \"$BOOTIMAGE\""
);
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image");
return false;
}
return true;
}
protected boolean extractZip() {
String arch;
if (Build.VERSION.SDK_INT >= 21) {
List<String> abis = Arrays.asList(Build.SUPPORTED_ABIS);
arch = abis.contains("x86") ? "x86" : "arm";
} else {
arch = TextUtils.equals(Build.CPU_ABI, "x86") ? "x86" : "arm";
}
console.add("- Device platform: " + Build.CPU_ABI);
File zip = new File(App.self.getCacheDir(), "magisk.zip");
if (!ShellUtils.checkSum("MD5", zip, Config.magiskMD5)) {
console.add("- Downloading zip");
Networking.get(Config.magiskLink)
.setDownloadProgressListener(new ProgressLog())
.execForFile(zip);
} else {
console.add("- Existing zip found");
}
try {
ZipInputStream zi = new ZipInputStream(new BufferedInputStream(
new FileInputStream(zip), (int) zip.length()));
ZipEntry ze;
while ((ze = zi.getNextEntry()) != null) {
if (ze.isDirectory())
continue;
String name = null;
String[] names = { arch + "/", "common/", "META-INF/com/google/android/update-binary" };
for (String n : names) {
if (ze.getName().startsWith(n)) {
name = ze.getName().substring(ze.getName().lastIndexOf('/') + 1);
break;
}
}
if (name == null && ze.getName().startsWith("chromeos/"))
name = ze.getName();
if (name == null)
continue;
File dest;
if (installDir instanceof SuFile) {
dest = new SuFile(installDir, name);
} else {
dest = new File(installDir, name);
}
dest.getParentFile().mkdirs();
try (OutputStream out = new SuFileOutputStream(dest)) {
ShellUtils.pump(zi, out);
}
}
} catch (IOException e) {
console.add("! Cannot unzip zip");
return false;
}
return true;
}
protected boolean copyBoot(Uri bootUri) {
srcBoot = new File(installDir, "boot.img").getPath();
console.add("- Copying image to cache");
// Copy boot image to local
try (InputStream in = App.self.getContentResolver().openInputStream(bootUri);
OutputStream out = new FileOutputStream(srcBoot)) {
if (in == null)
throw new FileNotFoundException();
InputStream src;
if (Utils.getNameFromUri(App.self, bootUri).endsWith(".tar")) {
// Extract boot.img from tar
TarInputStream tar = new TarInputStream(new BufferedInputStream(in));
org.kamranzafar.jtar.TarEntry entry;
while ((entry = tar.getNextEntry()) != null) {
if (entry.getName().equals("boot.img"))
break;
}
src = tar;
} else {
// Direct copy raw image
src = new BufferedInputStream(in);
}
ShellUtils.pump(src, out);
} catch (FileNotFoundException e) {
console.add("! Invalid Uri");
return false;
} catch (IOException e) {
console.add("! Copy failed");
return false;
}
return true;
}
protected boolean patchBoot() {
boolean isSigned;
try (InputStream in = new SuFileInputStream(srcBoot)) {
isSigned = SignBoot.verifySignature(in, null);
if (isSigned) {
console.add("- Boot image is signed with AVB 1.0");
}
} catch (IOException e) {
console.add("! Unable to check signature");
return false;
}
Shell.Job job = Shell.sh("cd " + installDir);
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.length != 0) {
job.add("mv -f magiskinit64 magiskinit 2>/dev/null");
} else {
job.add("rm -f magiskinit64 2>/dev/null");
}
if (!job.add(Utils.fmt("KEEPFORCEENCRYPT=%b KEEPVERITY=%b " +
"sh update-binary sh boot_patch.sh %s",
Config.keepEnc, Config.keepVerity, srcBoot)).to(console, logs).exec().isSuccess())
return false;
job = Shell.sh("./magiskboot --cleanup",
"mv bin/busybox busybox",
"rm -rf magisk.apk bin boot.img update-binary",
"cd /");
File patched = new File(installDir, "new-boot.img");
if (isSigned) {
console.add("- Signing boot image with test keys");
File signed = new File(installDir, "signed.img");
try (InputStream in = new SuFileInputStream(patched);
OutputStream out = new BufferedOutputStream(new FileOutputStream(signed))) {
SignBoot.doSignature("/boot", in, out, null, null);
} catch (IOException e) {
return false;
}
job.add("mv -f " + signed + " " + patched);
}
job.exec();
return true;
}
protected boolean flashBoot() {
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
if (!Config.keepVerity)
Shell.su("patch_dtbo_image").to(console, logs).exec();
return true;
}
protected boolean storeBoot() {
File patched = new File(installDir, "new-boot.img");
String fmt = Config.get(Config.Key.BOOT_FORMAT);
File dest = new File(Const.EXTERNAL_PATH, "patched_boot" + fmt);
dest.getParentFile().mkdirs();
OutputStream os;
try {
switch (fmt) {
case ".img.tar":
os = new TarOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
((TarOutputStream) os).putNextEntry(new TarEntry(patched, "boot.img"));
break;
default:
case ".img":
os = new BufferedOutputStream(new FileOutputStream(dest));
break;
}
try (InputStream in = new SuFileInputStream(patched)) {
ShellUtils.pump(in, os);
os.close();
}
} catch (IOException e) {
console.add("! Failed to store boot to " + dest);
return false;
}
Shell.sh("rm -f " + patched).exec();
console.add("");
console.add("****************************");
console.add(" Patched image is placed in ");
console.add(" " + dest + " ");
console.add("****************************");
return true;
}
protected boolean postOTA() {
SuFile bootctl = new SuFile("/data/adb/bootctl");
try (InputStream in = Networking.get(Const.Url.BOOTCTL_URL).execForInputStream().getResult();
OutputStream out = new SuFileOutputStream(bootctl)) {
ShellUtils.pump(in, out);
} catch (IOException e) {
e.printStackTrace();
return false;
}
Shell.su("post_ota " + bootctl.getParent()).exec();
console.add("***************************************");
console.add(" Next reboot will boot to second slot!");
console.add("***************************************");
return true;
}
@WorkerThread
protected abstract boolean operations();
@MainThread
protected abstract void onResult(boolean success);
public void exec() {
App.THREAD_POOL.execute(() -> {
boolean b = operations();
UiThreadHandler.run(() -> onResult(b));
});
}
}

View File

@@ -0,0 +1,176 @@
package com.topjohnwu.magisk.tasks;
import android.database.Cursor;
import android.util.Pair;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.container.Repo;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.Request;
import org.json.JSONArray;
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;
import java.util.Date;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;
private App app = App.self;
private Set<String> cached;
private Queue<Pair<String, Date>> moduleQueue;
static {
DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private void runTasks(Runnable task) {
Future[] futures = new Future[App.THREAD_POOL.getMaximumPoolSize() - 1];
for (int i = 0; i < futures.length; ++i) {
futures[i] = App.THREAD_POOL.submit(task);
}
for (Future f : futures) {
while (true) {
try {
f.get();
} catch (InterruptedException e) {
continue;
} catch (ExecutionException ignored) {}
break;
}
}
}
/* 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));
if (page == 0) {
String etag = Config.get(Config.Key.ETAG_KEY);
if (etag != null)
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
}
Request.Result<JSONArray> res = req.execForJSONArray();
// JSON not updated
if (res.getCode() == HttpURLConnection.HTTP_NOT_MODIFIED)
return false;
// Network error
if (res.getResult() == null) {
cached.clear();
return true;
}
// Current page is the last page
if (res.getResult().length() == 0)
return true;
try {
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"));
moduleQueue.offer(new Pair<>(id, date));
}
} catch (JSONException | ParseException e) {
// Should not happen, but if exception occurs, page load fails
return false;
}
// Update ETAG
if (page == 0) {
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);
}
}
String links = res.getConnection().getHeaderField(Const.Key.LINK_KEY);
return links == null || !links.contains("next") || parsePage(page + 1);
}
private boolean loadPages() {
if (!parsePage(0))
return false;
runTasks(() -> {
while (true) {
Pair<String, Date> pair = moduleQueue.poll();
if (pair == null)
return;
Repo repo = app.repoDB.getRepo(pair.first);
try {
if (repo == null)
repo = new Repo(pair.first);
else
cached.remove(pair.first);
repo.update(pair.second);
app.repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.repoDB.removeRepo(pair.first);
}
}
});
return true;
}
private void fullReload() {
Cursor c = app.repoDB.getRawCursor();
runTasks(() -> {
while (true) {
Repo repo;
synchronized (c) {
if (!c.moveToNext())
return;
repo = new Repo(c);
}
try {
repo.update();
app.repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.repoDB.removeRepo(repo);
}
}
});
}
public void exec(boolean force) {
Topic.reset(Topic.REPO_LOAD_DONE);
App.THREAD_POOL.execute(() -> {
cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet());
moduleQueue = new ConcurrentLinkedQueue<>();
if (loadPages()) {
// The leftover cached means they are removed from online repo
app.repoDB.removeRepo(cached);
} else if (force) {
fullReload();
}
Topic.publish(Topic.REPO_LOAD_DONE);
});
}
public void exec() {
exec(false);
}
}

View File

@@ -0,0 +1,123 @@
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,18 @@
package com.topjohnwu.magisk.utils;
public interface ISafetyNetHelper {
int RESPONSE_ERR = 0x01;
int CONNECTION_FAIL = 0x02;
int BASIC_PASS = 0x10;
int CTS_PASS = 0x20;
void attest();
int getVersion();
interface Callback {
void onResponse(int responseCode);
}
}

View File

@@ -0,0 +1,146 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import androidx.annotation.StringRes;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.InternalUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
public class LocaleManager {
public static Locale locale = Locale.getDefault();
public final static Locale defaultLocale = Locale.getDefault();
public static List<Locale> locales;
public static Locale forLanguageTag(String tag) {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(tag);
} else {
String[] tok = tag.split("-");
if (tok.length == 0) {
return new Locale("");
}
String language;
switch (tok[0]) {
case "und":
language = ""; // Undefined
break;
case "fil":
language = "tl"; // Filipino
break;
default:
language = tok[0];
}
if ((language.length() != 2 && language.length() != 3))
return new Locale("");
if (tok.length == 1)
return new Locale(language);
String country = tok[1];
if (country.length() != 2 && country.length() != 3)
return new Locale(language);
return new Locale(language, country);
}
}
public static String toLanguageTag(Locale loc) {
if (Build.VERSION.SDK_INT >= 21) {
return loc.toLanguageTag();
} else {
String language = loc.getLanguage();
String country = loc.getCountry();
String variant = loc.getVariant();
if (language.isEmpty() || !language.matches("\\p{Alpha}{2,8}")) {
language = "und"; // Follow the Locale#toLanguageTag() implementation
} else if (language.equals("iw")) {
language = "he"; // correct deprecated "Hebrew"
} else if (language.equals("in")) {
language = "id"; // correct deprecated "Indonesian"
} else if (language.equals("ji")) {
language = "yi"; // correct deprecated "Yiddish"
}
// ensure valid country code, if not well formed, it's omitted
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}")) {
country = "";
}
// variant subtags that begin with a letter must be at least 5 characters long
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}")) {
variant = "";
}
StringBuilder tag = new StringBuilder(language);
if (!country.isEmpty())
tag.append('-').append(country);
if (!variant.isEmpty())
tag.append('-').append(variant);
return tag.toString();
}
}
public static void setLocale(ContextWrapper wrapper) {
String localeConfig = Config.get(Config.Key.LOCALE);
if (localeConfig.isEmpty()) {
locale = defaultLocale;
} else {
locale = forLanguageTag(localeConfig);
}
Locale.setDefault(locale);
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(locale));
}
public static Context getLocaleContext(Context context, Locale locale) {
Configuration config = new Configuration(context.getResources().getConfiguration());
config.setLocale(locale);
return context.createConfigurationContext(config);
}
public static Context getLocaleContext(Locale locale) {
return getLocaleContext(App.self.getBaseContext(), locale);
}
public static String getString(Locale locale, @StringRes int id) {
return getLocaleContext(locale).getString(id);
}
public static void loadAvailableLocales(@StringRes int compareId) {
Shell.EXECUTOR.execute(() -> {
locales = new ArrayList<>();
HashSet<String> set = new HashSet<>();
Resources res = App.self.getResources();
Locale locale;
// Add default locale
locales.add(Locale.ENGLISH);
set.add(getString(Locale.ENGLISH, compareId));
// Add some special locales
locales.add(Locale.TAIWAN);
set.add(getString(Locale.TAIWAN, compareId));
locale = new Locale("pt", "BR");
locales.add(locale);
set.add(getString(locale, compareId));
// Other locales
for (String s : res.getAssets().getLocales()) {
locale = forLanguageTag(s);
if (set.add(getString(locale, compareId))) {
locales.add(locale);
}
}
Collections.sort(locales, (a, b) -> a.getDisplayName(a).compareTo(b.getDisplayName(b)));
Topic.publish(Topic.LOCALE_FETCH_DONE);
});
}
}

View File

@@ -0,0 +1,26 @@
package com.topjohnwu.magisk.utils;
import android.util.Log;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Const;
public class Logger {
public static void debug(String line) {
if (BuildConfig.DEBUG)
Log.d(Const.DEBUG_TAG, "DEBUG: " + line);
}
public static void debug(String fmt, Object... args) {
debug(Utils.fmt(fmt, args));
}
public static void error(String line) {
Log.e(Const.DEBUG_TAG, "ERROR: " + line);
}
public static void error(String fmt, Object... args) {
error(Utils.fmt(fmt, args));
}
}

View File

@@ -0,0 +1,42 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
public class RootUtils extends Shell.Initializer {
public static void rmAndLaunch(String rm, String launch) {
Shell.su(Utils.fmt("(rm_launch %s %s)&", rm, launch)).exec();
}
@Override
public boolean onInit(Context context, @NonNull Shell shell) {
Shell.Job job = shell.newJob();
if (shell.isRoot()) {
job.add(context.getResources().openRawResource(R.raw.util_functions))
.add(context.getResources().openRawResource(R.raw.utils));
Const.MAGISK_DISABLE_FILE = new SuFile("/cache/.disable_magisk");
Config.loadMagiskInfo();
} else {
InputStream nonroot = context.getResources().openRawResource(R.raw.nonroot_utils);
job.add(nonroot);
}
job.add("mount_partitions", "get_flags", "run_migrations", "export BOOTMODE=true").exec();
Config.keepVerity = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPVERITY"));
Config.keepEnc = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPFORCEENCRYPT"));
Config.recovery = Boolean.parseBoolean(ShellUtils.fastCmd("echo $RECOVERYMODE"));
return true;
}
}

View File

@@ -0,0 +1,61 @@
package com.topjohnwu.magisk.utils;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Bundle;
import android.text.TextUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public abstract class SuConnector {
private LocalSocket socket;
protected DataOutputStream out;
protected DataInputStream in;
protected SuConnector(String name) throws IOException {
socket = new LocalSocket();
socket.connect(new LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT));
out = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
in = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
}
private String readString() throws IOException {
int len = in.readInt();
byte[] buf = new byte[len];
in.readFully(buf);
return new String(buf, "UTF-8");
}
public Bundle readSocketInput() throws IOException {
Bundle bundle = new Bundle();
while (true) {
String name = readString();
if (TextUtils.equals(name, "eof"))
break;
bundle.putString(name, readString());
}
return bundle;
}
public void response() {
try {
onResponse();
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
try {
in.close();
out.close();
socket.close();
} catch (IOException ignored) { }
}
protected abstract void onResponse() throws IOException;
}

View File

@@ -0,0 +1,86 @@
package com.topjohnwu.magisk.utils;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Process;
import android.widget.Toast;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.container.SuLogEntry;
import java.util.Date;
public abstract class SuLogger {
public void handleLogs(Intent intent) {
int fromUid = intent.getIntExtra("from.uid", -1);
if (fromUid < 0) return;
if (fromUid == Process.myUid()) return;
App app = App.self;
PackageManager pm = app.getPackageManager();
Policy policy;
boolean notify;
Bundle data = intent.getExtras();
if (data.containsKey("notify")) {
notify = data.getBoolean("notify");
try {
policy = new Policy(fromUid, pm);
} catch (PackageManager.NameNotFoundException e) {
return;
}
} else {
// Doesn't report whether notify or not, check database ourselves
policy = app.mDB.getPolicy(fromUid);
if (policy == null)
return;
notify = policy.notification;
}
policy.policy = data.getInt("policy", -1);
if (policy.policy < 0)
return;
if (notify)
handleNotify(policy);
SuLogEntry log = new SuLogEntry(policy);
int toUid = intent.getIntExtra("to.uid", -1);
if (toUid < 0) return;
int pid = intent.getIntExtra("pid", -1);
if (pid < 0) return;
String command = intent.getStringExtra("command");
if (command == null) return;
log.toUid = toUid;
log.fromPid = pid;
log.command = command;
log.date = new Date();
app.mDB.addLog(log);
}
private void handleNotify(Policy policy) {
if (policy.notification &&
(int) Config.get(Config.Key.SU_NOTIFICATION) == Config.Value.NOTIFICATION_TOAST)
Utils.toast(getMessage(policy), Toast.LENGTH_SHORT);
}
public void handleNotify(Intent intent) {
int fromUid = intent.getIntExtra("from.uid", -1);
if (fromUid < 0) return;
if (fromUid == Process.myUid()) return;
try {
Policy policy = new Policy(fromUid, App.self.getPackageManager());
policy.policy = intent.getIntExtra("policy", -1);
if (policy.policy >= 0)
handleNotify(policy);
} catch (PackageManager.NameNotFoundException ignored) {}
}
public abstract String getMessage(Policy policy);
}

View File

@@ -0,0 +1,108 @@
package com.topjohnwu.magisk.utils;
import androidx.annotation.IntDef;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.Set;
public class Topic {
public static final int MAGISK_HIDE_DONE = 0;
public static final int MODULE_LOAD_DONE = 1;
public static final int REPO_LOAD_DONE = 2;
public static final int UPDATE_CHECK_DONE = 3;
public static final int LOCALE_FETCH_DONE = 4;
@IntDef({MAGISK_HIDE_DONE, MODULE_LOAD_DONE, REPO_LOAD_DONE,
UPDATE_CHECK_DONE, LOCALE_FETCH_DONE})
@Retention(RetentionPolicy.SOURCE)
public @interface TopicID {}
// We will not dynamically add topics, so use arrays instead of hash tables
private static Store[] topicList = new Store[5];
public static void subscribe(Subscriber sub, @TopicID int... topics) {
for (int topic : topics) {
if (topicList[topic] == null)
topicList[topic] = new Store();
topicList[topic].subscribers.add(sub);
if (topicList[topic].published) {
sub.onPublish(topic, topicList[topic].results);
}
}
}
public static void subscribe(AutoSubscriber sub) {
subscribe(sub, sub.getSubscribedTopics());
}
public static void unsubscribe(Subscriber sub, @TopicID int... topics) {
for (int topic : topics) {
if (topicList[topic] == null)
continue;
topicList[topic].subscribers.remove(sub);
}
}
public static void unsubscribe(AutoSubscriber sub) {
unsubscribe(sub, sub.getSubscribedTopics());
}
public static void publish(@TopicID int topic, Object... results) {
publish(true, topic, results);
}
public static void publish(boolean persist, @TopicID int topic, Object... results) {
if (topicList[topic] == null)
topicList[topic] = new Store();
if (persist) {
topicList[topic].results = results;
topicList[topic].published = true;
}
for (Subscriber sub : topicList[topic].subscribers) {
UiThreadHandler.run(() -> sub.onPublish(topic, results));
}
}
public static void reset(@TopicID int... topics) {
for (int topic : topics) {
if (topicList[topic] == null)
continue;
topicList[topic].published = false;
topicList[topic].results = null;
}
}
public static boolean isPublished(@TopicID int... topics) {
for (int topic : topics) {
if (topicList[topic] == null)
return false;
if (!topicList[topic].published)
return false;
}
return true;
}
public static boolean isPublished(AutoSubscriber sub) {
return isPublished(sub.getSubscribedTopics());
}
private static class Store {
boolean published = false;
Set<Subscriber> subscribers = new HashSet<>();
Object[] results;
}
public interface Subscriber {
void onPublish(int topic, Object[] result);
}
public interface AutoSubscriber extends Subscriber {
@TopicID
int[] getSubscribedTopics();
}
}

View File

@@ -0,0 +1,127 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
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.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.OpenableColumns;
import android.widget.Toast;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.container.ValueSortedMap;
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;
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 String getNameFromUri(Context context, Uri uri) {
String name = null;
try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
if (c != null) {
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex != -1) {
c.moveToFirst();
name = c.getString(nameIndex);
}
}
}
if (name == null) {
int idx = uri.getPath().lastIndexOf('/');
name = uri.getPath().substring(idx + 1);
}
return name;
}
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.locale);
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("\\", "_");
}
public static void loadModules() {
Topic.reset(Topic.MODULE_LOAD_DONE);
App.THREAD_POOL.execute(() -> {
Map<String, Module> moduleMap = new ValueSortedMap<>();
SuFile path = new SuFile(Const.MAGISK_PATH);
SuFile[] modules = path.listFiles(
(file, name) -> !name.equals("lost+found") && !name.equals(".core"));
for (SuFile file : modules) {
if (file.isFile()) continue;
Module module = new Module(Const.MAGISK_PATH + "/" + file.getName());
moduleMap.put(module.getId(), module);
}
Topic.publish(Topic.MODULE_LOAD_DONE, 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 Context getDEContext() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?
App.self.createDeviceProtectedStorageContext() : App.self;
}
public static void reboot() {
Shell.su("/system/bin/reboot" + (Config.recovery ? " recovery" : "")).submit();
}
}

View File

@@ -0,0 +1,65 @@
package com.topjohnwu.magisk.utils;
import com.topjohnwu.signing.JarMap;
import com.topjohnwu.signing.SignAPK;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public class ZipUtils {
public static void unzip(File zip, File folder, String path, boolean junkPath) throws IOException {
InputStream in = new BufferedInputStream(new FileInputStream(zip));
unzip(in, folder, path, junkPath);
in.close();
}
public static void unzip(InputStream zip, File folder, String path, boolean junkPath) throws IOException {
try {
ZipInputStream zipfile = new ZipInputStream(zip);
ZipEntry entry;
while ((entry = zipfile.getNextEntry()) != null) {
if (!entry.getName().startsWith(path) || entry.isDirectory()){
// Ignore directories, only create files
continue;
}
String name;
if (junkPath) {
name = entry.getName().substring(entry.getName().lastIndexOf('/') + 1);
} else {
name = entry.getName();
}
File dest = new File(folder, name);
if (!dest.getParentFile().exists() && !dest.getParentFile().mkdirs()) {
dest = new SuFile(folder, name);
dest.getParentFile().mkdirs();
}
try (OutputStream out = new SuFileOutputStream(dest)) {
ShellUtils.pump(zipfile, out);
}
}
}
catch(IOException e) {
e.printStackTrace();
throw e;
}
}
public static void signZip(File input, File output) throws Exception {
try (JarMap map = new JarMap(input, false);
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(output))) {
SignAPK.sign(map, out);
}
}
}

1
app/src/main/res/raw/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/util_functions.sh

View File

@@ -0,0 +1,9 @@
mount_partitions() {
[ "`getprop ro.build.ab_update`" = "true" ] && SLOT=`getprop ro.boot.slot_suffix` || SLOT=
[ "`getprop ro.build.system_root_image`" = "true" ] && SYSTEM_ROOT=true || SYSTEM_ROOT=false
}
get_flags() {
$SYSTEM_ROOT && KEEPVERITY=true || KEEPVERITY=false
[ "`getprop ro.crypto.state`" = "encrypted" ] && KEEPFORCEENCRYPT=true || KEEPFORCEENCRYPT=false
}

View File

@@ -0,0 +1,116 @@
env_check() {
for file in busybox magisk magiskboot magiskinit util_functions.sh boot_patch.sh; do
[ -f $MAGISKBIN/$file ] || return 1
done
return 0
}
fix_env() {
cd $MAGISKBIN
local OLDPATH="$PATH"
PATH=/sbin:/system/bin:/vendor/bin
sh update-binary -x
PATH="$OLDPATH"
./busybox rm -f $MAGISKTMP/busybox/*
$MAGISKTMP/mirror/bin/busybox --install -s $MAGISKTMP/busybox
rm -f update-binary magisk.apk
chmod -R 755 .
cd /
}
run_migrations() {
# Move the stock backups
if [ -f /data/magisk/stock_boot* ]; then
mv /data/magisk/stock_boot* /data 2>/dev/null
fi
if [ -f /data/adb/magisk/stock_boot* ]; then
mv /data/adb/magisk/stock_boot* /data 2>/dev/null
fi
# Remove old dbs
rm -f /data/user*/*/magisk.db
[ -L /data/magisk.img ] || mv /data/magisk.img /data/adb/magisk.img 2>/dev/null
}
direct_install() {
rm -rf $MAGISKBIN/* 2>/dev/null
mkdir -p $MAGISKBIN 2>/dev/null
chmod 700 $NVBASE
cp -af $1/. $MAGISKBIN
rm -f $MAGISKBIN/new-boot.img
echo "- Flashing new boot image"
flash_image $1/new-boot.img $2
if [ $? -ne 0 ]; then
echo "! Insufficient partition size"
return 1
fi
rm -rf $1
return 0
}
mm_patch_dtbo() {
$KEEPVERITY && return 1 || patch_dtbo_image
}
restore_imgs() {
local SHA1=`grep_prop SHA1 /sbin/.magisk/config`
[ -z $SHA1 ] && local SHA1=`cat /.backup/.sha1`
[ -z $SHA1 ] && return 1
local STOCKBOOT=/data/stock_boot_${SHA1}.img.gz
local STOCKDTBO=/data/stock_dtbo.img.gz
[ -f $STOCKBOOT ] || return 1
find_boot_image
find_dtbo_image
if [ -f $STOCKDTBO -a -b "$DTBOIMAGE" ]; then
flash_image $STOCKDTBO $DTBOIMAGE
fi
if [ -f $STOCKBOOT -a -b "$BOOTIMAGE" ]; then
flash_image $STOCKBOOT $BOOTIMAGE
return 0
fi
return 1
}
post_ota() {
cd $1
chmod 755 bootctl
./bootctl hal-info || return
[ `./bootctl get-current-slot` -eq 0 ] && SLOT_NUM=1 || SLOT_NUM=0
./bootctl set-active-boot-slot $SLOT_NUM
echo "BCTRL=${1}/bootctl;\$BCTRL mark-boot-successful;rm -f \$BCTRL \$0" > post-fs-data.d/post_ota.sh
chmod 755 post-fs-data.d/post_ota.sh
cd /
}
add_hosts_module() {
# Do not touch existing hosts module
[ -d /sbin/.magisk/img/hosts ] && return
cd /sbin/.magisk/img
mkdir -p hosts/system/etc
cat << EOF > hosts/module.prop
id=hosts
name=Systemless Hosts
version=1.0
versionCode=1
author=Magisk Manager
description=Magisk Manager built-in systemless hosts module
minMagisk=17000
EOF
if [ -f .core/hosts ]; then
# Migrate old hosts file to new module
mv -f .core/hosts hosts/system/etc/hosts
else
cp -f /system/etc/hosts hosts/system/etc/hosts
fi
magisk --clone-attr /system/etc/hosts hosts/system/etc/hosts
touch hosts/update
touch hosts/auto_mount
cd /
}
rm_launch() {
pm uninstall $1
am start -n ${2}/a.c
exit
}