blob: 67c60e5e14e1eefa256ba9546ca2d791f6ee1a11 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.incrementalinstall;
import android.app.Application;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.Map;
/**
* An Application that replaces itself with another Application (as defined in
* an AndroidManifext.xml meta-data tag). It loads the other application only
* after side-loading its .so and .dex files from /data/local/tmp.
*
* This class is highly dependent on the private implementation details of
* Android's ActivityThread.java. However, it has been tested to work with
* JellyBean through Marshmallow.
*/
public final class BootstrapApplication extends Application {
private static final String TAG = "cr.incrementalinstall";
private static final String MANAGED_DIR_PREFIX = "/data/local/tmp/incremental-app-";
private static final String REAL_APP_META_DATA_NAME = "incremental-install-real-app";
private static final String REAL_INSTRUMENTATION_META_DATA_NAME =
"incremental-install-real-instrumentation";
private ClassLoaderPatcher mClassLoaderPatcher;
private Application mRealApplication;
private Instrumentation mRealInstrumentation;
private Object mStashedProviderList;
private Object mActivityThread;
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
File incrementalRootDir = new File(MANAGED_DIR_PREFIX + context.getPackageName());
File libDir = new File(incrementalRootDir, "lib");
File dexDir = new File(incrementalRootDir, "dex");
File installLockFile = new File(incrementalRootDir, "install.lock");
File firstRunLockFile = new File(incrementalRootDir, "firstrun.lock");
try {
mActivityThread = Reflect.invokeMethod(Class.forName("android.app.ActivityThread"),
"currentActivityThread");
mClassLoaderPatcher = new ClassLoaderPatcher(context);
boolean isFirstRun = LockFile.installerLockExists(firstRunLockFile);
if (isFirstRun) {
if (mClassLoaderPatcher.mIsPrimaryProcess) {
// Wait for incremental_install.py to finish.
LockFile.waitForInstallerLock(installLockFile, 30 * 1000);
} else {
// Wait for the browser process to create the optimized dex files
// and copy the library files.
LockFile.waitForInstallerLock(firstRunLockFile, 60 * 1000);
}
}
mClassLoaderPatcher.importNativeLibs(libDir);
mClassLoaderPatcher.loadDexFiles(dexDir);
if (isFirstRun && mClassLoaderPatcher.mIsPrimaryProcess) {
LockFile.clearInstallerLock(firstRunLockFile);
}
// mInstrumentationAppDir is one of a set of fields that is initialized only when
// instrumentation is active.
if (Reflect.getField(mActivityThread, "mInstrumentationAppDir") != null) {
String realInstrumentationName =
getClassNameFromMetadata(REAL_INSTRUMENTATION_META_DATA_NAME);
initInstrumentation(realInstrumentationName);
} else {
Log.i(TAG, "No instrumentation active.");
}
// Even when instrumentation is not enabled, ActivityThread uses a default
// Instrumentation instance internally. We hook it here in order to hook into the
// call to Instrumentation.onCreate().
Reflect.setField(mActivityThread, "mInstrumentation",
new BootstrapInstrumentation(this));
// attachBaseContext() is called from ActivityThread#handleBindApplication() and
// Application#mApplication is changed right after we return. Thus, we cannot swap
// the Application instances until onCreate() is called.
String realApplicationName = getClassNameFromMetadata(REAL_APP_META_DATA_NAME);
Log.i(TAG, "Instantiating " + realApplicationName);
mRealApplication =
(Application) Reflect.newInstance(Class.forName(realApplicationName));
Reflect.invokeMethod(mRealApplication, "attachBaseContext", context);
// Between attachBaseContext() and onCreate(), ActivityThread tries to instantiate
// all ContentProviders. The ContentProviders break without the correct Application
// class being installed, so temporarily pretend there are no providers, and then
// instantiate them explicitly within onCreate().
disableContentProviders();
Log.i(TAG, "Waiting for Instrumentation.onCreate");
} catch (Exception e) {
throw new RuntimeException("Incremental install failed.", e);
}
}
/**
* Returns the fully-qualified class name for the given key, stored in a
* <meta> witin the manifest.
*/
private String getClassNameFromMetadata(String key) throws NameNotFoundException {
ApplicationInfo appInfo = getPackageManager().getApplicationInfo(getPackageName(),
PackageManager.GET_META_DATA);
String value = appInfo.metaData.getString(key);
if (!value.contains(".")) {
value = getPackageName() + "." + value;
}
return value;
}
/**
* Instantiates and initializes mRealInstrumentation (the real Instrumentation class).
*/
private void initInstrumentation(String realInstrumentationName)
throws ReflectiveOperationException {
Log.i(TAG, "Instantiating instrumentation " + realInstrumentationName);
mRealInstrumentation = (Instrumentation) Reflect.newInstance(
Class.forName(realInstrumentationName));
Instrumentation oldInstrumentation =
(Instrumentation) Reflect.getField(mActivityThread, "mInstrumentation");
// Initialize the fields that are set by Instrumentation.init().
String[] initFields = {"mThread", "mMessageQueue", "mInstrContext", "mAppContext",
"mWatcher", "mUiAutomationConnection"};
for (String fieldName : initFields) {
Reflect.setField(mRealInstrumentation, fieldName,
Reflect.getField(oldInstrumentation, fieldName));
}
// But make sure the correct ComponentName is used.
ComponentName newName = new ComponentName(
oldInstrumentation.getComponentName().getPackageName(), realInstrumentationName);
Reflect.setField(mRealInstrumentation, "mComponent", newName);
}
/**
* Called by BootstrapInstrumentation from Instrumentation.onCreate().
* This happens regardless of whether or not instrumentation is enabled.
*/
void onInstrumentationCreate(Bundle arguments) {
Log.i(TAG, "Instrumentation.onCreate() called. Swapping references.");
try {
swapApplicationReferences();
enableContentProviders();
if (mRealInstrumentation != null) {
Reflect.setField(mActivityThread, "mInstrumentation", mRealInstrumentation);
mRealInstrumentation.onCreate(arguments);
}
} catch (Exception e) {
throw new RuntimeException("Incremental install failed.", e);
}
}
@Override
public void onCreate() {
super.onCreate();
try {
Log.i(TAG, "Application.onCreate() called.");
mRealApplication.onCreate();
} catch (Exception e) {
throw new RuntimeException("Incremental install failed.", e);
}
}
/**
* Nulls out ActivityThread.mBoundApplication.providers.
*/
private void disableContentProviders() throws ReflectiveOperationException {
Object data = Reflect.getField(mActivityThread, "mBoundApplication");
mStashedProviderList = Reflect.getField(data, "providers");
Reflect.setField(data, "providers", null);
}
/**
* Restores the value of ActivityThread.mBoundApplication.providers, and invokes
* ActivityThread#installContentProviders().
*/
private void enableContentProviders() throws ReflectiveOperationException {
Object data = Reflect.getField(mActivityThread, "mBoundApplication");
Reflect.setField(data, "providers", mStashedProviderList);
if (mStashedProviderList != null && mClassLoaderPatcher.mIsPrimaryProcess) {
Log.i(TAG, "Instantiating content providers");
Reflect.invokeMethod(mActivityThread, "installContentProviders", mRealApplication,
mStashedProviderList);
}
mStashedProviderList = null;
}
/**
* Changes all fields within framework classes that have stored an reference to this
* BootstrapApplication to instead store references to mRealApplication.
* @throws NoSuchFieldException
*/
@SuppressWarnings("unchecked")
private void swapApplicationReferences() throws ReflectiveOperationException {
if (Reflect.getField(mActivityThread, "mInitialApplication") == this) {
Reflect.setField(mActivityThread, "mInitialApplication", mRealApplication);
}
List<Application> allApplications =
(List<Application>) Reflect.getField(mActivityThread, "mAllApplications");
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == this) {
allApplications.set(i, mRealApplication);
}
}
for (String fieldName : new String[] { "mPackages", "mResourcePackages" }) {
Map<String, WeakReference<?>> packageMap =
(Map<String, WeakReference<?>>) Reflect.getField(mActivityThread, fieldName);
for (Map.Entry<String, WeakReference<?>> entry : packageMap.entrySet()) {
Object loadedApk = entry.getValue().get();
if (loadedApk != null && Reflect.getField(loadedApk, "mApplication") == this) {
Reflect.setField(loadedApk, "mApplication", mRealApplication);
Reflect.setField(mRealApplication, "mLoadedApk", loadedApk);
}
}
}
}
}