// 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.chrome.browser;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Looper;
import android.os.MessageQueue;
import android.os.SystemClock;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;

import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.FieldTrialList;
import org.chromium.base.PowerMonitor;
import org.chromium.base.SysUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.bookmarkswidget.BookmarkWidgetProvider;
import org.chromium.chrome.browser.crash.MinidumpUploadService;
import org.chromium.chrome.browser.init.ProcessInitializationHandler;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.media.MediaCaptureNotificationService;
import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.partnerbookmarks.PartnerBookmarksShim;
import org.chromium.chrome.browser.partnercustomizations.HomepageManager;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.physicalweb.PhysicalWeb;
import org.chromium.chrome.browser.precache.PrecacheLauncher;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
import org.chromium.chrome.browser.preferences.privacy.PrivacyPreferencesManager;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.chrome.browser.webapps.WebApkVersionManager;
import org.chromium.chrome.browser.webapps.WebappRegistry;
import org.chromium.components.minidump_uploader.CrashFileManager;
import org.chromium.content.browser.ChildProcessLauncher;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

/**
 * Handler for application level tasks to be completed on deferred startup.
 */
public class DeferredStartupHandler {
    private static final String TAG = "DeferredStartupHandler";
    /** Prevents race conditions when deleting snapshot database. */
    private static final Object SNAPSHOT_DATABASE_LOCK = new Object();
    private static final String SNAPSHOT_DATABASE_REMOVED = "snapshot_database_removed";
    private static final String SNAPSHOT_DATABASE_NAME = "snapshots.db";

    private static class Holder {
        private static final DeferredStartupHandler INSTANCE = new DeferredStartupHandler();
    }

    private boolean mDeferredStartupInitializedForApp;
    private boolean mDeferredStartupCompletedForApp;
    private long mDeferredStartupDuration;
    private long mMaxTaskDuration;
    private final Context mAppContext;

    private final Queue<Runnable> mDeferredTasks;

    /**
     * This class is an application specific object that handles the deferred startup.
     * @return The singleton instance of {@link DeferredStartupHandler}.
     */
    public static DeferredStartupHandler getInstance() {
        return Holder.INSTANCE;
    }

    private DeferredStartupHandler() {
        mAppContext = ContextUtils.getApplicationContext();
        mDeferredTasks = new LinkedList<>();
    }

