Make main app fully independent from the stub

- Skip 0x7f01XXXX - 0x7f05XXXX resource IDs in the main app; they are
reserved for stub resources
- Support sending additional data from host to guest
- Use resource mapping passed from host when they are being sent
to the system framework (notifications and shortcuts)
This commit is contained in:
topjohnwu 2019-10-17 02:04:22 -04:00
parent 9f9de8c43b
commit 40eda05a30
16 changed files with 161 additions and 133 deletions

1
.gitignore vendored
View File

@ -4,7 +4,6 @@ out
*.apk
/config.prop
/update.sh
/stub-ids.txt
# Built binaries
native/out

5
app/res-ids.txt Normal file
View File

@ -0,0 +1,5 @@
com.topjohnwu.magisk:color/xxxxxxxx = 0x7f010000
com.topjohnwu.magisk:drawable/xxxxxxxx = 0x7f020000
com.topjohnwu.magisk:string/xxxxxxxx = 0x7f030000
com.topjohnwu.magisk:style/xxxxxxxx = 0x7f040000
com.topjohnwu.magisk:xml/xxxxxxxx = 0x7f050000

View File

@ -2,14 +2,12 @@ package a;
import com.topjohnwu.magisk.App;
import java.util.Map;
public class e extends App {
public e() {
super();
}
public e(Map<String, String> map) {
super(map);
public e(Object o) {
super(o);
}
}

View File

@ -15,6 +15,7 @@ import com.topjohnwu.magisk.di.ActivityTracker
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.unwrap
import com.topjohnwu.magisk.utils.DynAPK
import com.topjohnwu.magisk.utils.RootInit
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
@ -23,9 +24,8 @@ import timber.log.Timber
open class App() : Application() {
constructor(map: Map<String, String>) : this() {
isRunningAsStub = true
ClassMap.componentMap = map
constructor(o: Any) : this() {
ClassMap.data = DynAPK.load(o)
}
init {
@ -53,7 +53,6 @@ open class App() : Application() {
val app: Application
val impl: Context
if (base is Application) {
isRunningAsStub = true
app = base
impl = base.baseContext
} else {

View File

@ -28,7 +28,7 @@ import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.magisk.utils.defaultLocale
import java.util.*
var isRunningAsStub = false
val isRunningAsStub get() = ClassMap.data != null
private val addAssetPath by lazy {
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
@ -39,10 +39,14 @@ fun AssetManager.addAssetPath(path: String) {
}
fun Context.wrap(global: Boolean = true): Context
= if (!global) ResContext(this) else GlobalResContext(this)
= if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
override fun getApplicationContext(): Context {
return this
}
@SuppressLint("NewApi")
override fun getSystemService(name: String): Any? {
return if (!isRunningAsStub) super.getSystemService(name) else
@ -65,26 +69,32 @@ private fun Resources.patch(config: Configuration = Configuration(configuration)
fun Class<*>.cmp(pkg: String = BuildConfig.APPLICATION_ID): ComponentName {
val name = ClassMap[this].name
return ComponentName(pkg, ClassMap.componentMap?.get(name)
?: name)
return ComponentName(pkg, ClassMap.data?.componentMap?.get(name) ?: name)
}
fun Context.intent(c: Class<*>): Intent {
val cls = ClassMap[c]
return ClassMap.componentMap?.let {
val className = it.getOrElse(cls.name) { cls.name }
return ClassMap.data?.let {
val className = it.componentMap.getOrElse(cls.name) { cls.name }
Intent().setComponent(ComponentName(this, className))
} ?: Intent(this, cls)
}
fun resolveRes(idx: Int): Int {
return ClassMap.data?.resourceMap?.get(idx) ?: when(idx) {
DynAPK.NOTIFICATION -> R.drawable.ic_magisk_outline
DynAPK.DOWNLOAD -> R.drawable.sc_cloud_download
DynAPK.SUPERUSER -> R.drawable.sc_superuser
DynAPK.MODULES -> R.drawable.sc_extension
DynAPK.MAGISKHIDE -> R.drawable.sc_magiskhide
else -> -1
}
}
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
open val mRes: Resources get() = ResourceMgr.resource
private val loader by lazy { javaClass.classLoader!! }
override fun getApplicationContext(): Context {
return this
}
override fun getResources(): Resources {
return mRes
}
@ -162,7 +172,7 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
// We need to patch the component of JobInfo to access WorkManager SystemJobService
val name = service.className
val component = ComponentName(service.packageName, ClassMap.componentMap?.get(name)
val component = ComponentName(service.packageName, ClassMap.data?.componentMap?.get(name)
?: name)
// Clone the JobInfo except component
@ -205,7 +215,7 @@ private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler
object ClassMap {
private val classMap = mapOf(
private val map = mapOf(
App::class.java to a.e::class.java,
MainActivity::class.java to a.b::class.java,
SplashActivity::class.java to a.c::class.java,
@ -216,8 +226,7 @@ object ClassMap {
SuRequestActivity::class.java to a.m::class.java
)
// This will be set if running as guest app
var componentMap: Map<String, String>? = null
internal var data: DynAPK.Data? = null
operator fun get(c: Class<*>) = classMap.getOrElse(c) { throw IllegalArgumentException() }
operator fun get(c: Class<*>) = map.getOrElse(c) { throw IllegalArgumentException() }
}

View File

@ -12,10 +12,12 @@ import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.DynAPK
object Notifications {
val mgr by lazy { NotificationManagerCompat.from(get()) }
private val icon by lazy { resolveRes(DynAPK.NOTIFICATION) }
fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -39,7 +41,7 @@ object Notifications {
PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(R.drawable.ic_magisk_outline)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.magisk_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setVibrate(longArrayOf(0, 100, 100, 100))
@ -58,7 +60,7 @@ object Notifications {
Const.ID.APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(R.drawable.ic_magisk_outline)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.manager_update_title))
.setContentText(context.getString(R.string.manager_download_install))
.setVibrate(longArrayOf(0, 100, 100, 100))
@ -75,7 +77,7 @@ object Notifications {
Const.ID.DTBO_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(context, Const.ID.UPDATE_NOTIFICATION_CHANNEL)
builder.setSmallIcon(R.drawable.ic_magisk_outline)
builder.setSmallIcon(icon)
.setContentTitle(context.getString(R.string.dtbo_patched_title))
.setContentText(context.getString(R.string.dtbo_patched_reboot))
.setVibrate(longArrayOf(0, 100, 100, 100))

View File

@ -9,19 +9,20 @@ import android.os.Build
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.DynAPK
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
object Shortcuts {
fun setup(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
if (Build.VERSION.SDK_INT >= 25) {
val manager = context.getSystemService(ShortcutManager::class.java)
manager?.dynamicShortcuts = getShortCuts(context)
}
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
@RequiresApi(api = 25)
private fun getShortCuts(context: Context): List<ShortcutInfo> {
val shortCuts = mutableListOf<ShortcutInfo>()
val root = Shell.rootAccess()
@ -33,7 +34,7 @@ object Shortcuts {
.putExtra(Const.Key.OPEN_SECTION, "superuser")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_superuser))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.SUPERUSER)))
.setRank(0)
.build())
}
@ -44,7 +45,7 @@ object Shortcuts {
.putExtra(Const.Key.OPEN_SECTION, "magiskhide")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_magiskhide))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MAGISKHIDE)))
.setRank(1)
.build())
}
@ -55,7 +56,7 @@ object Shortcuts {
.putExtra(Const.Key.OPEN_SECTION, "modules")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_extension))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.MODULES)))
.setRank(3)
.build())
shortCuts.add(ShortcutInfo.Builder(context, "downloads")
@ -64,7 +65,7 @@ object Shortcuts {
.putExtra(Const.Key.OPEN_SECTION, "downloads")
.setAction(Intent.ACTION_VIEW)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK))
.setIcon(Icon.createWithResource(context, R.drawable.sc_cloud_download))
.setIcon(Icon.createWithResource(context, resolveRes(DynAPK.DOWNLOAD)))
.setRank(2)
.build())
}

