Open source stub APK loader

Close #3537
This commit is contained in:
topjohnwu 2020-12-29 01:44:02 -08:00
parent 6b42db943d
commit 41a644afb9
14 changed files with 381 additions and 11 deletions

View File

@ -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")
}

View File

@ -5,25 +5,82 @@
package="com.topjohnwu.magisk">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<application
android:allowBackup="true"
tools:ignore="AllowBackup">
android:appComponentFactory=".DelegateComponentFactory"
android:name="a.e"
android:allowBackup="false"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning" >
<activity android:name=".MainActivity">
<!-- Splash -->
<activity
android:name="a.c">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Main -->
<activity android:name="a.b" />
<!-- Superuser -->
<activity
android:name="a.m"
android:directBootAware="true"
android:excludeFromRecents="true"
android:exported="false"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!-- Receiver -->
<receiver
android:name="a.h"
android:directBootAware="true">
<intent-filter>
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<!-- DownloadService -->
<service android:name="a.j" />
<!-- FileProvider -->
<provider
android:name=".FileProvider"
android:name="a.p"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
android:exported="false"
android:grantUriPermissions="true">
</provider>
<!-- Hardcode GMS version -->
<meta-data
android:name="com.google.android.gms.version"
android:value="12451000" />
</application>
</manifest>

View File

@ -0,0 +1,6 @@
package a;
import com.topjohnwu.magisk.DownloadActivity;
public class c extends DownloadActivity {
}

View File

@ -0,0 +1,6 @@
package a;
import com.topjohnwu.magisk.DelegateApplication;
public class e extends DelegateApplication {
}

View File

@ -0,0 +1,6 @@
package a;
import com.topjohnwu.magisk.dummy.DummyReceiver;
public class h extends DummyReceiver {
}

View File

@ -0,0 +1,6 @@
package a;
import com.topjohnwu.magisk.FileProvider;
public class p extends FileProvider {
}

View File

@ -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);
}
}

View File

@ -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> {
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> T create(String name, DummyFactory<T> factory) {
try {
return (T) loader.loadClass(name).newInstance();
} catch (Exception ignored) {
return factory.create();
}
}
}

View File

@ -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();
});
}
});
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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) {}
}

View File

@ -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;
}
}