| // 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.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 dalvik.system.DexFile; |
| |
| 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 = "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_NAME0 = |
| "incremental-install-real-instrumentation-0"; |
| private static final String REAL_INSTRUMENTATION_META_DATA_NAME1 = |
| "incremental-install-real-instrumentation-1"; |
| |
| private ClassLoaderPatcher mClassLoaderPatcher; |
| private Application mRealApplication; |
| private Instrumentation mOrigInstrumentation; |
| private Instrumentation mRealInstrumentation; |
| private Object mStashedProviderList; |
| private Object mActivityThread; |
| public static DexFile[] sIncrementalDexFiles; // Needed by junit test runner. |
| |
| @Override |
| protected void attachBaseContext(Context context) { |
| super.attachBaseContext(context); |
| try { |
| mActivityThread = Reflect.invokeMethod(Class.forName("android.app.ActivityThread"), |
| "currentActivityThread"); |
| mClassLoaderPatcher = new ClassLoaderPatcher(context); |
| |
| mOrigInstrumentation = |
| (Instrumentation) Reflect.getField(mActivityThread, "mInstrumentation"); |
| Context instContext = mOrigInstrumentation.getContext(); |
| if (instContext == null) { |
| instContext = context; |
| } |
| |
| // When running with an instrumentation that lives in a different package from the |
| // application, we must load the dex files and native libraries from both pacakges. |
| // This logic likely won't work when the instrumentation is incremental, but the app is |
| // non-incremental. This configuration isn't used right now though. |
| String appPackageName = getPackageName(); |
| String instPackageName = instContext.getPackageName(); |
| boolean instPackageNameDiffers = !appPackageName.equals(instPackageName); |
| Log.i(TAG, "App PackageName: " + appPackageName); |
| if (instPackageNameDiffers) { |
| Log.i(TAG, "Inst PackageName: " + instPackageName); |
| } |
| |
| File appIncrementalRootDir = new File(MANAGED_DIR_PREFIX + appPackageName); |
| File appLibDir = new File(appIncrementalRootDir, "lib"); |
| File appDexDir = new File(appIncrementalRootDir, "dex"); |
| File appInstallLockFile = new File(appIncrementalRootDir, "install.lock"); |
| File appFirstRunLockFile = new File(appIncrementalRootDir, "firstrun.lock"); |
| File instIncrementalRootDir = new File(MANAGED_DIR_PREFIX + instPackageName); |
| File instLibDir = new File(instIncrementalRootDir, "lib"); |
| File instDexDir = new File(instIncrementalRootDir, "dex"); |
| File instInstallLockFile = new File(instIncrementalRootDir, "install.lock"); |
| File instFirstRunLockFile = new File(instIncrementalRootDir, "firstrun.lock"); |
| |
| boolean isFirstRun = LockFile.installerLockExists(appFirstRunLockFile) |
| || (instPackageNameDiffers |
| && LockFile.installerLockExists(instFirstRunLockFile)); |
| if (isFirstRun) { |
| if (mClassLoaderPatcher.mIsPrimaryProcess) { |
| // Wait for incremental_install.py to finish. |
| LockFile.waitForInstallerLock(appInstallLockFile, 30 * 1000); |
| LockFile.waitForInstallerLock(instInstallLockFile, 30 * 1000); |
| } else { |
| // Wait for the browser process to create the optimized dex files |
| // and copy the library files. |
| LockFile.waitForInstallerLock(appFirstRunLockFile, 60 * 1000); |
| LockFile.waitForInstallerLock(instFirstRunLockFile, 60 * 1000); |
| } |
| } |
| |
| mClassLoaderPatcher.importNativeLibs(instLibDir); |
| sIncrementalDexFiles = mClassLoaderPatcher.loadDexFiles(instDexDir, instPackageName); |
| if (instPackageNameDiffers) { |
| mClassLoaderPatcher.importNativeLibs(appLibDir); |
| mClassLoaderPatcher.loadDexFiles(appDexDir, appPackageName); |
| } |
| |
| if (isFirstRun && mClassLoaderPatcher.mIsPrimaryProcess) { |
| LockFile.clearInstallerLock(appFirstRunLockFile); |
| if (instPackageNameDiffers) { |
| LockFile.clearInstallerLock(instFirstRunLockFile); |
| } |
| } |
| |
| // mInstrumentationAppDir is one of a set of fields that is initialized only when |
| // instrumentation is active. |
| if (Reflect.getField(mActivityThread, "mInstrumentationAppDir") != null) { |
| String metaDataName = REAL_INSTRUMENTATION_META_DATA_NAME0; |
| if (mOrigInstrumentation instanceof SecondInstrumentation) { |
| metaDataName = REAL_INSTRUMENTATION_META_DATA_NAME1; |
| } |
| mRealInstrumentation = |
| initInstrumentation(getClassNameFromMetadata(metaDataName, instContext)); |
| } 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(). |
| BootstrapInstrumentation bootstrapInstrumentation = new BootstrapInstrumentation(this); |
| populateInstrumenationFields(bootstrapInstrumentation); |
| Reflect.setField(mActivityThread, "mInstrumentation", bootstrapInstrumentation); |
| |
| // 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, context); |
| Log.i(TAG, "Instantiating " + realApplicationName); |
| Instrumentation anyInstrumentation = |
| mRealInstrumentation != null ? mRealInstrumentation : mOrigInstrumentation; |
| mRealApplication = anyInstrumentation.newApplication( |
| getClassLoader(), realApplicationName, 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 static String getClassNameFromMetadata(String key, Context context) |
| throws NameNotFoundException { |
| String pkgName = context.getPackageName(); |
| ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkgName, |
| PackageManager.GET_META_DATA); |
| String value = appInfo.metaData.getString(key); |
| if (value != null && !value.contains(".")) { |
| value = pkgName + "." + value; |
| } |
| return value; |
| } |
| |
| /** |
| * Instantiates and initializes mRealInstrumentation (the real Instrumentation class). |
| */ |
| private Instrumentation initInstrumentation(String realInstrumentationName) |
| throws ReflectiveOperationException { |
| if (realInstrumentationName == null) { |
| // This is the case when an incremental app is used as a target for an instrumentation |
| // test. In this case, ActivityThread can instantiate the proper class just fine since |
| // it exists within the test apk (as opposed to the incremental apk-under-test). |
| Log.i(TAG, "Running with external instrumentation"); |
| return null; |
| } |
| // For unit tests, the instrumentation class is replaced in the manifest by a build step |
| // because ActivityThread tries to instantiate it before we get a chance to load the |
| // incremental dex files. |
| Log.i(TAG, "Instantiating instrumentation " + realInstrumentationName); |
| Instrumentation ret = |
| (Instrumentation) Reflect.newInstance(Class.forName(realInstrumentationName)); |
| populateInstrumenationFields(ret); |
| return ret; |
| } |
| |
| /** |
| * Sets important fields on a newly created Instrumentation object by copying them from the |
| * original Instrumentation instance. |
| */ |
| private void populateInstrumenationFields(Instrumentation target) |
| throws ReflectiveOperationException { |
| // Initialize the fields that are set by Instrumentation.init(). |
| String[] initFields = {"mAppContext", "mComponent", "mInstrContext", "mMessageQueue", |
| "mThread", "mUiAutomationConnection", "mWatcher"}; |
| for (String fieldName : initFields) { |
| Reflect.setField(target, fieldName, Reflect.getField(mOrigInstrumentation, fieldName)); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| @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); |
| } |
| } |
| |
| // Contains a reference to BootstrapApplication and will cause BroadCastReceivers to fail |
| // if not replaced. |
| Context contextImpl = mRealApplication.getBaseContext(); |
| Reflect.setField(contextImpl, "mOuterContext", 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); |
| } |
| } |
| } |
| } |
| } |