blob: 1fb5e40b83b813aea7bae8ed88c3fc2d6f4049d3 [file] [log] [blame]
Ben Murdoch097c5b22016-05-18 11:27:45 +01001// 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
5package org.chromium.incrementalinstall;
6
7import android.app.Application;
8import android.app.Instrumentation;
9import android.content.ComponentName;
10import android.content.Context;
11import android.content.pm.ApplicationInfo;
12import android.content.pm.PackageManager;
13import android.content.pm.PackageManager.NameNotFoundException;
14import android.os.Bundle;
15import android.util.Log;
16
17import java.io.File;
18import java.lang.ref.WeakReference;
19import java.util.List;
20import 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 */
31public 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}