blob: 538957ed482b60bae75e27f82198b880dcdef948 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.fd.runtime;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH;
import static android.os.Build.VERSION_CODES.JELLY_BEAN;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static com.android.tools.fd.runtime.BootstrapApplication.LOG_TAG;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.view.ContextThemeWrapper;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A utility class which uses reflection hacks to replace the application instance and
* the resource data for the current app.
* This is based on the reflection parts of
* com.google.devtools.build.android.incrementaldeployment.StubApplication,
* plus changes to compile on JDK 6.
* <p>
* It now also has a lot of extra reflection machinery to do live resource swapping
* in a running app (e.g. swiping through data structures, updating resource managers,
* flushing cached theme entries, etc.)
* <p>
* The original is
* https://github.com/google/bazel/blob/master/src/tools/android/java/com/google/devtools/build/android/incrementaldeployment/StubApplication.java
* (May 11 revision, ca96e11)
* <p>
* (The code to handle resource loading etc is different; see FileManager.)
* Furthermore, the resource patching was hacked on some more such that it can
* handle live (activity-restart) changes, which allows us to for example patch
* the theme and have existing activities have their themes updated!
* <p>
* Original comment for the StubApplication, which contained the reflection methods:
* <p>
* A stub application that patches the class loader, then replaces itself with the real application
* by applying a liberal amount of reflection on Android internals.
* <p>
* <p>This is, of course, terribly error-prone. Most of this code was tested with API versions
* 8, 10, 14, 15, 16, 17, 18, 19 and 21 on the Android emulator, a Nexus 5 running Lollipop LRX22C
* and a Samsung GT-I5800 running Froyo XWJPE. The exception is {@code monkeyPatchAssetManagers},
* which only works on Kitkat and Lollipop.
* <p>
* <p>Note that due to a bug in Dalvik, this only works on Kitkat if ART is the Java runtime.
* <p>
* <p>Unfortunately, if this does not work, we don't have a fallback mechanism: as soon as we
* build the APK with this class as the Application, we are committed to going through with it.
* <p>
* <p>This class should use as few other classes as possible before the class loader is patched
* because any class loaded before it cannot be incrementally deployed.
*/
public class MonkeyPatcher {
@SuppressWarnings("unchecked") // Lots of conversions with generic types
public static void monkeyPatchApplication(@Nullable Context context,
@Nullable Application bootstrap,
@Nullable Application realApplication,
@Nullable String externalResourceFile) {
/*
The code seems to perform this:
Application realApplication = the newly instantiated (in attachBaseContext) user app
currentActivityThread = ActivityThread.currentActivityThread;
Application initialApplication = currentActivityThread.mInitialApplication;
if (initialApplication == BootstrapApplication.this) {
currentActivityThread.mInitialApplication = realApplication;
// Replace all instance of the stub application in ActivityThread#mAllApplications with the
// real one
List<Application> allApplications = currentActivityThread.mAllApplications;
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == BootstrapApplication.this) {
allApplications.set(i, realApplication);
}
}
// Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
// ActivityThread#mResourcePackages and do two things:
// - Replace the Application instance in its mApplication field with the real one
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
ArrayMap<String, WeakReference<LoadedApk>> map1 = currentActivityThread.mPackages;
for (Map.Entry<String, WeakReference<?>> entry : map1.entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (loadedApk.mApplication == BootstrapApplication.this) {
loadedApk.mApplication = realApplication;
if (externalResourceFile != null) {
loadedApk.mResDir = externalResourceFile;
}
realApplication.mLoadedApk = loadedApk;
}
}
// Exactly the same as above, except done for mResourcePackages instead of mPackages
ArrayMap<String, WeakReference<LoadedApk>> map2 = currentActivityThread.mResourcePackages;
for (Map.Entry<String, WeakReference<?>> entry : map2.entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (loadedApk.mApplication == BootstrapApplication.this) {
loadedApk.mApplication = realApplication;
if (externalResourceFile != null) {
loadedApk.mResDir = externalResourceFile;
}
realApplication.mLoadedApk = loadedApk;
}
}
*/
// BootstrapApplication is created by reflection in Application#handleBindApplication() ->
// LoadedApk#makeApplication(), and its return value is used to set the Application field in all
// sorts of Android internals.
//
// Fortunately, Application#onCreate() is called quite soon after, so what we do is monkey
// patch in the real Application instance in BootstrapApplication#onCreate().
//
// A few places directly use the created Application instance (as opposed to the fields it is
// eventually stored in). Fortunately, it's easy to forward those to the actual real
// Application class.
try {
// Find the ActivityThread instance for the current thread
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context, activityThread);
// Find the mInitialApplication field of the ActivityThread to the real application
Field mInitialApplication = activityThread.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication.get(currentActivityThread);
if (realApplication != null && initialApplication == bootstrap) {
mInitialApplication.set(currentActivityThread, realApplication);
}
// Replace all instance of the stub application in ActivityThread#mAllApplications with the
// real one
if (realApplication != null) {
Field mAllApplications = activityThread.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List<Application> allApplications = (List<Application>) mAllApplications
.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
// Figure out how loaded APKs are stored.
// API version 8 has PackageInfo, 10 has LoadedApk. 9, I don't know.
Class<?> loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
// 10 doesn't have this field, 14 does. Fortunately, there are not many Honeycomb devices
// floating around.
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
// According to testing, it's okay to ignore this.
}
// Enumerate all LoadedApk (or PackageInfo) fields in ActivityThread#mPackages and
// ActivityThread#mResourcePackages and do two things:
// - Replace the Application instance in its mApplication field with the real one
// - Replace mResDir to point to the external resource file instead of the .apk. This is
// used as the asset path for new Resources objects.
// - Set Application#mLoadedApk to the found LoadedApk instance
for (String fieldName : new String[]{"mPackages", "mResourcePackages"}) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry<String, WeakReference<?>> entry :
((Map<String, WeakReference<?>>) value).entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk == null) {
continue;
}
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
if (realApplication != null && mLoadedApk != null) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
@Nullable
public static Object getActivityThread(@Nullable Context context,
@Nullable Class<?> activityThread) {
try {
if (activityThread == null) {
activityThread = Class.forName("android.app.ActivityThread");
}
Method m = activityThread.getMethod("currentActivityThread");
m.setAccessible(true);
Object currentActivityThread = m.invoke(null);
if (currentActivityThread == null && context != null) {
// In older versions of Android (prior to frameworks/base 66a017b63461a22842)
// the currentActivityThread was built on thread locals, so we'll need to try
// even harder
Field mLoadedApk = context.getClass().getField("mLoadedApk");
mLoadedApk.setAccessible(true);
Object apk = mLoadedApk.get(context);
Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
mActivityThreadField.setAccessible(true);
currentActivityThread = mActivityThreadField.get(apk);
}
return currentActivityThread;
} catch (Throwable ignore) {
return null;
}
}
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
/*
(Note: the resource directory is *also* inserted into the loadedApk in
monkeyPatchApplication)
The code seems to perform this:
File externalResourceFile = <path to resources.ap_ or extracted directory>
AssetManager newAssetManager = new AssetManager();
newAssetManager.addAssetPath(externalResourceFile)
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
newAssetManager.ensureStringBlocks();
// Find the singleton instance of ResourcesManager
ResourcesManager resourcesManager = ResourcesManager.getInstance();
// Iterate over all known Resources objects
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (WeakReference<Resources> wr : resourcesManager.mActiveResources.values()) {
Resources resources = wr.get();
// Set the AssetManager of the Resources instance to our brand new one
resources.mAssets = newAssetManager;
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
// Also, for each context, call getTheme() to get the current theme; null out its
// mTheme field, then invoke initializeTheme() to force it to be recreated (with the
// new asset manager!)
*/
try {
// Create a new AssetManager instance and point it to the resources installed under
// /sdcard
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme");
mtm.setAccessible(true);
mtm.invoke(activity);
if (SDK_INT < 24) { // As of API 24, mTheme is gone (but updates work
// without these changes
Method mCreateTheme = AssetManager.class
.getDeclaredMethod("createTheme");
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
}
} catch (Throwable e) {
Log.e(LOG_TAG, "Failed to update existing theme for activity " + activity,
e);
}
pruneResourceCaches(resources);
}
}
// Iterate over all known Resources objects
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
// Find the singleton instance of ResourcesManager
Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null);
try {
Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
@SuppressWarnings("unchecked")
ArrayMap<?, WeakReference<Resources>> arrayMap =
(ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
//noinspection unchecked
references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
}
} else {
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
@SuppressWarnings("unchecked")
HashMap<?, WeakReference<Resources>> map =
(HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
references = map.values();
}
for (WeakReference<Resources> wr : references) {
Resources resources = wr.get();
if (resources != null) {
// Set the AssetManager of the Resources instance to our brand new one
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
private static void pruneResourceCaches(@NonNull Object resources) {
// Drain TypedArray instances from the typed array pool since these can hold on
// to stale asset data
if (SDK_INT >= LOLLIPOP) {
try {
Field typedArrayPoolField =
Resources.class.getDeclaredField("mTypedArrayPool");
typedArrayPoolField.setAccessible(true);
Object pool = typedArrayPoolField.get(resources);
Class<?> poolClass = pool.getClass();
Method acquireMethod = poolClass.getDeclaredMethod("acquire");
acquireMethod.setAccessible(true);
while (true) {
Object typedArray = acquireMethod.invoke(pool);
if (typedArray == null) {
break;
}
}
} catch (Throwable ignore) {
}
}
if (SDK_INT >= Build.VERSION_CODES.M) {
// Really should only be N; fix this as soon as it has its own API level
try {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
// For the remainder, use the ResourcesImpl instead, where all the fields
// now live
resources = mResourcesImpl.get(resources);
} catch (Throwable ignore) {
}
}
// Prune bitmap and color state lists etc caches
Object lock = null;
if (SDK_INT >= JELLY_BEAN_MR2) {
try {
Field field = resources.getClass().getDeclaredField("mAccessLock");
field.setAccessible(true);
lock = field.get(resources);
} catch (Throwable ignore) {
}
} else {
try {
Field field = Resources.class.getDeclaredField("mTmpValue");
field.setAccessible(true);
lock = field.get(resources);
} catch (Throwable ignore) {
}
}
if (lock == null) {
lock = MonkeyPatcher.class;
}
//noinspection SynchronizationOnLocalVariableOrMethodParameter
synchronized (lock) {
// Prune bitmap and color caches
pruneResourceCache(resources, "mDrawableCache");
pruneResourceCache(resources,"mColorDrawableCache");
pruneResourceCache(resources,"mColorStateListCache");
if (SDK_INT >= M) {
pruneResourceCache(resources, "mAnimatorCache");
pruneResourceCache(resources, "mStateListAnimatorCache");
} else if (SDK_INT == KITKAT) {
pruneResourceCache(resources, "sPreloadedDrawables");
pruneResourceCache(resources, "sPreloadedColorDrawables");
pruneResourceCache(resources, "sPreloadedColorStateLists");
}
}
}
private static boolean pruneResourceCache(@NonNull Object resources,
@NonNull String fieldName) {
try {
Class<?> resourcesClass = resources.getClass();
Field cacheField;
try {
cacheField = resourcesClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException ignore) {
cacheField = Resources.class.getDeclaredField(fieldName);
}
cacheField.setAccessible(true);
Object cache = cacheField.get(resources);
// Find the class which defines the onConfigurationChange method
Class<?> type = cacheField.getType();
if (SDK_INT < JELLY_BEAN) {
if (cache instanceof SparseArray) {
((SparseArray) cache).clear();
return true;
} else if (SDK_INT >= ICE_CREAM_SANDWICH && cache instanceof LongSparseArray) {
// LongSparseArray has API level 16 but was private (and available inside
// the framework) in 15 and is used for this cache.
//noinspection AndroidLintNewApi
((LongSparseArray) cache).clear();
return true;
}
} else if (SDK_INT < M) {
// JellyBean, KitKat, Lollipop
if ("mColorStateListCache".equals(fieldName)) {
// For some reason framework doesn't call clearDrawableCachesLocked on
// this field
if (cache instanceof LongSparseArray) {
//noinspection AndroidLintNewApi
((LongSparseArray)cache).clear();
}
} else if (type.isAssignableFrom(ArrayMap.class)) {
Method clearArrayMap = Resources.class.getDeclaredMethod(
"clearDrawableCachesLocked", ArrayMap.class, Integer.TYPE);
clearArrayMap.setAccessible(true);
clearArrayMap.invoke(resources, cache, -1);
return true;
} else if (type.isAssignableFrom(LongSparseArray.class)) {
try {
Method clearSparseMap = Resources.class.getDeclaredMethod(
"clearDrawableCachesLocked", LongSparseArray.class, Integer.TYPE);
clearSparseMap.setAccessible(true);
clearSparseMap.invoke(resources, cache, -1);
return true;
} catch (NoSuchMethodException e) {
if (cache instanceof LongSparseArray) {
//noinspection AndroidLintNewApi
((LongSparseArray)cache).clear();
return true;
}
}
} else if (type.isArray() &&
type.getComponentType().isAssignableFrom(LongSparseArray.class)) {
LongSparseArray[] arrays = (LongSparseArray[])cache;
for (LongSparseArray array : arrays) {
if (array != null) {
//noinspection AndroidLintNewApi
array.clear();
}
}
return true;
}
} else {
// Marshmallow: DrawableCache class
while (type != null) {
try {
Method configChangeMethod = type.getDeclaredMethod(
"onConfigurationChange", Integer.TYPE);
configChangeMethod.setAccessible(true);
configChangeMethod.invoke(cache, -1);
return true;
} catch (Throwable ignore) {
}
type = type.getSuperclass();
}
}
} catch (Throwable ignore) {
// Not logging these; while there is some checking of SDK_INT here to avoid
// doing a lot of unnecessary field lookups, it's not entirely accurate and
// errs on the side of caution (since different devices may have picked up
// different snapshots of the framework); therefore, it's normal for this
// to attempt to look up a field for a cache that isn't there; only if it's
// really there will it continue to flush that particular cache.
}
return false;
}
}