// Copyright 2013 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.content.browser;

import android.content.Context;
import android.os.StrictMode;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.BuildInfo;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.LoaderErrors;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.ScopedSysTraceEvent;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.content.app.ContentMain;
import org.chromium.content.browser.ServicificationStartupUma.ServicificationStartup;
import org.chromium.content_public.browser.BrowserStartupController;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;

/**
 * Implementation of {@link BrowserStartupController}.
 * This is a singleton, and stores a reference to the application context.
 */
@JNINamespace("content")
public class BrowserStartupControllerImpl implements BrowserStartupController {
    private static final String TAG = "BrowserStartup";

    // Helper constants for {@link #executeEnqueuedCallbacks(int, boolean)}.
    @VisibleForTesting
    static final int STARTUP_SUCCESS = -1;
    @VisibleForTesting
    static final int STARTUP_FAILURE = 1;

    @IntDef({BrowserStartType.FULL_BROWSER, BrowserStartType.MINIMAL_BROWSER})
    @Retention(RetentionPolicy.SOURCE)
    public @interface BrowserStartType {
        int FULL_BROWSER = 0;
        int MINIMAL_BROWSER = 1;
    }

    private static BrowserStartupControllerImpl sInstance;

    private static boolean sShouldStartGpuProcessOnBrowserStartup;

    @VisibleForTesting
    @CalledByNative
    static void browserStartupComplete(int result) {
        if (sInstance != null) {
            sInstance.executeEnqueuedCallbacks(result);
        }
    }

    @CalledByNative
    static void minimalBrowserStartupComplete() {
        if (sInstance != null) {
            sInstance.minimalBrowserStarted();
        }
    }

    @CalledByNative
    static boolean shouldStartGpuProcessOnBrowserStartup() {
        return sShouldStartGpuProcessOnBrowserStartup;
    }

    // A list of callbacks that should be called when the async startup of the browser process is
    // complete.
    private final List<StartupCallback> mAsyncStartupCallbacks;

    // A list of callbacks that should be called after a minimal browser environment is initialized.
    // These callbacks will be called once all the ongoing requests to start a minimal or full
    // browser process are completed. For example, if there is no outstanding request to start full
    // browser process, the callbacks will be executed once the minimal browser starts. On the other
    // hand, the callbacks will be defered until full browser starts.
    private final List<StartupCallback> mMinimalBrowserStartedCallbacks;

    // Whether the async startup of the browser process has started.
    private boolean mHasStartedInitializingBrowserProcess;

    // Ensures prepareToStartBrowserProcess() logic happens only once.
    private boolean mPrepareToStartCompleted;

    private boolean mHasCalledContentStart;

    // Whether the async startup of the browser process is complete.
    private boolean mFullBrowserStartupDone;

    // This field is set after startup has been completed based on whether the startup was a success
    // or not. It is used when later requests to startup come in that happen after the initial set
    // of enqueued callbacks have been executed.
    private boolean mStartupSuccess;

    // Tests may inject a method to be run instead of calling ContentMain() in order for them to
    // initialize the C++ system via another means.
    private Runnable mContentMainCallbackForTests;

    // Browser start up type. If the type is |BROWSER_START_TYPE_MINIMAL|, start up
    // will be paused after the minimal environment is setup. Additional request to launch the full
    // browser process is needed to fully complete the startup process. Callbacks will executed
    // once the browser is fully started, or when the minimal environment is setup and there are no
    // outstanding requests to start the full browser.
    @BrowserStartType
    private int mCurrentBrowserStartType = BrowserStartType.FULL_BROWSER;

    // If the app is only started with a minimal browser, whether it needs to launch full browser
    // funcionalities now.
    private boolean mLaunchFullBrowserAfterMinimalBrowserStart;

    // Whether the minimal browser environment is set up.
    private boolean mMinimalBrowserStarted;

    private TracingControllerAndroidImpl mTracingController;

    BrowserStartupControllerImpl() {
        mAsyncStartupCallbacks = new ArrayList<>();
        mMinimalBrowserStartedCallbacks = new ArrayList<>();
        if (BuildInfo.isDebugAndroid() && !ContextUtils.isSdkSandboxProcess()) {
            // Only set up the tracing broadcast receiver on debug builds of the OS and
            // non-SdkSandbox process. Normal tracing should use the DevTools API.
            PostTask.postTask(TaskTraits.UI_DEFAULT, new Runnable() {
                @Override
                public void run() {
                    addStartupCompletedObserver(new StartupCallback() {
                        @Override
                        public void onSuccess() {
                            assert mTracingController == null;
                            Context context = ContextUtils.getApplicationContext();
                            mTracingController = new TracingControllerAndroidImpl(context);
                            mTracingController.registerReceiver(context);
                        }

                        @Override
                        public void onFailure() {
                            // Startup failed.
                        }
                    });
                }
            });
        }
    }

