| // 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 mOrigInstrumentation; |
| private Instrumentation mRealInstrumentation; |
| private Object mStashedProviderList; |
| private Object mActivityThread; |
| |
| @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); |
| mClassLoaderPatcher.loadDexFiles(instDexDir); |
| if (instPackageNameDiffers) { |
| mClassLoaderPatcher.importNativeLibs(appLibDir); |
| mClassLoaderPatcher.loadDexFiles(appDexDir); |
| } |
| |
| 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 realInstrumentationName = |
| getClassNameFromMetadata(REAL_INSTRUMENTATION_META_DATA_NAME, instContext); |
| 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, context); |
| 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 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 void 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"); |
| mRealInstrumentation = mOrigInstrumentation; |
| return; |
| } |
| // 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); |
| mRealInstrumentation = (Instrumentation) Reflect.newInstance( |
| Class.forName(realInstrumentationName)); |
| |
| // 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(mOrigInstrumentation, fieldName)); |
| } |
| // But make sure the correct ComponentName is used. |
| ComponentName newName = new ComponentName( |
| mOrigInstrumentation.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); |
| } |
| } |
| } |
| } |
| } |