    /**
     * Add the idle handler which will run deferred startup tasks in sequence when idle. This can
     * be called multiple times by different activities to schedule their own deferred startup
     * tasks.
     */
    public void queueDeferredTasksOnIdleHandler() {
        mMaxTaskDuration = 0;
        mDeferredStartupDuration = 0;
        Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
            @Override
            public boolean queueIdle() {
                Runnable currentTask = mDeferredTasks.poll();
                if (currentTask == null) {
                    if (mDeferredStartupInitializedForApp) {
                        mDeferredStartupCompletedForApp = true;
                        recordDeferredStartupStats();
                    }
                    return false;
                }

                long startTime = SystemClock.uptimeMillis();
                currentTask.run();
                long timeTaken = SystemClock.uptimeMillis() - startTime;

                mMaxTaskDuration = Math.max(mMaxTaskDuration, timeTaken);
                mDeferredStartupDuration += timeTaken;
                return true;
            }
        });
    }

    private void recordDeferredStartupStats() {
        RecordHistogram.recordLongTimesHistogram(
                "UMA.Debug.EnableCrashUpload.DeferredStartUpDuration",
                mDeferredStartupDuration,
                TimeUnit.MILLISECONDS);
        RecordHistogram.recordLongTimesHistogram(
                "UMA.Debug.EnableCrashUpload.DeferredStartUpMaxTaskDuration",
                mMaxTaskDuration,
                TimeUnit.MILLISECONDS);
        RecordHistogram.recordLongTimesHistogram(
                "UMA.Debug.EnableCrashUpload.DeferredStartUpCompleteTime",
                SystemClock.uptimeMillis() - UmaUtils.getForegroundStartTime(),
                TimeUnit.MILLISECONDS);
        LocaleManager.getInstance().recordStartupMetrics();
    }

    /**
     * Adds a single deferred task to the queue. The caller is responsible for calling
     * queueDeferredTasksOnIdleHandler after adding tasks.
     *
     * @param deferredTask The tasks to be run.
     */
    public void addDeferredTask(Runnable deferredTask) {
        ThreadUtils.assertOnUiThread();
        mDeferredTasks.add(deferredTask);
    }

    /**
     * Handle application level deferred startup tasks that can be lazily done after all
     * the necessary initialization has been completed. Any calls requiring network access should
     * probably go here.
     *
     * Keep these tasks short and break up long tasks into multiple smaller tasks, as they run on
     * the UI thread and are blocking. Remember to follow RAIL guidelines, as much as possible, and
     * that most devices are quite slow, so leave enough buffer.
     */
    @UiThread
    public void initDeferredStartupForApp() {
        if (mDeferredStartupInitializedForApp) return;
        mDeferredStartupInitializedForApp = true;
        ThreadUtils.assertOnUiThread();

        RecordHistogram.recordLongTimesHistogram(
                "UMA.Debug.EnableCrashUpload.DeferredStartUptime2",
                SystemClock.uptimeMillis() - UmaUtils.getForegroundStartTime(),
                TimeUnit.MILLISECONDS);

        mDeferredTasks.add(new Runnable() {
            @Override
            public void run() {
                // Punt all tasks that may block on disk off onto a background thread.
                initAsyncDiskTask();

                AfterStartupTaskUtils.setStartupComplete();

                PartnerBrowserCustomizations.setOnInitializeAsyncFinished(new Runnable() {
                    @Override
                    public void run() {
                        String homepageUrl = HomepageManager.getHomepageUri(mAppContext);
                        LaunchMetrics.recordHomePageLaunchMetrics(
                                HomepageManager.isHomepageEnabled(mAppContext),
                                NewTabPage.isNTPUrl(homepageUrl), homepageUrl);
                    }
                });

                PartnerBookmarksShim.kickOffReading(mAppContext);

                PowerMonitor.create(mAppContext);

                ShareHelper.clearSharedImages();

                OfflinePageUtils.clearSharedOfflineFiles(mAppContext);
            }
        });

        mDeferredTasks.add(new Runnable() {
            @Override
            public void run() {
                // Clear any media notifications that existed when Chrome was last killed.
                MediaCaptureNotificationService.clearMediaNotifications(mAppContext);

                startModerateBindingManagementIfNeeded();

                recordKeyboardLocaleUma();
            }
        });

        mDeferredTasks.add(new Runnable() {
            @Override
            public void run() {
                // Start or stop Physical Web
                PhysicalWeb.onChromeStart();
            }
        });

        final ChromeApplication application = (ChromeApplication) mAppContext;

        mDeferredTasks.add(new Runnable() {
            @Override
            public void run() {
                // Starts syncing with GSA.
                application.createGsaHelper().startSync();
            }
        });

        ProcessInitializationHandler.getInstance().initializeDeferredStartupTasks();
    }

    private void initAsyncDiskTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                try {
                    TraceEvent.begin("ChromeBrowserInitializer.onDeferredStartup.doInBackground");
                    long asyncTaskStartTime = SystemClock.uptimeMillis();

                    // Initialize the WebappRegistry if it's not already initialized. Must be in
                    // async task due to shared preferences disk access on N.
                    WebappRegistry.getInstance();

                    boolean crashDumpDisabled = CommandLine.getInstance().hasSwitch(
                            ChromeSwitches.DISABLE_CRASH_DUMP_UPLOAD);
                    if (!crashDumpDisabled) {
                        RecordHistogram.recordLongTimesHistogram(
                                "UMA.Debug.EnableCrashUpload.Uptime3",
                                asyncTaskStartTime - UmaUtils.getForegroundStartTime(),
                                TimeUnit.MILLISECONDS);
                        PrivacyPreferencesManager.getInstance().enablePotentialCrashUploading();
                        MinidumpUploadService.tryUploadAllCrashDumps(mAppContext);
                    }
                    CrashFileManager crashFileManager =
                            new CrashFileManager(mAppContext.getCacheDir());
                    crashFileManager.cleanOutAllNonFreshMinidumpFiles();

                    MinidumpUploadService.storeBreakpadUploadStatsInUma(
                            ChromePreferenceManager.getInstance(mAppContext));

                    // Force a widget refresh in order to wake up any possible zombie widgets.
                    // This is needed to ensure the right behavior when the process is suddenly
                    // killed.
                    BookmarkWidgetProvider.refreshAllWidgets(mAppContext);

                    // Initialize whether or not precaching is enabled.
                    PrecacheLauncher.updatePrecachingEnabled(mAppContext);

                    if (ChromeWebApkHost.isEnabled()) {
                        WebApkVersionManager.updateWebApksIfNeeded();
                    }

                    removeSnapshotDatabase();

                    cacheIsChromeDefaultBrowser();

                    RecordHistogram.recordLongTimesHistogram(
                            "UMA.Debug.EnableCrashUpload.DeferredStartUpDurationAsync",
                            SystemClock.uptimeMillis() - asyncTaskStartTime,
                            TimeUnit.MILLISECONDS);

                    // Warm up all web app shared prefs. This must be run after the WebappRegistry
                    // instance is initialized.
                    WebappRegistry.warmUpSharedPrefs();

                    return null;
                } finally {
                    TraceEvent.end("ChromeBrowserInitializer.onDeferredStartup.doInBackground");
                }
            }

            @Override
            protected void onPostExecute(Void params) {
                // Must be run on the UI thread after the WebappRegistry has been completely warmed.
                WebappRegistry.getInstance().unregisterOldWebapps(System.currentTimeMillis());
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    private void startModerateBindingManagementIfNeeded() {
        // Moderate binding doesn't apply to low end devices.
        if (SysUtils.isLowEndDevice()) return;

        boolean moderateBindingTillBackgrounded =
                FieldTrialList.findFullName("ModerateBindingOnBackgroundTabCreation")
                        .equals("Enabled");
        ChildProcessLauncher.startModerateBindingManagement(
                mAppContext, moderateBindingTillBackgrounded);
    }

    /**
     * Caches whether Chrome is set as a default browser on the device.
     */
    @WorkerThread
    private void cacheIsChromeDefaultBrowser() {
        // Retrieve whether Chrome is default in background to avoid strict mode checks.
        Intent intent = new Intent(Intent.ACTION_VIEW,
                Uri.parse("http://www.madeupdomainforcheck123.com/"));
        ResolveInfo info = mAppContext.getPackageManager().resolveActivity(intent, 0);
        boolean isDefault = (info != null && info.match != 0
                && mAppContext.getPackageName().equals(info.activityInfo.packageName));
        ChromePreferenceManager.getInstance(mAppContext).setCachedChromeDefaultBrowser(isDefault);
    }

    /**
     * Deletes the snapshot database which is no longer used because the feature has been removed
     * in Chrome M41.
     */
    @WorkerThread
    private void removeSnapshotDatabase() {
        synchronized (SNAPSHOT_DATABASE_LOCK) {
            SharedPreferences prefs =
                    ContextUtils.getAppSharedPreferences();
            if (!prefs.getBoolean(SNAPSHOT_DATABASE_REMOVED, false)) {
                mAppContext.deleteDatabase(SNAPSHOT_DATABASE_NAME);
                prefs.edit().putBoolean(SNAPSHOT_DATABASE_REMOVED, true).apply();
            }
        }
    }

    private void recordKeyboardLocaleUma() {
        InputMethodManager imm =
                (InputMethodManager) mAppContext.getSystemService(Context.INPUT_METHOD_SERVICE);
        List<InputMethodInfo> ims = imm.getEnabledInputMethodList();
        ArrayList<String> uniqueLanguages = new ArrayList<>();
        for (InputMethodInfo method : ims) {
            List<InputMethodSubtype> submethods =
                    imm.getEnabledInputMethodSubtypeList(method, true);
            for (InputMethodSubtype submethod : submethods) {
                if (submethod.getMode().equals("keyboard")) {
                    String language = submethod.getLocale().split("_")[0];
                    if (!uniqueLanguages.contains(language)) {
                        uniqueLanguages.add(language);
                    }
                }
            }
        }
        RecordHistogram.recordCountHistogram("InputMethod.ActiveCount", uniqueLanguages.size());

        InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
        Locale systemLocale = Locale.getDefault();
        if (currentSubtype != null && currentSubtype.getLocale() != null && systemLocale != null) {
            String keyboardLanguage = currentSubtype.getLocale().split("_")[0];
            boolean match = systemLocale.getLanguage().equalsIgnoreCase(keyboardLanguage);
            RecordHistogram.recordBooleanHistogram("InputMethod.MatchesSystemLanguage", match);
        }
    }

    /**
    * @return Whether deferred startup has been completed.
    */
    @VisibleForTesting
    public boolean isDeferredStartupCompleteForApp() {
        return mDeferredStartupCompletedForApp;
    }
}