    /**
     * Get BrowserStartupController instance, create a new one if no existing.
     *
     * @return BrowserStartupController instance.
     */
    public static BrowserStartupController getInstance() {
        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
        ThreadUtils.assertOnUiThread();
        if (sInstance == null) {
            sInstance = new BrowserStartupControllerImpl();
        }
        return sInstance;
    }

    public static void overrideInstanceForTest(BrowserStartupController controller) {
        sInstance = (BrowserStartupControllerImpl) controller;
    }

    @Override
    public void startBrowserProcessesAsync(@LibraryProcessType int libraryProcessType,
            boolean startGpuProcess, boolean startMinimalBrowser, final StartupCallback callback) {
        assert !LibraryLoader.isBrowserProcessStartupBlockedForTesting();
        assertProcessTypeSupported(libraryProcessType);
        assert ThreadUtils.runningOnUiThread() : "Tried to start the browser on the wrong thread.";
        ServicificationStartupUma.getInstance().record(ServicificationStartupUma.getStartupMode(
                mFullBrowserStartupDone, mMinimalBrowserStarted, startMinimalBrowser));

        if (mFullBrowserStartupDone || (startMinimalBrowser && mMinimalBrowserStarted)) {
            // Browser process initialization has already been completed, so we can immediately post
            // the callback.
            postStartupCompleted(callback);
            return;
        }

        // Browser process has not been fully started yet, so we defer executing the callback.
        if (startMinimalBrowser) {
            mMinimalBrowserStartedCallbacks.add(callback);
        } else {
            mAsyncStartupCallbacks.add(callback);
        }
        // If a minimal browser process is launched, we need to relaunch the full process in
        // minimalBrowserStarted() if such a request was received.
        mLaunchFullBrowserAfterMinimalBrowserStart |=
                (mCurrentBrowserStartType == BrowserStartType.MINIMAL_BROWSER)
                && !startMinimalBrowser;
        if (!mHasStartedInitializingBrowserProcess) {
            // This is the first time we have been asked to start the browser process. We set the
            // flag that indicates that we have kicked off starting the browser process.
            mHasStartedInitializingBrowserProcess = true;
            sShouldStartGpuProcessOnBrowserStartup |= startGpuProcess;

            // Start-up at this point occurs before the first frame of the app is drawn. Although
            // contentStart() can be called eagerly, deferring it would allow a frame to be drawn,
            // so that Android reports Chrome to start before our SurfaceView has rendered. Our
            // metrics have also adapted to this. Therefore we wrap contentStart() into Runnable,
            // and let prepareToStartBrowserProcess() decide whether to defer it by a frame (in
            // production) or not (overridden in tests). http://b/181151614#comment6
            prepareToStartBrowserProcess(false, new Runnable() {
                @Override
                public void run() {
                    ThreadUtils.assertOnUiThread();
                    if (mHasCalledContentStart) return;
                    mCurrentBrowserStartType = startMinimalBrowser
                            ? BrowserStartType.MINIMAL_BROWSER
                            : BrowserStartType.FULL_BROWSER;
                    if (contentStart() > 0) {
                        // Failed. The callbacks may not have run, so run them.
                        enqueueCallbackExecutionOnStartupFailure();
                    }
                }
            });

        } else if (mMinimalBrowserStarted && mLaunchFullBrowserAfterMinimalBrowserStart) {
            // If we missed the minimalBrowserStarted() call, launch the full browser now if needed.
            // Otherwise, minimalBrowserStarted() will handle the full browser launch.
            mCurrentBrowserStartType = BrowserStartType.FULL_BROWSER;
            if (contentStart() > 0) enqueueCallbackExecutionOnStartupFailure();
        }
    }

