// Copyright 2015 The Chromium Authors
// 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.AppComponentFactory;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
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-application";
    private static final String REAL_INSTRUMENTATION_META_DATA_NAME0 =
            "incremental-install-instrumentation-0";
    private static final String REAL_INSTRUMENTATION_META_DATA_NAME1 =
            "incremental-install-instrumentation-1";
    private static final String REAL_APP_COMPONENT_FACTORY =
            "incremental-install-app-component-factory";

    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 {
            ApplicationInfo appInfo =
                    context.getPackageManager()
                            .getApplicationInfo(
                                    context.getPackageName(), PackageManager.GET_META_DATA);
            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);
                }
            }

            // AppComponentFactory was introduced in Android P.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                String realAppComponentFactory =
                        appInfo.metaData.getString(REAL_APP_COMPONENT_FACTORY);
                if (realAppComponentFactory != null) {
                    BootstrapAppComponentFactory.sDelegate =
                            (AppComponentFactory)
                                    Reflect.newInstance(Class.forName(realAppComponentFactory));
                }
            }

            // 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(appInfo.metaData.getString(metaDataName));
            } 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 = appInfo.metaData.getString(REAL_APP_META_DATA_NAME);
            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);
        }
    }

    /** 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);
                }
            }
        }
    }
}