View File

@ -8,16 +8,4 @@
<string name="magiskhide" translatable="false">Magisk Hide</string>
<string name="empty" translatable="false"/>
<!-- Preserve 10 string slots for stub -->
<string name="stub_0" translatable="false"/>
<string name="stub_1" translatable="false"/>
<string name="stub_2" translatable="false"/>
<string name="stub_3" translatable="false"/>
<string name="stub_4" translatable="false"/>
<string name="stub_5" translatable="false"/>
<string name="stub_6" translatable="false"/>
<string name="stub_7" translatable="false"/>
<string name="stub_8" translatable="false"/>
<string name="stub_9" translatable="false"/>
</resources>

View File

@ -86,12 +86,11 @@ subprojects {
aaptOptions {
// Handle resource IDs
File resIds = rootProject.file('stable-ids.txt')
File stubIds = rootProject.file('stub-ids.txt')
if (module.name == 'app' && resIds.exists())
additionalParameters "--stable-ids", "${resIds.absolutePath}"
else if (module.name == 'stub')
additionalParameters "--emit-ids", "${stubIds.absolutePath}"
File resId = project.file('res-ids.txt')
if (resId.exists())
additionalParameters "--stable-ids", "${resId.absolutePath}"
else
additionalParameters "--emit-ids", "${resId.absolutePath}"
}
}
}

