From 41a644afb91d956f7492b45fe484a21d27b07a20 Mon Sep 17 00:00:00 2001 From: topjohnwu Date: Tue, 29 Dec 2020 01:44:02 -0800 Subject: [PATCH] Open source stub APK loader Close #3537 --- stub/build.gradle.kts | 3 +- stub/src/main/AndroidManifest.xml | 65 ++++++++++++++- stub/src/main/java/a/c.java | 6 ++ stub/src/main/java/a/e.java | 6 ++ stub/src/main/java/a/h.java | 6 ++ stub/src/main/java/a/p.java | 6 ++ .../topjohnwu/magisk/DelegateApplication.java | 41 ++++++++++ .../magisk/DelegateComponentFactory.java | 79 +++++++++++++++++++ ...ainActivity.java => DownloadActivity.java} | 28 +++++-- .../java/com/topjohnwu/magisk/InjectAPK.java | 78 ++++++++++++++++++ .../topjohnwu/magisk/dummy/DummyActivity.java | 13 +++ .../topjohnwu/magisk/dummy/DummyProvider.java | 38 +++++++++ .../topjohnwu/magisk/dummy/DummyReceiver.java | 10 +++ .../topjohnwu/magisk/dummy/DummyService.java | 13 +++ 14 files changed, 381 insertions(+), 11 deletions(-) create mode 100644 stub/src/main/java/a/c.java create mode 100644 stub/src/main/java/a/e.java create mode 100644 stub/src/main/java/a/h.java create mode 100644 stub/src/main/java/a/p.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/DelegateApplication.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/DelegateComponentFactory.java rename stub/src/main/java/com/topjohnwu/magisk/{MainActivity.java => DownloadActivity.java} (80%) create mode 100644 stub/src/main/java/com/topjohnwu/magisk/InjectAPK.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyActivity.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyProvider.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyReceiver.java create mode 100644 stub/src/main/java/com/topjohnwu/magisk/dummy/DummyService.java 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; + } +}