mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-06-12 13:17:39 +02:00
Separate backend logic from frontend UI
This commit is contained in:
2
core/.gitignore
vendored
Normal file
2
core/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
src/main/res/raw/util_functions.sh
|
33
core/build.gradle
Normal file
33
core/build.gradle
Normal file
@ -0,0 +1,33 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion rootProject.ext.compileSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(':net')
|
||||
implementation project(':utils')
|
||||
implementation 'com.github.topjohnwu:libsu:2.1.2'
|
||||
implementation 'org.kamranzafar:jtar:2.3'
|
||||
}
|
21
core/proguard-rules.pro
vendored
Normal file
21
core/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
core/src/main/AndroidManifest.xml
Normal file
2
core/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.topjohnwu.core" />
|
53
core/src/main/java/com/topjohnwu/core/App.java
Normal file
53
core/src/main/java/com/topjohnwu/core/App.java
Normal file
@ -0,0 +1,53 @@
|
||||
package com.topjohnwu.core;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.topjohnwu.core.database.MagiskDB;
|
||||
import com.topjohnwu.core.database.RepoDatabaseHelper;
|
||||
import com.topjohnwu.core.utils.LocaleManager;
|
||||
import com.topjohnwu.core.utils.RootUtils;
|
||||
import com.topjohnwu.superuser.ContainerApp;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
public class App extends ContainerApp {
|
||||
|
||||
public static App self;
|
||||
public static Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
public boolean init = false;
|
||||
|
||||
// Global resources
|
||||
public SharedPreferences prefs;
|
||||
public MagiskDB mDB;
|
||||
public RepoDatabaseHelper repoDB;
|
||||
|
||||
public App() {
|
||||
self = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER);
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG);
|
||||
Shell.Config.setInitializer(RootUtils.class);
|
||||
Shell.Config.setTimeout(2);
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
mDB = new MagiskDB(this);
|
||||
repoDB = new RepoDatabaseHelper(this);
|
||||
|
||||
LocaleManager.setLocale(this);
|
||||
Data.loadConfig();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
LocaleManager.setLocale(this);
|
||||
}
|
||||
}
|
150
core/src/main/java/com/topjohnwu/core/Const.java
Normal file
150
core/src/main/java/com/topjohnwu/core/Const.java
Normal file
@ -0,0 +1,150 @@
|
||||
package com.topjohnwu.core;
|
||||
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
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;
|
||||
|
||||
/* A list of apps that should not be shown as hide-able */
|
||||
public static final List<String> HIDE_BLACKLIST = Arrays.asList(
|
||||
App.self.getPackageName(),
|
||||
"com.google.android.gms"
|
||||
);
|
||||
|
||||
public static final int USER_ID = Process.myUid() / 100000;
|
||||
|
||||
public static final class MAGISK_VER {
|
||||
/* Currently no backwards compatibility needed */
|
||||
}
|
||||
|
||||
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 class Url {
|
||||
public static final String STABLE_URL = "https://raw.githubusercontent.com/topjohnwu/magisk_files/master/stable.json";
|
||||
public static final String BETA_URL = "https://raw.githubusercontent.com/topjohnwu/magisk_files/master/beta.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 = "https://raw.githubusercontent.com/topjohnwu/magisk_files/b66b1a914978e5f4c4bbfd74a59f4ad371bac107/snet.apk";
|
||||
}
|
||||
|
||||
|
||||
public static class Key {
|
||||
// su
|
||||
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";
|
||||
|
||||
// intents
|
||||
public static final String OPEN_SECTION = "section";
|
||||
public static final String INTENT_SET_NAME = "filename";
|
||||
public static final String INTENT_SET_LINK = "link";
|
||||
public static final String FLASH_ACTION = "action";
|
||||
public static final String FLASH_SET_BOOT = "boot";
|
||||
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
|
||||
public static final String BROADCAST_REBOOT = "reboot";
|
||||
|
||||
// others
|
||||
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 APP_VER = "app_version";
|
||||
public static final String MAGISKHIDE = "magiskhide";
|
||||
public static final String HOSTS = "hosts";
|
||||
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 LINK_KEY = "Link";
|
||||
public static final String IF_NONE_MATCH = "If-None-Match";
|
||||
public static final String REPO_ORDER = "repo_order";
|
||||
}
|
||||
|
||||
|
||||
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 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 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";
|
||||
public static final int[] timeoutList = {0, -1, 10, 20, 30, 60};
|
||||
public static final int ORDER_NAME = 0;
|
||||
public static final int ORDER_DATE = 1;
|
||||
}
|
||||
}
|
154
core/src/main/java/com/topjohnwu/core/Data.java
Normal file
154
core/src/main/java/com/topjohnwu/core/Data.java
Normal file
@ -0,0 +1,154 @@
|
||||
package com.topjohnwu.core;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Xml;
|
||||
|
||||
import com.topjohnwu.core.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 Data {
|
||||
|
||||
// Current status
|
||||
public static String magiskVersionString;
|
||||
public static int magiskVersionCode = -1;
|
||||
public 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;
|
||||
|
||||
// Configs
|
||||
public static boolean isDarkTheme;
|
||||
public static int suRequestTimeout;
|
||||
public static int multiuserState = -1;
|
||||
public static int suResponseType;
|
||||
public static int suNotificationType;
|
||||
public static int updateChannel;
|
||||
public static int repoOrder;
|
||||
public static int suLogTimeout = 14;
|
||||
|
||||
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 exportPrefs() {
|
||||
// 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 importPrefs() {
|
||||
SuFile config = new SuFile("/data/user/0/" + Const.MANAGER_CONFIGS);
|
||||
if (config.exists()) {
|
||||
SharedPreferences.Editor editor = App.self.prefs.edit();
|
||||
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(Const.Key.ETAG_KEY);
|
||||
editor.apply();
|
||||
loadConfig();
|
||||
config.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadConfig() {
|
||||
App app = App.self;
|
||||
// su
|
||||
suRequestTimeout = Utils.getPrefsInt(app.prefs, Const.Key.SU_REQUEST_TIMEOUT, Const.Value.timeoutList[2]);
|
||||
suResponseType = Utils.getPrefsInt(app.prefs, Const.Key.SU_AUTO_RESPONSE, Const.Value.SU_PROMPT);
|
||||
suNotificationType = Utils.getPrefsInt(app.prefs, Const.Key.SU_NOTIFICATION, Const.Value.NOTIFICATION_TOAST);
|
||||
|
||||
// config
|
||||
isDarkTheme = app.prefs.getBoolean(Const.Key.DARK_THEME, false);
|
||||
updateChannel = Utils.getPrefsInt(app.prefs, Const.Key.UPDATE_CHANNEL, Const.Value.STABLE_CHANNEL);
|
||||
repoOrder = app.prefs.getInt(Const.Key.REPO_ORDER, Const.Value.ORDER_DATE);
|
||||
}
|
||||
|
||||
public static void writeConfig() {
|
||||
App.self.prefs.edit()
|
||||
.putBoolean(Const.Key.DARK_THEME, isDarkTheme)
|
||||
.putBoolean(Const.Key.MAGISKHIDE, magiskHide)
|
||||
.putBoolean(Const.Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
|
||||
.putString(Const.Key.SU_REQUEST_TIMEOUT, String.valueOf(suRequestTimeout))
|
||||
.putString(Const.Key.SU_AUTO_RESPONSE, String.valueOf(suResponseType))
|
||||
.putString(Const.Key.SU_NOTIFICATION, String.valueOf(suNotificationType))
|
||||
.putString(Const.Key.UPDATE_CHANNEL, String.valueOf(updateChannel))
|
||||
.putInt(Const.Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
|
||||
.putInt(Const.Key.REPO_ORDER, repoOrder)
|
||||
.apply();
|
||||
}
|
||||
}
|
127
core/src/main/java/com/topjohnwu/core/container/BaseModule.java
Normal file
127
core/src/main/java/com/topjohnwu/core/container/BaseModule.java
Normal file
@ -0,0 +1,127 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseModule implements Comparable<BaseModule> {
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull BaseModule module) {
|
||||
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
}
|
||||
}
|
63
core/src/main/java/com/topjohnwu/core/container/Module.java
Normal file
63
core/src/main/java/com/topjohnwu/core/container/Module.java
Normal file
@ -0,0 +1,63 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
}
|
61
core/src/main/java/com/topjohnwu/core/container/Policy.java
Normal file
61
core/src/main/java/com/topjohnwu/core/container/Policy.java
Normal file
@ -0,0 +1,61 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.topjohnwu.core.utils.Utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
83
core/src/main/java/com/topjohnwu/core/container/Repo.java
Normal file
83
core/src/main/java/com/topjohnwu/core/container/Repo.java
Normal file
@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.utils.Logger;
|
||||
import com.topjohnwu.core.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 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.topjohnwu.core.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.topjohnwu.core.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.topjohnwu.core.container;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
186
core/src/main/java/com/topjohnwu/core/database/MagiskDB.java
Normal file
186
core/src/main/java/com/topjohnwu/core/database/MagiskDB.java
Normal file
@ -0,0 +1,186 @@
|
||||
package com.topjohnwu.core.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.container.Policy;
|
||||
import com.topjohnwu.core.container.SuLogEntry;
|
||||
import com.topjohnwu.core.utils.LocaleManager;
|
||||
import com.topjohnwu.core.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 = "err";
|
||||
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() - Data.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");
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
package com.topjohnwu.core.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.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;
|
||||
private Runnable adapterCb;
|
||||
|
||||
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))");
|
||||
App.self.prefs.edit().remove(Const.Key.ETAG_KEY).apply();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
onUpgrade(db, 0, DATABASE_VER);
|
||||
}
|
||||
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
notifyAdapter();
|
||||
}
|
||||
|
||||
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
|
||||
notifyAdapter();
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
notifyAdapter();
|
||||
}
|
||||
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
notifyAdapter();
|
||||
}
|
||||
|
||||
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 (Data.repoOrder) {
|
||||
case Const.Value.ORDER_NAME:
|
||||
orderBy = "name COLLATE NOCASE";
|
||||
break;
|
||||
case Const.Value.ORDER_DATE:
|
||||
orderBy = "last_update DESC";
|
||||
}
|
||||
return mDb.query(TABLE_NAME, null, "minMagisk<=? AND minMagisk>=?",
|
||||
new String[] { String.valueOf(Data.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;
|
||||
}
|
||||
|
||||
public void registerAdapterCallback(Runnable cb) {
|
||||
adapterCb = cb;
|
||||
}
|
||||
|
||||
public void unregisterAdapterCallback() {
|
||||
adapterCb = null;
|
||||
}
|
||||
|
||||
private void notifyAdapter() {
|
||||
if (adapterCb != null) {
|
||||
App.mainHandler.post(adapterCb);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.utils.Topic;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.net.ResponseListener;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CheckUpdates {
|
||||
|
||||
private static int getInt(JSONObject json, String name, int defValue) {
|
||||
if (json == null)
|
||||
return defValue;
|
||||
try {
|
||||
return json.getInt(name);
|
||||
} catch (JSONException e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getString(JSONObject json, String name, String defValue) {
|
||||
if (json == null)
|
||||
return defValue;
|
||||
try {
|
||||
return json.getString(name);
|
||||
} catch (JSONException e) {
|
||||
return defValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject getJson(JSONObject json, String name) {
|
||||
try {
|
||||
return json.getJSONObject(name);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void check(Runnable cb) {
|
||||
String url;
|
||||
switch (Data.updateChannel) {
|
||||
case Const.Value.STABLE_CHANNEL:
|
||||
url = Const.Url.STABLE_URL;
|
||||
break;
|
||||
case Const.Value.BETA_CHANNEL:
|
||||
url = Const.Url.BETA_URL;
|
||||
break;
|
||||
case Const.Value.CUSTOM_CHANNEL:
|
||||
url = App.self.prefs.getString(Const.Key.CUSTOM_CHANNEL, "");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
Networking.get(url).getAsJSONObject(new UpdateListener(cb));
|
||||
}
|
||||
|
||||
public static void check() {
|
||||
check(null);
|
||||
}
|
||||
|
||||
private static class UpdateListener implements ResponseListener<JSONObject> {
|
||||
|
||||
private Runnable cb;
|
||||
|
||||
UpdateListener(Runnable callback) {
|
||||
cb = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(JSONObject json) {
|
||||
JSONObject magisk = getJson(json, "magisk");
|
||||
Data.remoteMagiskVersionString = getString(magisk, "version", null);
|
||||
Data.remoteMagiskVersionCode = getInt(magisk, "versionCode", -1);
|
||||
Data.magiskLink = getString(magisk, "link", null);
|
||||
Data.magiskNoteLink = getString(magisk, "note", null);
|
||||
Data.magiskMD5 = getString(magisk, "md5", null);
|
||||
|
||||
JSONObject manager = getJson(json, "app");
|
||||
Data.remoteManagerVersionString = getString(manager, "version", null);
|
||||
Data.remoteManagerVersionCode = getInt(manager, "versionCode", -1);
|
||||
Data.managerLink = getString(manager, "link", null);
|
||||
Data.managerNoteLink = getString(manager, "note", null);
|
||||
|
||||
JSONObject uninstaller = getJson(json, "uninstaller");
|
||||
Data.uninstallerLink = getString(uninstaller, "link", null);
|
||||
|
||||
Topic.publish(Topic.UPDATE_CHECK_DONE);
|
||||
|
||||
if (cb != null)
|
||||
cb.run();
|
||||
}
|
||||
}
|
||||
}
|
83
core/src/main/java/com/topjohnwu/core/tasks/FlashZip.java
Normal file
83
core/src/main/java/com/topjohnwu/core/tasks/FlashZip.java
Normal file
@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.utils.Utils;
|
||||
import com.topjohnwu.core.utils.ZipUtils;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
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 ShellUtils.fastCmdResult("grep -q '#MAGISK' " + new File(tmpFile.getParentFile(), "updater-script"));
|
||||
}
|
||||
|
||||
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() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
boolean success = false;
|
||||
try {
|
||||
success = flash();
|
||||
} catch (IOException ignored) {}
|
||||
Shell.su("cd /", "rm -rf " + tmpFile.getParent() + " " + Const.TMP_FOLDER_PATH).submit();
|
||||
onResult(success);
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void onResult(boolean success);
|
||||
}
|
288
core/src/main/java/com/topjohnwu/core/tasks/MagiskInstaller.java
Normal file
288
core/src/main/java/com/topjohnwu/core/tasks/MagiskInstaller.java
Normal file
@ -0,0 +1,288 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.R;
|
||||
import com.topjohnwu.core.container.TarEntry;
|
||||
import com.topjohnwu.core.utils.Utils;
|
||||
import com.topjohnwu.net.DownloadProgressListener;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.internal.NOPList;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream;
|
||||
import com.topjohnwu.utils.SignBoot;
|
||||
|
||||
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;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
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 extractZip() {
|
||||
String arch;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
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.getFilesDir(), "magisk.zip");
|
||||
|
||||
if (!ShellUtils.checkSum("MD5", zip, Data.magiskMD5)) {
|
||||
console.add("- Downloading zip");
|
||||
Networking.get(Data.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;
|
||||
}
|
||||
Shell.sh(Utils.fmt("chmod -R 755 %s/*; %s/magiskinit -x magisk %s/magisk",
|
||||
installDir, installDir, installDir)).exec();
|
||||
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;
|
||||
}
|
||||
|
||||
// Patch boot image
|
||||
if (!Shell.sh(Utils.fmt("cd %s; KEEPFORCEENCRYPT=%b KEEPVERITY=%b " +
|
||||
"sh update-binary indep boot_patch.sh %s",
|
||||
installDir, Data.keepEnc, Data.keepVerity, srcBoot))
|
||||
.to(console, logs).exec().isSuccess())
|
||||
return false;
|
||||
|
||||
Shell.Job job = Shell.sh("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 (!Data.keepVerity)
|
||||
Shell.su("find_dtbo_image", "patch_dtbo_image").to(console, logs).exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean storeBoot() {
|
||||
File patched = new File(installDir, "new-boot.img");
|
||||
String fmt = App.self.prefs.getString(Const.Key.BOOT_FORMAT, ".img");
|
||||
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(Const.MAGISK_PATH + "/.core/bootctl");
|
||||
try (InputStream in = App.self.getResources().openRawResource(R.raw.bootctl);
|
||||
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() {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
boolean b = operations();
|
||||
App.mainHandler.post(() -> onResult(b));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public abstract class ParallelTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
||||
|
||||
private WeakReference<Activity> weakActivity;
|
||||
|
||||
public ParallelTask() {}
|
||||
|
||||
public ParallelTask(Activity context) {
|
||||
weakActivity = new WeakReference<>(context);
|
||||
}
|
||||
|
||||
protected Activity getActivity() {
|
||||
return weakActivity.get();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void exec(Params... params) {
|
||||
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
|
||||
}
|
||||
}
|
51
core/src/main/java/com/topjohnwu/core/tasks/SafetyNet.java
Normal file
51
core/src/main/java/com/topjohnwu/core/tasks/SafetyNet.java
Normal file
@ -0,0 +1,51 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.utils.ISafetyNetHelper;
|
||||
import com.topjohnwu.core.utils.Topic;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import dalvik.system.DexClassLoader;
|
||||
|
||||
public class SafetyNet {
|
||||
|
||||
public static final File EXT_APK =
|
||||
new File(App.self.getFilesDir().getParent() + "/snet", "snet.apk");
|
||||
|
||||
private static void dyRun(Activity activity) throws Exception {
|
||||
DexClassLoader loader = new DexClassLoader(EXT_APK.getPath(), EXT_APK.getParent(),
|
||||
null, ISafetyNetHelper.class.getClassLoader());
|
||||
Class<?> clazz = loader.loadClass("com.topjohnwu.snet.Snet");
|
||||
ISafetyNetHelper helper = (ISafetyNetHelper) clazz.getMethod("newHelper",
|
||||
Class.class, String.class, Activity.class, Object.class)
|
||||
.invoke(null, ISafetyNetHelper.class, EXT_APK.getPath(), activity,
|
||||
(ISafetyNetHelper.Callback) code ->
|
||||
Topic.publish(false, Topic.SNET_CHECK_DONE, code));
|
||||
if (helper.getVersion() < Const.SNET_EXT_VER)
|
||||
throw new Exception();
|
||||
helper.attest();
|
||||
}
|
||||
|
||||
public static void check(Activity activity) {
|
||||
try {
|
||||
dyRun(activity);
|
||||
} catch (Exception ignored) {
|
||||
Shell.sh("rm -rf " + EXT_APK.getParent()).exec();
|
||||
EXT_APK.getParentFile().mkdir();
|
||||
Networking.get(Const.Url.SNET_URL).getAsFile(f -> {
|
||||
try {
|
||||
dyRun(activity);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
Topic.publish(false, Topic.SNET_CHECK_DONE, -1);
|
||||
}
|
||||
}, EXT_APK);
|
||||
}
|
||||
}
|
||||
}
|
162
core/src/main/java/com/topjohnwu/core/tasks/UpdateRepos.java
Normal file
162
core/src/main/java/com/topjohnwu/core/tasks/UpdateRepos.java
Normal file
@ -0,0 +1,162 @@
|
||||
package com.topjohnwu.core.tasks;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.container.Repo;
|
||||
import com.topjohnwu.core.utils.Logger;
|
||||
import com.topjohnwu.core.utils.Topic;
|
||||
import com.topjohnwu.core.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.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class UpdateRepos {
|
||||
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
|
||||
private static final int CORE_POOL_SIZE = Math.max(2, CPU_COUNT - 1);
|
||||
private static final DateFormat dateFormat;
|
||||
|
||||
private App app = App.self;
|
||||
private Set<String> cached;
|
||||
private ExecutorService threadPool;
|
||||
|
||||
static {
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private void waitTasks() {
|
||||
threadPool.shutdown();
|
||||
while (true) {
|
||||
try {
|
||||
if (threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS))
|
||||
break;
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadJSON(JSONArray array) throws JSONException, ParseException {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
JSONObject rawRepo = array.getJSONObject(i);
|
||||
String id = rawRepo.getString("name");
|
||||
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
|
||||
threadPool.execute(() -> {
|
||||
Repo repo = app.repoDB.getRepo(id);
|
||||
try {
|
||||
if (repo == null)
|
||||
repo = new Repo(id);
|
||||
else
|
||||
cached.remove(id);
|
||||
repo.update(date);
|
||||
app.repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.repoDB.removeRepo(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* 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 loadPage(int page) {
|
||||
Request req = Networking.get(Utils.fmt(Const.Url.REPO_URL, page + 1));
|
||||
if (page == 0) {
|
||||
String etag = app.prefs.getString(Const.Key.ETAG_KEY, null);
|
||||
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 {
|
||||
loadJSON(res.getResult());
|
||||
} 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(Const.Key.ETAG_KEY);
|
||||
if (etag != null) {
|
||||
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
|
||||
app.prefs.edit().putString(Const.Key.ETAG_KEY, etag).apply();
|
||||
}
|
||||
}
|
||||
|
||||
String links = res.getConnection().getHeaderField(Const.Key.LINK_KEY);
|
||||
return links == null || !links.contains("next") || loadPage(page + 1);
|
||||
}
|
||||
|
||||
private boolean loadPages() {
|
||||
return loadPage(0);
|
||||
}
|
||||
|
||||
private void fullReload() {
|
||||
Cursor c = app.repoDB.getRawCursor();
|
||||
while (c.moveToNext()) {
|
||||
Repo repo = new Repo(c);
|
||||
threadPool.execute(() -> {
|
||||
try {
|
||||
repo.update();
|
||||
app.repoDB.addRepo(repo);
|
||||
} catch (Repo.IllegalRepoException e) {
|
||||
Logger.debug(e.getMessage());
|
||||
app.repoDB.removeRepo(repo);
|
||||
}
|
||||
});
|
||||
}
|
||||
waitTasks();
|
||||
}
|
||||
|
||||
public void exec(boolean force) {
|
||||
Topic.reset(Topic.REPO_LOAD_DONE);
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
cached = Collections.synchronizedSet(app.repoDB.getRepoIDSet());
|
||||
threadPool = Executors.newFixedThreadPool(CORE_POOL_SIZE);
|
||||
|
||||
if (loadPages()) {
|
||||
waitTasks();
|
||||
// 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);
|
||||
}
|
||||
}
|
44
core/src/main/java/com/topjohnwu/core/utils/BootSigner.java
Normal file
44
core/src/main/java/com/topjohnwu/core/utils/BootSigner.java
Normal file
@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import com.topjohnwu.utils.SignBoot;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class BootSigner {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
||||
String certPath = "";
|
||||
if (args.length >= 2) {
|
||||
/* args[1] is the path to a public key certificate */
|
||||
certPath = args[1];
|
||||
}
|
||||
boolean signed = SignBoot.verifySignature(System.in,
|
||||
certPath.isEmpty() ? null : new FileInputStream(certPath));
|
||||
System.exit(signed ? 0 : 1);
|
||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
||||
InputStream cert = null;
|
||||
InputStream key = null;
|
||||
|
||||
if (args.length >= 3) {
|
||||
cert = new FileInputStream(args[1]);
|
||||
key = new FileInputStream(args[2]);
|
||||
}
|
||||
|
||||
boolean success = SignBoot.doSignature("/boot", System.in, System.out, cert, key);
|
||||
System.exit(success ? 0 : 1);
|
||||
} else {
|
||||
System.err.println(
|
||||
"BootSigner <actions> [args]\n" +
|
||||
"Input from stdin, outputs to stdout\n" +
|
||||
"\n" +
|
||||
"Actions:\n" +
|
||||
" -verify [x509.pem]\n" +
|
||||
" verify image, cert is optional\n" +
|
||||
" -sign [x509.pem] [pk8]\n" +
|
||||
" sign image, cert and key pair is optional\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.topjohnwu.core.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public class LocaleManager {
|
||||
public static Locale locale = Locale.getDefault();
|
||||
public final static Locale defaultLocale = Locale.getDefault();
|
||||
public static List<Locale> locales;
|
||||
|
||||
public static void setLocale(App app) {
|
||||
String localeConfig = app.prefs.getString(Const.Key.LOCALE, "");
|
||||
if (localeConfig.isEmpty()) {
|
||||
locale = defaultLocale;
|
||||
} else {
|
||||
locale = Locale.forLanguageTag(localeConfig);
|
||||
}
|
||||
Locale.setDefault(locale);
|
||||
Resources res = app.getResources();
|
||||
Configuration config = res.getConfiguration();
|
||||
config.setLocale(locale);
|
||||
res.updateConfiguration(config, res.getDisplayMetrics());
|
||||
}
|
||||
|
||||
public static String getString(Locale locale, @StringRes int id) {
|
||||
Configuration config = new Configuration();
|
||||
config.setLocale(locale);
|
||||
return App.self.createConfigurationContext(config).getString(id);
|
||||
}
|
||||
|
||||
public static void loadAvailableLocales(@StringRes int compareId) {
|
||||
AsyncTask.THREAD_POOL_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 = 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);
|
||||
});
|
||||
}
|
||||
}
|
26
core/src/main/java/com/topjohnwu/core/utils/Logger.java
Normal file
26
core/src/main/java/com/topjohnwu/core/utils/Logger.java
Normal file
@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.topjohnwu.core.BuildConfig;
|
||||
import com.topjohnwu.core.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));
|
||||
}
|
||||
}
|
47
core/src/main/java/com/topjohnwu/core/utils/RootUtils.java
Normal file
47
core/src/main/java/com/topjohnwu/core/utils/RootUtils.java
Normal file
@ -0,0 +1,47 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.R;
|
||||
import com.topjohnwu.superuser.BusyBox;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class RootUtils extends Shell.Initializer {
|
||||
|
||||
static {
|
||||
BusyBox.BB_PATH = new File(Const.BUSYBOX_PATH);
|
||||
}
|
||||
|
||||
public static void rmAndLaunch(String rm, String launch) {
|
||||
Shell.su(Utils.fmt("(rm_launch %d %s %s)&", Const.USER_ID, 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");
|
||||
Data.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();
|
||||
|
||||
Data.keepVerity = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPVERITY"));
|
||||
Data.keepEnc = Boolean.parseBoolean(ShellUtils.fastCmd("echo $KEEPFORCEENCRYPT"));
|
||||
return true;
|
||||
}
|
||||
}
|
107
core/src/main/java/com/topjohnwu/core/utils/Topic.java
Normal file
107
core/src/main/java/com/topjohnwu/core/utils/Topic.java
Normal file
@ -0,0 +1,107 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.IntDef;
|
||||
|
||||
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 SNET_CHECK_DONE = 4;
|
||||
public static final int LOCALE_FETCH_DONE = 5;
|
||||
|
||||
@IntDef({MAGISK_HIDE_DONE, MODULE_LOAD_DONE, REPO_LOAD_DONE,
|
||||
UPDATE_CHECK_DONE, SNET_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[6];
|
||||
|
||||
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) {
|
||||
if (sub instanceof Subscriber)
|
||||
subscribe((Subscriber) 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) {
|
||||
if (sub instanceof Subscriber)
|
||||
unsubscribe((Subscriber) 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) {
|
||||
App.mainHandler.post(() -> 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
@TopicID
|
||||
int[] getSubscribedTopics();
|
||||
}
|
||||
}
|
125
core/src/main/java/com/topjohnwu/core/utils/Utils.java
Normal file
125
core/src/main/java/com/topjohnwu/core/utils/Utils.java
Normal file
@ -0,0 +1,125 @@
|
||||
package com.topjohnwu.core.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.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.topjohnwu.core.App;
|
||||
import com.topjohnwu.core.Const;
|
||||
import com.topjohnwu.core.Data;
|
||||
import com.topjohnwu.core.container.Module;
|
||||
import com.topjohnwu.core.container.ValueSortedMap;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
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) {
|
||||
App.mainHandler.post(() -> Toast.makeText(App.self, msg, duration).show());
|
||||
}
|
||||
|
||||
public static void toast(int resId, int duration) {
|
||||
App.mainHandler.post(() -> 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);
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.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() {
|
||||
if (Data.multiuserState < 0)
|
||||
Data.multiuserState = App.self.mDB.getSettings(Const.Key.SU_MULTIUSER_MODE,
|
||||
Const.Value.MULTIUSER_MODE_OWNER_ONLY);
|
||||
return Shell.rootAccess() && (Const.USER_ID == 0 ||
|
||||
Data.multiuserState != Const.Value.MULTIUSER_MODE_OWNER_MANAGED);
|
||||
}
|
||||
|
||||
public static Context getDEContext() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?
|
||||
App.self.createDeviceProtectedStorageContext() : App.self;
|
||||
}
|
||||
}
|
65
core/src/main/java/com/topjohnwu/core/utils/ZipUtils.java
Normal file
65
core/src/main/java/com/topjohnwu/core/utils/ZipUtils.java
Normal file
@ -0,0 +1,65 @@
|
||||
package com.topjohnwu.core.utils;
|
||||
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream;
|
||||
import com.topjohnwu.utils.JarMap;
|
||||
import com.topjohnwu.utils.SignAPK;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
BIN
core/src/main/res/raw/bootctl
Normal file
BIN
core/src/main/res/raw/bootctl
Normal file
Binary file not shown.
13
core/src/main/res/raw/nonroot_utils.sh
Normal file
13
core/src/main/res/raw/nonroot_utils.sh
Normal file
@ -0,0 +1,13 @@
|
||||
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
|
||||
}
|
||||
|
||||
run_migrations() {
|
||||
# NOP
|
||||
}
|
105
core/src/main/res/raw/utils.sh
Normal file
105
core/src/main/res/raw/utils.sh
Normal file
@ -0,0 +1,105 @@
|
||||
env_check() {
|
||||
for file in busybox magisk magiskboot magiskinit util_functions.sh boot_patch.sh; do
|
||||
[ -f /data/adb/magisk/$file ] || return 1
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
fix_env() {
|
||||
cd /data/adb/magisk
|
||||
sh update-binary extract
|
||||
rm -f update-binary magisk.apk
|
||||
cd /
|
||||
rm -rf /sbin/.magisk/busybox/*
|
||||
/sbin/.magisk/mirror/bin/busybox --install -s /sbin/.magisk/busybox
|
||||
}
|
||||
|
||||
direct_install() {
|
||||
rm -rf /data/adb/magisk/* 2>/dev/null
|
||||
mkdir -p /data/adb/magisk 2>/dev/null
|
||||
chmod 700 /data/adb
|
||||
cp -rf $1/* /data/adb/magisk
|
||||
rm -rf /data/adb/magisk/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() {
|
||||
if $KEEPVERITY; then
|
||||
return 1
|
||||
else
|
||||
find_dtbo_image
|
||||
patch_dtbo_image
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
../../../../../app/src/full/res/raw/bootctl hal-info || return
|
||||
[ `../../../../../app/src/full/res/raw/bootctl get-current-slot` -eq 0 ] && SLOT_NUM=1 || SLOT_NUM=0
|
||||
../../../../../app/src/full/res/raw/bootctl set-active-boot-slot $SLOT_NUM
|
||||
echo '${0%/*}/../bootctl mark-boot-successful;rm -f ${0%/*}/../bootctl $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() {
|
||||
db_clean $1
|
||||
pm uninstall $2
|
||||
am start -n ${3}/a.c
|
||||
exit
|
||||
}
|
Reference in New Issue
Block a user