View File

@ -3,9 +3,21 @@ package com.topjohnwu.magisk.utils;
import android.content.Context;
import java.io.File;
import java.util.Map;
public class DynAPK {
// Indices of the object array
private static final int COMPONENT_MAP = 0;
private static final int RESOURCE_MAP = 1;
// Indices of the resource map
public static final int NOTIFICATION = 0;
public static final int DOWNLOAD = 1;
public static final int SUPERUSER = 2;
public static final int MODULES = 3;
public static final int MAGISKHIDE = 4;
private static File dynDir;
private static File getDynDir(Context c) {
@ -23,4 +35,24 @@ public class DynAPK {
public static File update(Context c) {
return new File(getDynDir(c), "update.apk");
}
public static Data load(Object o) {
Object[] arr = (Object[]) o;
Data data = new Data();
data.componentMap = (Map<String, String>) arr[COMPONENT_MAP];
data.resourceMap = (int[]) arr[RESOURCE_MAP];
return data;
}
public static Object pack(Data data) {
Object[] arr = new Object[2];
arr[COMPONENT_MAP] = data.componentMap;
arr[RESOURCE_MAP] = data.resourceMap;
return arr;
}
public static class Data {
public Map<String, String> componentMap;
public int[] resourceMap;
}
}

View File

@ -1,32 +0,0 @@
com.topjohnwu.magisk:color/dark = 0x7f010000
com.topjohnwu.magisk:color/ic_launcher_background = 0x7f010001
com.topjohnwu.magisk:color/light = 0x7f010002
com.topjohnwu.magisk:drawable/ic_cloud_download = 0x7f020000
com.topjohnwu.magisk:drawable/ic_extension = 0x7f020001
com.topjohnwu.magisk:drawable/ic_launcher = 0x7f020002
com.topjohnwu.magisk:drawable/ic_logo = 0x7f020003
com.topjohnwu.magisk:drawable/ic_magisk = 0x7f020004
com.topjohnwu.magisk:drawable/ic_magisk_outline = 0x7f020005
com.topjohnwu.magisk:drawable/ic_magisk_padded = 0x7f020006
com.topjohnwu.magisk:drawable/ic_magiskhide = 0x7f020007
com.topjohnwu.magisk:drawable/ic_splash_activity = 0x7f020008
com.topjohnwu.magisk:drawable/ic_superuser = 0x7f020009
com.topjohnwu.magisk:drawable/sc_cloud_download = 0x7f02000a
com.topjohnwu.magisk:drawable/sc_extension = 0x7f02000b
com.topjohnwu.magisk:drawable/sc_magiskhide = 0x7f02000c
com.topjohnwu.magisk:drawable/sc_superuser = 0x7f02000d
com.topjohnwu.magisk:style/SplashTheme = 0x7f040000
com.topjohnwu.magisk:style/SplashThemeBase = 0x7f040001
com.topjohnwu.magisk:style/SplashThemeBase.V19 = 0x7f040002
com.topjohnwu.magisk:xml/file_paths = 0x7f050000
com.topjohnwu.magisk:string/stub_0 = 0x7f030000
com.topjohnwu.magisk:string/stub_1 = 0x7f030001
com.topjohnwu.magisk:string/stub_2 = 0x7f030002
com.topjohnwu.magisk:string/stub_3 = 0x7f030003
com.topjohnwu.magisk:string/stub_4 = 0x7f030004
com.topjohnwu.magisk:string/stub_5 = 0x7f030005
com.topjohnwu.magisk:string/stub_6 = 0x7f030006
com.topjohnwu.magisk:string/stub_7 = 0x7f030007
com.topjohnwu.magisk:string/stub_8 = 0x7f030008
com.topjohnwu.magisk:string/stub_9 = 0x7f030009

27
stub/res-ids.txt Normal file
View File

@ -0,0 +1,27 @@
com.topjohnwu.magisk:style/SplashThemeBase.V19 = 0x7f040000
com.topjohnwu.magisk:string/app_name = 0x7f030000
com.topjohnwu.magisk:string/yes = 0x7f030001
com.topjohnwu.magisk:style/SplashTheme = 0x7f040001
com.topjohnwu.magisk:drawable/sc_superuser = 0x7f020000
com.topjohnwu.magisk:string/no_thanks = 0x7f030002
com.topjohnwu.magisk:drawable/ic_splash_activity = 0x7f020001
com.topjohnwu.magisk:drawable/sc_magiskhide = 0x7f020002
com.topjohnwu.magisk:color/light = 0x7f010000
com.topjohnwu.magisk:string/no_internet_msg = 0x7f030003
com.topjohnwu.magisk:drawable/sc_extension = 0x7f020003
com.topjohnwu.magisk:drawable/ic_magisk_outline = 0x7f020004
com.topjohnwu.magisk:color/ic_launcher_background = 0x7f010001
com.topjohnwu.magisk:xml/file_paths = 0x7f050000
com.topjohnwu.magisk:drawable/sc_cloud_download = 0x7f020005
com.topjohnwu.magisk:string/ok = 0x7f030004
com.topjohnwu.magisk:drawable/ic_magisk = 0x7f020006
com.topjohnwu.magisk:drawable/ic_superuser = 0x7f020007
com.topjohnwu.magisk:drawable/ic_cloud_download = 0x7f020008
com.topjohnwu.magisk:style/SplashThemeBase = 0x7f040002
com.topjohnwu.magisk:drawable/ic_magiskhide = 0x7f020009
com.topjohnwu.magisk:drawable/ic_launcher = 0x7f02000a
com.topjohnwu.magisk:drawable/ic_logo = 0x7f02000b
com.topjohnwu.magisk:drawable/ic_extension = 0x7f02000c
com.topjohnwu.magisk:string/upgrade_msg = 0x7f030005
com.topjohnwu.magisk:color/dark = 0x7f010002
com.topjohnwu.magisk:drawable/ic_magisk_padded = 0x7f02000d

View File

@ -1,12 +0,0 @@
package androidx.work.impl;
import com.topjohnwu.magisk.dummy.DummyProvider;
public class WorkManagerInitializer extends DummyProvider {
/* This class have to exist, or else pre 9.0 devices
* will experience ClassNotFoundException crashes when
* launching the stub, as ContentProviders are constructed
* when an app starts up. Pre 9.0 devices do not have
* our custom DelegateComponentFactory to help creating
* dummies. */
}

View File

@ -1,5 +1,6 @@
package com.topjohnwu.magisk;
import android.annotation.SuppressLint;
import android.app.AppComponentFactory;
import android.app.Application;
import android.content.Context;
@ -13,7 +14,6 @@ import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File;
import java.lang.reflect.Method;
import java.util.Map;
import static com.topjohnwu.magisk.DownloadActivity.TAG;
@ -34,35 +34,7 @@ public class DelegateApplication extends Application {
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (Build.VERSION.SDK_INT >= 28) {
// If 9.0+, try to dynamically load the APK
DelegateComponentFactory factory = (DelegateComponentFactory) this.factory;
MANAGER_APK = DynAPK.current(this);
File update = DynAPK.update(this);
if (update.exists())
update.renameTo(MANAGER_APK);
if (MANAGER_APK.exists()) {
ClassLoader cl = new DynamicClassLoader(MANAGER_APK, factory.loader);
try {
// Create the delegate AppComponentFactory
Object df = cl.loadClass("a.a").newInstance();
// Create the delegate Application
delegate = (Application) cl.loadClass("a.e").getConstructor(Map.class)
.newInstance(ComponentMap.inverseMap);
// 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);
// If everything went well, set our loader and delegate
factory.delegate = (AppComponentFactory) df;
factory.loader = cl;
} catch (Exception e) {
Log.e(TAG, "dyn load", e);
MANAGER_APK.delete();
}
}
setUpDynAPK();
} else {
MANAGER_APK = new File(base.getCacheDir(), "manager.apk");
}
@ -73,4 +45,36 @@ public class DelegateApplication extends Application {
super.onConfigurationChanged(newConfig);
delegate.onConfigurationChanged(newConfig);
}
@SuppressLint("NewApi")
private void setUpDynAPK() {
DelegateComponentFactory factory = (DelegateComponentFactory) this.factory;
MANAGER_APK = DynAPK.current(this);
File update = DynAPK.update(this);
if (update.exists())
update.renameTo(MANAGER_APK);
if (MANAGER_APK.exists()) {
ClassLoader cl = new DynamicClassLoader(MANAGER_APK, factory.loader);
try {
// Create the delegate AppComponentFactory
Object df = cl.loadClass("a.a").newInstance();
// Create the delegate Application
delegate = (Application) cl.loadClass("a.e").getConstructor(Object.class)
.newInstance(DynAPK.pack(Mapping.data));
// 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);
// If everything went well, set our loader and delegate
factory.delegate = (AppComponentFactory) df;
factory.loader = cl;
} catch (Exception e) {
Log.e(TAG, "dyn load", e);
MANAGER_APK.delete();
}
}
}
}

View File

@ -35,7 +35,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Log.d(TAG, className);
if (delegate != null)
return delegate.instantiateActivity(loader, ComponentMap.get(className), intent);
return delegate.instantiateActivity(loader, Mapping.get(className), intent);
return create(className, DummyActivity.class);
}
@ -44,7 +44,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Log.d(TAG, className);
if (delegate != null)
return delegate.instantiateReceiver(loader, ComponentMap.get(className), intent);
return delegate.instantiateReceiver(loader, Mapping.get(className), intent);
return create(className, DummyReceiver.class);
}
@ -53,7 +53,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Log.d(TAG, className);
if (delegate != null)
return delegate.instantiateService(loader, ComponentMap.get(className), intent);
return delegate.instantiateService(loader, Mapping.get(className), intent);
return create(className, DummyService.class);
}
@ -63,7 +63,7 @@ public class DelegateComponentFactory extends AppComponentFactory {
Log.d(TAG, className);
if (loader == null) loader = cl;
if (delegate != null)
return delegate.instantiateProvider(loader, ComponentMap.get(className));
return delegate.instantiateProvider(loader, Mapping.get(className));
return create(className, DummyProvider.class);
}