    @Override
    public void startBrowserProcessesSync(@LibraryProcessType int libraryProcessType,
            boolean singleProcess, boolean startGpuProcess) {
        assert !LibraryLoader.isBrowserProcessStartupBlockedForTesting();
        assertProcessTypeSupported(libraryProcessType);

        sShouldStartGpuProcessOnBrowserStartup |= startGpuProcess;

        ServicificationStartupUma.getInstance().record(ServicificationStartupUma.getStartupMode(
                mFullBrowserStartupDone, mMinimalBrowserStarted, false /* startMinimalBrowser */));

        // If already started skip to checking the result
        if (!mFullBrowserStartupDone) {
            // contentStart() need not be deferred, so passing null.
            prepareToStartBrowserProcess(singleProcess, null /* deferrableTask */);

            boolean startedSuccessfully = true;
            if (!mHasCalledContentStart
                    || mCurrentBrowserStartType == BrowserStartType.MINIMAL_BROWSER) {
                mCurrentBrowserStartType = BrowserStartType.FULL_BROWSER;
                if (contentStart() > 0) {
                    // Failed. The callbacks may not have run, so run them.
                    enqueueCallbackExecutionOnStartupFailure();
                    startedSuccessfully = false;
                }
            }
            if (startedSuccessfully) {
                flushStartupTasks();
            }
        }

        // Startup should now be complete
        assert mFullBrowserStartupDone;
        if (!mStartupSuccess) {
            throw new ProcessInitException(LoaderErrors.NATIVE_STARTUP_FAILED);
        }
    }

    /**
     * Start the browser process by calling ContentMain.start().
     */
    int contentStart() {
        int result = 0;
        if (mContentMainCallbackForTests == null) {
            boolean startMinimalBrowser =
                    mCurrentBrowserStartType == BrowserStartType.MINIMAL_BROWSER;
            result = contentMainStart(startMinimalBrowser);
            // No need to launch the full browser again if we are launching full browser now.
            if (!startMinimalBrowser) mLaunchFullBrowserAfterMinimalBrowserStart = false;
        } else {
            assert mCurrentBrowserStartType == BrowserStartType.FULL_BROWSER;
            // Run the injected Runnable instead of ContentMain().
            mContentMainCallbackForTests.run();
            mLaunchFullBrowserAfterMinimalBrowserStart = false;
        }
        mHasCalledContentStart = true;
        return result;
    }

    @Override
    public void setContentMainCallbackForTests(Runnable r) {
        assert !mHasCalledContentStart;
        mContentMainCallbackForTests = r;
    }

    /**
     * Wrap ContentMain.start() for testing.
     */
    @VisibleForTesting
    int contentMainStart(boolean startMinimalBrowser) {
        return ContentMain.start(startMinimalBrowser);
    }

    @VisibleForTesting
    void flushStartupTasks() {
        BrowserStartupControllerImplJni.get().flushStartupTasks();
    }

    @Override
    public boolean isFullBrowserStarted() {
        ThreadUtils.assertOnUiThread();
        return mFullBrowserStartupDone && mStartupSuccess;
    }

    @Override
    public boolean isRunningInMinimalBrowserMode() {
        ThreadUtils.assertOnUiThread();
        return mMinimalBrowserStarted && !mFullBrowserStartupDone && mStartupSuccess;
    }

    @Override
    public boolean isNativeStarted() {
        ThreadUtils.assertOnUiThread();
        return (mMinimalBrowserStarted || mFullBrowserStartupDone) && mStartupSuccess;
    }

    @Override
    public void addStartupCompletedObserver(StartupCallback callback) {
        ThreadUtils.assertOnUiThread();
        if (mFullBrowserStartupDone) {
            postStartupCompleted(callback);
        } else {
            mAsyncStartupCallbacks.add(callback);
        }
    }
    @Override
    public @ServicificationStartup int getStartupMode(boolean startMinimalBrowser) {
        return ServicificationStartupUma.getStartupMode(
                mFullBrowserStartupDone, mMinimalBrowserStarted, startMinimalBrowser);
    }

    /**
     * Asserts that library process type is one of the supported types.
     * @param libraryProcessType the type of process the shared library is loaded. It must be
     *                           LibraryProcessType.PROCESS_BROWSER,
     *                           LibraryProcessType.PROCESS_WEBVIEW or
     *                           LibraryProcessType.PROCESS_WEBLAYER.
     */
    private void assertProcessTypeSupported(@LibraryProcessType int libraryProcessType) {
        assert LibraryProcessType.PROCESS_BROWSER == libraryProcessType
                || LibraryProcessType.PROCESS_WEBVIEW == libraryProcessType
                || LibraryProcessType.PROCESS_WEBLAYER == libraryProcessType;
        LibraryLoader.getInstance().assertCompatibleProcessType(libraryProcessType);
    }

