// 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.CrashFileManager;
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.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();
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);
return null;
} finally {
TraceEvent.end("ChromeBrowserInitializer.onDeferredStartup.doInBackground");
}
}
}.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;
}
}