diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts
index 34bce5740..16e59cabe 100644
--- a/stub/build.gradle.kts
+++ b/stub/build.gradle.kts
@@ -3,12 +3,13 @@ plugins {
}
android {
- val canary = !Config["appVersion"].orEmpty().contains(".")
+ val canary = !Config.appVersion.contains(".")
defaultConfig {
applicationId = "com.topjohnwu.magisk"
versionCode = 1
versionName = Config.appVersion
+ buildConfigField("int", "STUB_VERSION", "15")
buildConfigField("String", "DEV_CHANNEL", Config["DEV_CHANNEL"] ?: "null")
buildConfigField("boolean", "CANARY", if (canary) "true" else "false")
}
diff --git a/stub/src/main/AndroidManifest.xml b/stub/src/main/AndroidManifest.xml
index 736e4b7b2..02339a47f 100644
--- a/stub/src/main/AndroidManifest.xml
+++ b/stub/src/main/AndroidManifest.xml
@@ -5,25 +5,82 @@
package="com.topjohnwu.magisk">
+
+
+
+
+
+ android:appComponentFactory=".DelegateComponentFactory"
+ android:name="a.e"
+ android:allowBackup="false"
+ tools:ignore="UnusedAttribute,GoogleAppIndexingWarning" >
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/stub/src/main/java/a/c.java b/stub/src/main/java/a/c.java
new file mode 100644
index 000000000..44b3521e1
--- /dev/null
+++ b/stub/src/main/java/a/c.java
@@ -0,0 +1,6 @@
+package a;
+
+import com.topjohnwu.magisk.DownloadActivity;
+
+public class c extends DownloadActivity {
+}
diff --git a/stub/src/main/java/a/e.java b/stub/src/main/java/a/e.java
new file mode 100644
index 000000000..cd2e45a4c
--- /dev/null
+++ b/stub/src/main/java/a/e.java
@@ -0,0 +1,6 @@
+package a;
+
+import com.topjohnwu.magisk.DelegateApplication;
+
+public class e extends DelegateApplication {
+}
diff --git a/stub/src/main/java/a/h.java b/stub/src/main/java/a/h.java
new file mode 100644
index 000000000..51120d19c
--- /dev/null
+++ b/stub/src/main/java/a/h.java
@@ -0,0 +1,6 @@
+package a;
+
+import com.topjohnwu.magisk.dummy.DummyReceiver;
+
+public class h extends DummyReceiver {
+}
diff --git a/stub/src/main/java/a/p.java b/stub/src/main/java/a/p.java
new file mode 100644
index 000000000..6d7efc971
--- /dev/null
+++ b/stub/src/main/java/a/p.java
@@ -0,0 +1,6 @@
+package a;
+
+import com.topjohnwu.magisk.FileProvider;
+
+public class p extends FileProvider {
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java
new file mode 100644
index 000000000..b9d550e8a
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java
@@ -0,0 +1,41 @@
+package com.topjohnwu.magisk;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.Configuration;
+import android.os.Build;
+
+import java.lang.reflect.Method;
+
+public class DelegateApplication extends Application {
+
+ private Application delegate;
+ static boolean dynLoad = false;
+
+ @Override
+ protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+
+ // Only dynamic load full APK if hidden and possible
+ dynLoad = Build.VERSION.SDK_INT >= 28 &&
+ !base.getPackageName().equals(BuildConfig.APPLICATION_ID);
+ if (!dynLoad)
+ return;
+
+ delegate = InjectAPK.setup(this);
+ if (delegate != null) try {
+ // Call attachBaseContext without ContextImpl to show it is being wrapped
+ Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
+ m.setAccessible(true);
+ m.invoke(delegate, this);
+ } catch (Exception ignored) { /* Impossible */ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (delegate != null)
+ delegate.onConfigurationChanged(newConfig);
+ }
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java
new file mode 100644
index 000000000..7ece78d46
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java
@@ -0,0 +1,79 @@
+package com.topjohnwu.magisk;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AppComponentFactory;
+import android.app.Application;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Intent;
+
+import com.topjohnwu.magisk.dummy.DummyProvider;
+import com.topjohnwu.magisk.dummy.DummyReceiver;
+import com.topjohnwu.magisk.dummy.DummyService;
+
+@SuppressLint("NewApi")
+public class DelegateComponentFactory extends AppComponentFactory {
+
+ ClassLoader loader;
+ AppComponentFactory delegate;
+
+ interface DummyFactory {
+ T create();
+ }
+
+ public DelegateComponentFactory() {
+ InjectAPK.factory = this;
+ }
+
+ @Override
+ public Application instantiateApplication(ClassLoader cl, String className) {
+ if (loader == null) loader = cl;
+ return new DelegateApplication();
+ }
+
+ @Override
+ public Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
+ throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ if (delegate != null)
+ return delegate.instantiateActivity(loader, className, intent);
+ return create(className, DownloadActivity::new);
+ }
+
+ @Override
+ public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
+ throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ if (delegate != null)
+ return delegate.instantiateReceiver(loader, className, intent);
+ return create(className, DummyReceiver::new);
+ }
+
+ @Override
+ public Service instantiateService(ClassLoader cl, String className, Intent intent)
+ throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ if (delegate != null)
+ return delegate.instantiateService(loader, className, intent);
+ return create(className, DummyService::new);
+ }
+
+ @Override
+ public ContentProvider instantiateProvider(ClassLoader cl, String className)
+ throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+ if (loader == null) loader = cl;
+ if (delegate != null)
+ return delegate.instantiateProvider(loader, className);
+ return create(className, DummyProvider::new);
+ }
+
+ /**
+ * Create the class or dummy implementation if creation failed
+ */
+ private T create(String name, DummyFactory factory) {
+ try {
+ return (T) loader.loadClass(name).newInstance();
+ } catch (Exception ignored) {
+ return factory.create();
+ }
+ }
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/MainActivity.java b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java
similarity index 80%
rename from stub/src/main/java/com/topjohnwu/magisk/MainActivity.java
rename to stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java
index 544129b38..de5421ab5 100644
--- a/stub/src/main/java/com/topjohnwu/magisk/MainActivity.java
+++ b/stub/src/main/java/com/topjohnwu/magisk/DownloadActivity.java
@@ -4,9 +4,11 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextThemeWrapper;
+import android.widget.Toast;
import com.topjohnwu.magisk.net.Networking;
import com.topjohnwu.magisk.net.Request;
@@ -20,11 +22,13 @@ import java.io.File;
import static android.R.string.no;
import static android.R.string.ok;
import static android.R.string.yes;
+import static com.topjohnwu.magisk.DelegateApplication.dynLoad;
import static com.topjohnwu.magisk.R.string.dling;
import static com.topjohnwu.magisk.R.string.no_internet_msg;
+import static com.topjohnwu.magisk.R.string.relaunch_app;
import static com.topjohnwu.magisk.R.string.upgrade_msg;
-public class MainActivity extends Activity {
+public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk Manager";
private static final String CDN_URL = "https://cdn.jsdelivr.net/gh/topjohnwu/magisk_files@%s/%s";
@@ -107,11 +111,23 @@ public class MainActivity extends Activity {
private void dlAPK() {
dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
// Download and upgrade the app
- File apk = new File(getCacheDir(), "manager.apk");
- request(apkLink).getAsFile(apk, file -> {
- dialog.dismiss();
- APKInstall.install(this, file);
- finish();
+ File apk = dynLoad ? DynAPK.current(this) : new File(getCacheDir(), "manager.apk");
+ request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR).getAsFile(apk, file -> {
+ if (dynLoad) {
+ InjectAPK.setup(this);
+ runOnUiThread(() -> {
+ dialog.dismiss();
+ Toast.makeText(themed, relaunch_app, Toast.LENGTH_LONG).show();
+ finish();
+ });
+ } else {
+ runOnUiThread(() -> {
+ dialog.dismiss();
+ APKInstall.install(this, file);
+ finish();
+ });
+ }
});
}
+
}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java b/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java
new file mode 100644
index 000000000..41ebbdea0
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java
@@ -0,0 +1,78 @@
+package com.topjohnwu.magisk;
+
+import android.app.AppComponentFactory;
+import android.app.Application;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.topjohnwu.magisk.utils.DynamicClassLoader;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+
+public class InjectAPK {
+
+ static DelegateComponentFactory factory;
+
+ static Application setup(Context context) {
+ File apk = DynAPK.current(context);
+ File update = DynAPK.update(context);
+ if (update.exists())
+ update.renameTo(apk);
+ Application delegate = null;
+ if (!apk.exists()) {
+ // Try copying APK
+ Uri uri = new Uri.Builder().scheme("content")
+ .authority("com.topjohnwu.magisk.provider")
+ .encodedPath("apk_file").build();
+ ContentResolver resolver = context.getContentResolver();
+ try (InputStream is = resolver.openInputStream(uri)) {
+ if (is != null) {
+ try (OutputStream out = new FileOutputStream(apk)) {
+ byte[] buf = new byte[4096];
+ for (int read; (read = is.read(buf)) >= 0;) {
+ out.write(buf, 0, read);
+ }
+ }
+ }
+ } catch (Exception e) {
+ Log.e(InjectAPK.class.getSimpleName(), "", e);
+ }
+ }
+ if (apk.exists()) {
+ ClassLoader cl = new DynamicClassLoader(apk, factory.loader);
+ try {
+ // Create the delegate AppComponentFactory
+ AppComponentFactory df = (AppComponentFactory)
+ cl.loadClass("androidx.core.app.CoreComponentFactory").newInstance();
+
+ // Create the delegate Application
+ delegate = (Application) cl.loadClass("a.e").getConstructor(Object.class)
+ .newInstance(DynAPK.pack(dynData()));
+
+ // If everything went well, set our loader and delegate
+ factory.delegate = df;
+ factory.loader = cl;
+ } catch (Exception e) {
+ Log.e(InjectAPK.class.getSimpleName(), "", e);
+ apk.delete();
+ }
+ }
+ return delegate;
+ }
+
+ private static DynAPK.Data dynData() {
+ DynAPK.Data data = new DynAPK.Data();
+ data.version = BuildConfig.STUB_VERSION;
+ // Public source code does not do component name obfuscation
+ data.classToComponent = new HashMap<>();
+ return data;
+ }
+
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java
new file mode 100644
index 000000000..7a279ba55
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java
@@ -0,0 +1,13 @@
+package com.topjohnwu.magisk.dummy;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class DummyActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ finish();
+ }
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyProvider.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyProvider.java
new file mode 100644
index 000000000..0c407ff8c
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyProvider.java
@@ -0,0 +1,38 @@
+package com.topjohnwu.magisk.dummy;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+
+public class DummyProvider extends ContentProvider {
+ @Override
+ public boolean onCreate() {
+ return false;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyReceiver.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyReceiver.java
new file mode 100644
index 000000000..f4e57e7a7
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyReceiver.java
@@ -0,0 +1,10 @@
+package com.topjohnwu.magisk.dummy;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class DummyReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {}
+}
diff --git a/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java
new file mode 100644
index 000000000..6bce30183
--- /dev/null
+++ b/stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java
@@ -0,0 +1,13 @@
+package com.topjohnwu.magisk.dummy;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class DummyService extends Service {
+ @Override
+ public IBinder onBind(Intent intent) {
+ stopSelf();
+ return null;
+ }
+}