    /**
     * Called when the minimal browser environment is done initializing.
     */
    private void minimalBrowserStarted() {
        mMinimalBrowserStarted = true;
        if (mLaunchFullBrowserAfterMinimalBrowserStart) {
            // If startFullBrowser() fails, execute the callbacks right away. Otherwise,
            // callbacks will be deferred until browser startup completes.
            mCurrentBrowserStartType = BrowserStartType.FULL_BROWSER;
            if (contentStart() > 0) enqueueCallbackExecutionOnStartupFailure();
            return;
        }

        if (mCurrentBrowserStartType == BrowserStartType.MINIMAL_BROWSER) {
            executeMinimalBrowserStartupCallbacks(STARTUP_SUCCESS);
        }
        recordStartupUma();
    }

    private void executeEnqueuedCallbacks(int startupResult) {
        assert ThreadUtils.runningOnUiThread() : "Callback from browser startup from wrong thread.";
        mFullBrowserStartupDone = true;
        mStartupSuccess = (startupResult <= 0);
        for (StartupCallback asyncStartupCallback : mAsyncStartupCallbacks) {
            if (mStartupSuccess) {
                asyncStartupCallback.onSuccess();
            } else {
                asyncStartupCallback.onFailure();
            }
        }
        // We don't want to hold on to any objects after we do not need them anymore.
        mAsyncStartupCallbacks.clear();

        executeMinimalBrowserStartupCallbacks(startupResult);
        recordStartupUma();
    }

    private void executeMinimalBrowserStartupCallbacks(int startupResult) {
        mStartupSuccess = (startupResult <= 0);
        for (StartupCallback callback : mMinimalBrowserStartedCallbacks) {
            if (mStartupSuccess) {
                callback.onSuccess();
            } else {
                callback.onFailure();
            }
        }
        mMinimalBrowserStartedCallbacks.clear();
    }

    // Post a task to tell the callbacks that startup failed. Since the execution clears the
    // callback lists, it is safe to call this more than once.
    private void enqueueCallbackExecutionOnStartupFailure() {
        PostTask.postTask(TaskTraits.UI_DEFAULT, () -> executeEnqueuedCallbacks(STARTUP_FAILURE));
    }

    private void postStartupCompleted(final StartupCallback callback) {
        PostTask.postTask(TaskTraits.UI_DEFAULT, new Runnable() {
            @Override
            public void run() {
                if (mStartupSuccess) {
                    callback.onSuccess();
                } else {
                    callback.onFailure();
                }
            }
        });
    }

    @VisibleForTesting
    void prepareToStartBrowserProcess(final boolean singleProcess, final Runnable deferrableTask) {
        if (mPrepareToStartCompleted) {
            return;
        }
        Log.d(TAG, "Initializing chromium process, singleProcess=%b", singleProcess);
        mPrepareToStartCompleted = true;
        try (ScopedSysTraceEvent e = ScopedSysTraceEvent.scoped("prepareToStartBrowserProcess")) {
            // This strictmode exception is to cover the case where the browser process is being
            // started asynchronously but not in the main browser flow.  The main browser flow
            // will trigger library loading earlier and this will be a no-op, but in the other
            // cases this will need to block on loading libraries. This applies to tests and
            // ManageSpaceActivity, which can be launched from Settings.
            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
            try {
                // Normally Main.java will have already loaded the library asynchronously, we
                // only need to load it here if we arrived via another flow, e.g. bookmark
                // access & sync setup.
                LibraryLoader.getInstance().ensureInitialized();
            } finally {
                StrictMode.setThreadPolicy(oldPolicy);
            }

            // TODO(yfriedman): Remove dependency on a command line flag for this.
            DeviceUtilsImpl.addDeviceSpecificUserAgentSwitch();
            BrowserStartupControllerImplJni.get().setCommandLineFlags(singleProcess);
        }

        if (deferrableTask != null) {
            PostTask.postTask(TaskTraits.UI_USER_BLOCKING, deferrableTask);
        }
    }

    /**
     * Can be overridden by testing.
     */
    @VisibleForTesting
    void recordStartupUma() {
        ServicificationStartupUma.getInstance().commit();
    }

    @NativeMethods
    interface Natives {
        void setCommandLineFlags(boolean singleProcess);

        void flushStartupTasks();
    }
}
