Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 1 | // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | package org.chromium.incrementalinstall; |
| 6 | |
| 7 | import android.app.Application; |
| 8 | import android.app.Instrumentation; |
| 9 | import android.content.ComponentName; |
| 10 | import android.content.Context; |
| 11 | import android.content.pm.ApplicationInfo; |
| 12 | import android.content.pm.PackageManager; |
| 13 | import android.content.pm.PackageManager.NameNotFoundException; |
| 14 | import android.os.Bundle; |
| 15 | import android.util.Log; |
| 16 | |
| 17 | import java.io.File; |
| 18 | import java.lang.ref.WeakReference; |
| 19 | import java.util.List; |
| 20 | import java.util.Map; |
| 21 | |
| 22 | /** |
| 23 | * An Application that replaces itself with another Application (as defined in |
| 24 | * an AndroidManifext.xml meta-data tag). It loads the other application only |
| 25 | * after side-loading its .so and .dex files from /data/local/tmp. |
| 26 | * |
| 27 | * This class is highly dependent on the private implementation details of |
| 28 | * Android's ActivityThread.java. However, it has been tested to work with |
| 29 | * JellyBean through Marshmallow. |
| 30 | */ |
| 31 | public final class BootstrapApplication extends Application { |
| 32 | private static final String TAG = "cr.incrementalinstall"; |
| 33 | private static final String MANAGED_DIR_PREFIX = "/data/local/tmp/incremental-app-"; |
| 34 | private static final String REAL_APP_META_DATA_NAME = "incremental-install-real-app"; |
| 35 | private static final String REAL_INSTRUMENTATION_META_DATA_NAME = |
| 36 | "incremental-install-real-instrumentation"; |
| 37 | |
| 38 | private ClassLoaderPatcher mClassLoaderPatcher; |
| 39 | private Application mRealApplication; |
| 40 | private Instrumentation mOrigInstrumentation; |
| 41 | private Instrumentation mRealInstrumentation; |
| 42 | private Object mStashedProviderList; |
| 43 | private Object mActivityThread; |
| 44 | |
| 45 | @Override |
| 46 | protected void attachBaseContext(Context context) { |
| 47 | super.attachBaseContext(context); |
| 48 | try { |
| 49 | mActivityThread = Reflect.invokeMethod(Class.forName("android.app.ActivityThread"), |
| 50 | "currentActivityThread"); |
| 51 | mClassLoaderPatcher = new ClassLoaderPatcher(context); |
| 52 | |
| 53 | mOrigInstrumentation = |
| 54 | (Instrumentation) Reflect.getField(mActivityThread, "mInstrumentation"); |
| 55 | Context instContext = mOrigInstrumentation.getContext(); |
| 56 | if (instContext == null) { |
| 57 | instContext = context; |
| 58 | } |
| 59 | |
| 60 | // When running with an instrumentation that lives in a different package from the |
| 61 | // application, we must load the dex files and native libraries from both pacakges. |
| 62 | // This logic likely won't work when the instrumentation is incremental, but the app is |
| 63 | // non-incremental. This configuration isn't used right now though. |
| 64 | String appPackageName = getPackageName(); |
| 65 | String instPackageName = instContext.getPackageName(); |
| 66 | boolean instPackageNameDiffers = !appPackageName.equals(instPackageName); |
| 67 | Log.i(TAG, "App PackageName: " + appPackageName); |
| 68 | if (instPackageNameDiffers) { |
| 69 | Log.i(TAG, "Inst PackageName: " + instPackageName); |
| 70 | } |
| 71 | |
| 72 | File appIncrementalRootDir = new File(MANAGED_DIR_PREFIX + appPackageName); |
| 73 | File appLibDir = new File(appIncrementalRootDir, "lib"); |
| 74 | File appDexDir = new File(appIncrementalRootDir, "dex"); |
| 75 | File appInstallLockFile = new File(appIncrementalRootDir, "install.lock"); |
| 76 | File appFirstRunLockFile = new File(appIncrementalRootDir, "firstrun.lock"); |
| 77 | File instIncrementalRootDir = new File(MANAGED_DIR_PREFIX + instPackageName); |
| 78 | File instLibDir = new File(instIncrementalRootDir, "lib"); |
| 79 | File instDexDir = new File(instIncrementalRootDir, "dex"); |
| 80 | File instInstallLockFile = new File(instIncrementalRootDir, "install.lock"); |
| 81 | File instFirstRunLockFile = new File(instIncrementalRootDir , "firstrun.lock"); |
| 82 | |
| 83 | boolean isFirstRun = LockFile.installerLockExists(appFirstRunLockFile) |
| 84 | || (instPackageNameDiffers |
| 85 | && LockFile.installerLockExists(instFirstRunLockFile)); |
| 86 | if (isFirstRun) { |
| 87 | if (mClassLoaderPatcher.mIsPrimaryProcess) { |
| 88 | // Wait for incremental_install.py to finish. |
| 89 | LockFile.waitForInstallerLock(appInstallLockFile, 30 * 1000); |
| 90 | LockFile.waitForInstallerLock(instInstallLockFile, 30 * 1000); |
| 91 | } else { |
| 92 | // Wait for the browser process to create the optimized dex files |
| 93 | // and copy the library files. |
| 94 | LockFile.waitForInstallerLock(appFirstRunLockFile, 60 * 1000); |
| 95 | LockFile.waitForInstallerLock(instFirstRunLockFile, 60 * 1000); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | mClassLoaderPatcher.importNativeLibs(instLibDir); |
| 100 | mClassLoaderPatcher.loadDexFiles(instDexDir); |
| 101 | if (instPackageNameDiffers) { |
| 102 | mClassLoaderPatcher.importNativeLibs(appLibDir); |
| 103 | mClassLoaderPatcher.loadDexFiles(appDexDir); |
| 104 | } |
| 105 | |
| 106 | if (isFirstRun && mClassLoaderPatcher.mIsPrimaryProcess) { |
| 107 | LockFile.clearInstallerLock(appFirstRunLockFile); |
| 108 | if (instPackageNameDiffers) { |
| 109 | LockFile.clearInstallerLock(instFirstRunLockFile); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // mInstrumentationAppDir is one of a set of fields that is initialized only when |
| 114 | // instrumentation is active. |
| 115 | if (Reflect.getField(mActivityThread, "mInstrumentationAppDir") != null) { |
| 116 | String realInstrumentationName = |
| 117 | getClassNameFromMetadata(REAL_INSTRUMENTATION_META_DATA_NAME, instContext); |
| 118 | initInstrumentation(realInstrumentationName); |
| 119 | } else { |
| 120 | Log.i(TAG, "No instrumentation active."); |
| 121 | } |
| 122 | |
| 123 | // Even when instrumentation is not enabled, ActivityThread uses a default |
| 124 | // Instrumentation instance internally. We hook it here in order to hook into the |
| 125 | // call to Instrumentation.onCreate(). |
| 126 | Reflect.setField(mActivityThread, "mInstrumentation", |
| 127 | new BootstrapInstrumentation(this)); |
| 128 | |
| 129 | // attachBaseContext() is called from ActivityThread#handleBindApplication() and |
| 130 | // Application#mApplication is changed right after we return. Thus, we cannot swap |
| 131 | // the Application instances until onCreate() is called. |
| 132 | String realApplicationName = getClassNameFromMetadata(REAL_APP_META_DATA_NAME, context); |
| 133 | Log.i(TAG, "Instantiating " + realApplicationName); |
| 134 | mRealApplication = |
| 135 | (Application) Reflect.newInstance(Class.forName(realApplicationName)); |
| 136 | Reflect.invokeMethod(mRealApplication, "attachBaseContext", context); |
| 137 | |
| 138 | // Between attachBaseContext() and onCreate(), ActivityThread tries to instantiate |
| 139 | // all ContentProviders. The ContentProviders break without the correct Application |
| 140 | // class being installed, so temporarily pretend there are no providers, and then |
| 141 | // instantiate them explicitly within onCreate(). |
| 142 | disableContentProviders(); |
| 143 | Log.i(TAG, "Waiting for Instrumentation.onCreate"); |
| 144 | } catch (Exception e) { |
| 145 | throw new RuntimeException("Incremental install failed.", e); |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | /** |
| 150 | * Returns the fully-qualified class name for the given key, stored in a |
| 151 | * <meta> witin the manifest. |
| 152 | */ |
| 153 | private static String getClassNameFromMetadata(String key, Context context) |
| 154 | throws NameNotFoundException { |
| 155 | String pkgName = context.getPackageName(); |
| 156 | ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkgName, |
| 157 | PackageManager.GET_META_DATA); |
| 158 | String value = appInfo.metaData.getString(key); |
| 159 | if (value != null && !value.contains(".")) { |
| 160 | value = pkgName + "." + value; |
| 161 | } |
| 162 | return value; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Instantiates and initializes mRealInstrumentation (the real Instrumentation class). |
| 167 | */ |
| 168 | private void initInstrumentation(String realInstrumentationName) |
| 169 | throws ReflectiveOperationException { |
| 170 | if (realInstrumentationName == null) { |
| 171 | // This is the case when an incremental app is used as a target for an instrumentation |
| 172 | // test. In this case, ActivityThread can instantiate the proper class just fine since |
| 173 | // it exists within the test apk (as opposed to the incremental apk-under-test). |
| 174 | Log.i(TAG, "Running with external instrumentation"); |
| 175 | mRealInstrumentation = mOrigInstrumentation; |
| 176 | return; |
| 177 | } |
| 178 | // For unit tests, the instrumentation class is replaced in the manifest by a build step |
| 179 | // because ActivityThread tries to instantiate it before we get a chance to load the |
| 180 | // incremental dex files. |
| 181 | Log.i(TAG, "Instantiating instrumentation " + realInstrumentationName); |
| 182 | mRealInstrumentation = (Instrumentation) Reflect.newInstance( |
| 183 | Class.forName(realInstrumentationName)); |
| 184 | |
| 185 | // Initialize the fields that are set by Instrumentation.init(). |
| 186 | String[] initFields = {"mThread", "mMessageQueue", "mInstrContext", "mAppContext", |
| 187 | "mWatcher", "mUiAutomationConnection"}; |
| 188 | for (String fieldName : initFields) { |
| 189 | Reflect.setField(mRealInstrumentation, fieldName, |
| 190 | Reflect.getField(mOrigInstrumentation, fieldName)); |
| 191 | } |
| 192 | // But make sure the correct ComponentName is used. |
| 193 | ComponentName newName = new ComponentName( |
| 194 | mOrigInstrumentation.getComponentName().getPackageName(), realInstrumentationName); |
| 195 | Reflect.setField(mRealInstrumentation, "mComponent", newName); |
| 196 | } |
| 197 | |
| 198 | /** |
| 199 | * Called by BootstrapInstrumentation from Instrumentation.onCreate(). |
| 200 | * This happens regardless of whether or not instrumentation is enabled. |
| 201 | */ |
| 202 | void onInstrumentationCreate(Bundle arguments) { |
| 203 | Log.i(TAG, "Instrumentation.onCreate() called. Swapping references."); |
| 204 | try { |
| 205 | swapApplicationReferences(); |
| 206 | enableContentProviders(); |
| 207 | if (mRealInstrumentation != null) { |
| 208 | Reflect.setField(mActivityThread, "mInstrumentation", mRealInstrumentation); |
| 209 | mRealInstrumentation.onCreate(arguments); |
| 210 | } |
| 211 | } catch (Exception e) { |
| 212 | throw new RuntimeException("Incremental install failed.", e); |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | @Override |
| 217 | public void onCreate() { |
| 218 | super.onCreate(); |
| 219 | try { |
| 220 | Log.i(TAG, "Application.onCreate() called."); |
| 221 | mRealApplication.onCreate(); |
| 222 | } catch (Exception e) { |
| 223 | throw new RuntimeException("Incremental install failed.", e); |
| 224 | } |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Nulls out ActivityThread.mBoundApplication.providers. |
| 229 | */ |
| 230 | private void disableContentProviders() throws ReflectiveOperationException { |
| 231 | Object data = Reflect.getField(mActivityThread, "mBoundApplication"); |
| 232 | mStashedProviderList = Reflect.getField(data, "providers"); |
| 233 | Reflect.setField(data, "providers", null); |
| 234 | } |
| 235 | |
| 236 | /** |
| 237 | * Restores the value of ActivityThread.mBoundApplication.providers, and invokes |
| 238 | * ActivityThread#installContentProviders(). |
| 239 | */ |
| 240 | private void enableContentProviders() throws ReflectiveOperationException { |
| 241 | Object data = Reflect.getField(mActivityThread, "mBoundApplication"); |
| 242 | Reflect.setField(data, "providers", mStashedProviderList); |
| 243 | if (mStashedProviderList != null && mClassLoaderPatcher.mIsPrimaryProcess) { |
| 244 | Log.i(TAG, "Instantiating content providers"); |
| 245 | Reflect.invokeMethod(mActivityThread, "installContentProviders", mRealApplication, |
| 246 | mStashedProviderList); |
| 247 | } |
| 248 | mStashedProviderList = null; |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Changes all fields within framework classes that have stored an reference to this |
| 253 | * BootstrapApplication to instead store references to mRealApplication. |
| 254 | * @throws NoSuchFieldException |
| 255 | */ |
| 256 | @SuppressWarnings("unchecked") |
| 257 | private void swapApplicationReferences() throws ReflectiveOperationException { |
| 258 | if (Reflect.getField(mActivityThread, "mInitialApplication") == this) { |
| 259 | Reflect.setField(mActivityThread, "mInitialApplication", mRealApplication); |
| 260 | } |
| 261 | |
| 262 | List<Application> allApplications = |
| 263 | (List<Application>) Reflect.getField(mActivityThread, "mAllApplications"); |
| 264 | for (int i = 0; i < allApplications.size(); i++) { |
| 265 | if (allApplications.get(i) == this) { |
| 266 | allApplications.set(i, mRealApplication); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) { |
| 271 | Map<String, WeakReference<?>> packageMap = |
| 272 | (Map<String, WeakReference<?>>) Reflect.getField(mActivityThread, fieldName); |
| 273 | for (Map.Entry<String, WeakReference<?>> entry : packageMap.entrySet()) { |
| 274 | Object loadedApk = entry.getValue().get(); |
| 275 | if (loadedApk != null && Reflect.getField(loadedApk, "mApplication") == this) { |
| 276 | Reflect.setField(loadedApk, "mApplication", mRealApplication); |
| 277 | Reflect.setField(mRealApplication, "mLoadedApk", loadedApk); |
| 278 | } |
| 279 | } |
| 280 | } |
| 281 | } |
| 282 | } |