View File

@ -3,11 +3,13 @@ package com.topjohnwu.magisk;
import java.util.HashMap;
import java.util.Map;
class ComponentMap {
import static com.topjohnwu.magisk.utils.DynAPK.*;
class Mapping {
private static Map<String, String> map = new HashMap<>();
// This mapping will be sent into the guest app
static Map<String, String> inverseMap;
static Data data = new Data();
static {
map.put(a.z.class.getName(), "a.c");
@ -18,10 +20,17 @@ class ComponentMap {
map.put("a.v", "a.j");
map.put("a.j", "androidx.work.impl.background.systemjob.SystemJobService");
inverseMap = new HashMap<>(map.size());
data.componentMap = new HashMap<>(map.size());
for (Map.Entry<String, String> e : map.entrySet()) {
inverseMap.put(e.getValue(), e.getKey());
data.componentMap.put(e.getValue(), e.getKey());
}
int[] res = new int[5];
res[NOTIFICATION] = R.drawable.ic_magisk_outline;
res[SUPERUSER] = R.drawable.sc_superuser;
res[MAGISKHIDE] = R.drawable.sc_magiskhide;
res[DOWNLOAD] = R.drawable.sc_cloud_download;
res[MODULES] = R.drawable.sc_extension;
data.resourceMap = res;
}
static String get(String name) {