Skip to content

Commit

Permalink
Support register ContentProviders in host, fix #253
Browse files Browse the repository at this point in the history
  • Loading branch information
galenlin committed Nov 4, 2016
1 parent 016c8f5 commit 08cc09e
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 67 deletions.
6 changes: 6 additions & 0 deletions Android/DevSample/small/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,11 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="stateHidden|adjustPan"
android:hardwareAccelerated="true"/>

<!-- SetUp Provider -->
<provider
android:exported="false"
android:authorities="net.wequick.small"
android:name=".SetUpProvider"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Handler;
Expand Down Expand Up @@ -101,10 +102,14 @@ private static class LoadedApk {
private static ConcurrentHashMap<String, List<IntentFilter>> sLoadedIntentFilters;

protected static Instrumentation sHostInstrumentation;
private static Instrumentation sBundleInstrumentation;
protected static InstrumentationWrapper sBundleInstrumentation;

private static final char REDIRECT_FLAG = '>';

protected static Object sActivityThread;
protected static List<ProviderInfo> sProviders;
protected static List<ProviderInfo> mLazyInitProviders;

/**
* Class for restore activity info from Stub to Real
*/
Expand All @@ -131,20 +136,23 @@ public boolean handleMessage(Message msg) {
/**
* Class for redirect activity from Stub(AndroidManifest.xml) to Real(Plugin)
*/
private static class InstrumentationWrapper extends Instrumentation
protected static class InstrumentationWrapper extends Instrumentation
implements InstrumentationInternal {

private Instrumentation mBase;
private static final int STUB_ACTIVITIES_COUNT = 4;

public InstrumentationWrapper() { }
public InstrumentationWrapper(Instrumentation base) {
mBase = base;
}

/** @Override V21+
* Wrap activity from REAL to STUB */
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, android.os.Bundle options) {
wrapIntent(intent);
return ReflectAccelerator.execStartActivity(sHostInstrumentation,
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode, options);
}

Expand All @@ -154,7 +162,7 @@ public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode) {
wrapIntent(intent);
return ReflectAccelerator.execStartActivity(sHostInstrumentation,
return ReflectAccelerator.execStartActivity(mBase,
who, contextThread, token, target, intent, requestCode);
}

Expand Down Expand Up @@ -259,7 +267,44 @@ public void callActivityOnDestroy(Activity activity) {
sHostInstrumentation.callActivityOnDestroy(activity);
}

private static void wrapIntent(Intent intent) {
@Override
public boolean onException(Object obj, Throwable e) {
if (sProviders != null && e.getClass().equals(ClassNotFoundException.class)) {
boolean errorOnInstallProvider = false;
StackTraceElement[] stacks = e.getStackTrace();
for (StackTraceElement st : stacks) {
if (st.getMethodName().equals("installProvider")) {
errorOnInstallProvider = true;
break;
}
}

if (errorOnInstallProvider) {
// We'll reinstall this content provider later, so just ignores it!!!
// FIXME: any better way to get the class name?
String msg = e.getMessage();
final String prefix = "Didn't find class \"";
if (msg.startsWith(prefix)) {
String providerClazz = msg.substring(prefix.length());
providerClazz = providerClazz.substring(0, providerClazz.indexOf("\""));
for (ProviderInfo info : sProviders) {
if (info.name.equals(providerClazz)) {
if (mLazyInitProviders == null) {
mLazyInitProviders = new ArrayList<ProviderInfo>();
}
mLazyInitProviders.add(info);
break;
}
}
}
return true;
}
}

return super.onException(obj, e);
}

private void wrapIntent(Intent intent) {
ComponentName component = intent.getComponent();
String realClazz;
if (component == null) {
Expand All @@ -284,7 +329,7 @@ private static void wrapIntent(Intent intent) {
intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
}

private static String resolveActivity(Intent intent) {
private String resolveActivity(Intent intent) {
if (sLoadedIntentFilters == null) return null;

Iterator<Map.Entry<String, List<IntentFilter>>> it =
Expand All @@ -308,10 +353,10 @@ private static String resolveActivity(Intent intent) {
return null;
}

private static String[] mStubQueue;
private String[] mStubQueue;

/** Get an usable stub activity clazz from real activity */
private static String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
// In standard mode, the stub activity is reusable.
// Cause the `windowIsTranslucent' attribute cannot be dynamically set,
Expand Down Expand Up @@ -371,7 +416,7 @@ private void inqueueStubActivity(ActivityInfo ai, String realActivityClazz) {
}

public static void wrapIntent(Intent intent) {
InstrumentationWrapper.wrapIntent(intent);
sBundleInstrumentation.wrapIntent(intent);
}

private static String unwrapIntent(Intent intent) {
Expand Down Expand Up @@ -423,56 +468,42 @@ public ApplicationInfo getApplicationInfo() {
@Override
public void setUp(Context context) {
super.setUp(context);
if (sHostInstrumentation == null) {
try {
// Inject instrumentation
final Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object thread = ReflectAccelerator.getActivityThread(context, activityThreadClass);
Field field = activityThreadClass.getDeclaredField("mInstrumentation");
field.setAccessible(true);
sHostInstrumentation = (Instrumentation) field.get(thread);
Instrumentation wrapper = new InstrumentationWrapper();
field.set(thread, wrapper);
if (!sHostInstrumentation.getClass().getName().equals("android.app.Instrumentation")) {
sBundleInstrumentation = wrapper; // record for later replacement
}

if (context instanceof Activity) {
field = Activity.class.getDeclaredField("mInstrumentation");
field.setAccessible(true);
field.set(context, wrapper);
}
Field f;

// Inject message handler
try {
f = sActivityThread.getClass().getDeclaredField("mH");
f.setAccessible(true);
Handler ah = (Handler) f.get(sActivityThread);
f = Handler.class.getDeclaredField("mCallback");
f.setAccessible(true);
f.set(ah, new ActivityThreadHandlerCallback());
} catch (Exception e) {
throw new RuntimeException("Failed to replace message handler for thread: " + sActivityThread);
}

// Inject handler
field = activityThreadClass.getDeclaredField("mH");
field.setAccessible(true);
Handler ah = (Handler) field.get(thread);
field = Handler.class.getDeclaredField("mCallback");
field.setAccessible(true);
field.set(ah, new ActivityThreadHandlerCallback());

// AOP for pending intent
Field f = TaskStackBuilder.class.getDeclaredField("IMPL");
f.setAccessible(true);
final Object impl = f.get(TaskStackBuilder.class);
InvocationHandler aop = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Intent[] intents = (Intent[]) args[1];
for (Intent intent : intents) {
InstrumentationWrapper.wrapIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
return method.invoke(impl, args);
// AOP for pending intent
try {
f = TaskStackBuilder.class.getDeclaredField("IMPL");
f.setAccessible(true);
final Object impl = f.get(TaskStackBuilder.class);
InvocationHandler aop = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Intent[] intents = (Intent[]) args[1];
for (Intent intent : intents) {
sBundleInstrumentation.wrapIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
}
};
Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
f.set(TaskStackBuilder.class, newImpl);
} catch (Exception ignored) {
ignored.printStackTrace();
// Usually, cannot reach here
}
return method.invoke(impl, args);
}
};
Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
f.set(TaskStackBuilder.class, newImpl);
} catch (Exception ignored) {
ignored.printStackTrace();
}
}

Expand All @@ -499,7 +530,7 @@ public void postSetUp() {
if (i != paths.length) {
paths = Arrays.copyOf(paths, i);
}
ReflectAccelerator.mergeResources(app, paths);
ReflectAccelerator.mergeResources(app, sActivityThread, paths);

// Merge all the dex into host's class loader
ClassLoader cl = app.getClassLoader();
Expand Down Expand Up @@ -555,8 +586,22 @@ public void run() {
}
}

// Lazy init content providers
if (mLazyInitProviders != null) {
try {
Method m = sActivityThread.getClass().getDeclaredMethod(
"installContentProviders", Context.class, List.class);
m.setAccessible(true);
m.invoke(sActivityThread, app, mLazyInitProviders);
} catch (Exception e) {
throw new RuntimeException("Failed to lazy init content providers: " + mLazyInitProviders);
}
}

// Free temporary variables
sLoadedApks = null;
sProviders = null;
sActivityThread = null;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package net.wequick.small;

import android.app.Application;
import android.app.Instrumentation;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.Nullable;

import net.wequick.small.util.ReflectAccelerator;

import java.lang.reflect.Field;
import java.util.List;

public class SetUpProvider extends ContentProvider {

public SetUpProvider() {
super();
}

@Override
public boolean onCreate() {
Application application = (Application) getContext().getApplicationContext();
Object/*ActivityThread*/ thread;
List<ProviderInfo> providers;
Instrumentation base;
ApkBundleLauncher.InstrumentationWrapper wrapper;
Field f;

// Get activity thread
thread = ReflectAccelerator.getActivityThread(application);

// Replace instrumentation
try {
f = thread.getClass().getDeclaredField("mInstrumentation");
f.setAccessible(true);
base = (Instrumentation) f.get(thread);
wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
f.set(thread, wrapper);
} catch (Exception e) {
throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
}

// Get providers
try {
f = thread.getClass().getDeclaredField("mBoundApplication");
f.setAccessible(true);
Object/*AppBindData*/ data = f.get(thread);
f = data.getClass().getDeclaredField("providers");
f.setAccessible(true);
providers = (List<ProviderInfo>) f.get(data);
} catch (Exception e) {
throw new RuntimeException("Failed to get providers from thread: " + thread);
}

ApkBundleLauncher.sActivityThread = thread;
ApkBundleLauncher.sProviders = providers;
ApkBundleLauncher.sHostInstrumentation = base;
ApkBundleLauncher.sBundleInstrumentation = wrapper;
return false;
}

@Nullable
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}

@Nullable
@Override
public String getType(Uri uri) {
return null;
}

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

0 comments on commit 08cc09e

Please sign in to comment.