blob: dd496ccd271c559d4bcec1c00eaa51c0bb2cbe04 [file] [log] [blame]
// Copyright 2016 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.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.SharedPreferences;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ApplicationStateListener;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.LocaleUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.accessibility.FontSizePrefs;
import org.chromium.chrome.browser.browsing_data.BrowsingDataType;
import org.chromium.chrome.browser.browsing_data.TimePeriod;
import org.chromium.chrome.browser.metrics.UmaUtils;
import org.chromium.chrome.browser.metrics.VariationsSession;
import org.chromium.chrome.browser.notifications.NotificationPlatformBridge;
import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.chrome.browser.preferences.privacy.BrowsingDataBridge;
import org.chromium.chrome.browser.profiles.ProfileManagerUtils;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.util.FeatureUtilities;
import java.util.Locale;
/**
* Tracks the foreground session state for the Chrome activities.
*/
public class ChromeActivitySessionTracker {
private static final String PREF_LOCALE = "locale";
@SuppressLint("StaticFieldLeak")
private static ChromeActivitySessionTracker sInstance;
private final PowerBroadcastReceiver mPowerBroadcastReceiver = new PowerBroadcastReceiver();
// Used to trigger variation changes (such as seed fetches) upon application foregrounding.
private VariationsSession mVariationsSession;
private Application mApplication;
private boolean mIsInitialized;
private boolean mIsStarted;
private boolean mIsFinishedCachingNativeFlags;
/**
* @return The activity session tracker for Chrome.
*/
public static ChromeActivitySessionTracker getInstance() {
ThreadUtils.assertOnUiThread();
if (sInstance == null) sInstance = new ChromeActivitySessionTracker();
return sInstance;
}
/**
* Constructor exposed for extensibility only.
* @see #getInstance()
*/
protected ChromeActivitySessionTracker() {
mApplication = (Application) ContextUtils.getApplicationContext();
mVariationsSession = AppHooks.get().createVariationsSession();
}
/**
* Asynchronously returns the value of the "restrict" URL param that the variations service
* should use for variation seed requests.
* @param callback Callback that will be called with the param value when available.
*/
public void getVariationsRestrictModeValue(Callback<String> callback) {
mVariationsSession.getRestrictModeValue(mApplication, callback);
}
/**
* @return The latest country according to the current variations state. Null if not available.
*/
@Nullable
public String getVariationsLatestCountry() {
return mVariationsSession.getLatestCountry();
}
/**
* Handle any initialization that occurs once native has been loaded.
*/
public void initializeWithNative() {
ThreadUtils.assertOnUiThread();
if (mIsInitialized) return;
mIsInitialized = true;
assert !mIsStarted;
ApplicationStatus.registerApplicationStateListener(createApplicationStateListener());
}
/**
* Each top-level activity (those extending {@link ChromeActivity}) should call this during
* its onStart phase. When called for the first time, this marks the beginning of a foreground
* session and calls onForegroundSessionStart(). Subsequent calls are noops until
* onForegroundSessionEnd() is called, to handle changing top-level Chrome activities in one
* foreground session.
*/
public void onStartWithNative() {
ThreadUtils.assertOnUiThread();
if (mIsStarted) return;
mIsStarted = true;
assert mIsInitialized;
onForegroundSessionStart();
cacheNativeFlags();
}
/**
* Called when a top-level Chrome activity (ChromeTabbedActivity, CustomTabActivity) is
* started in foreground. It will not be called again when other Chrome activities take over
* (see onStart()), that is, when correct activity calls startActivity() for another Chrome
* activity.
*/
private void onForegroundSessionStart() {
UmaUtils.recordForegroundStartTime();
updatePasswordEchoState();
FontSizePrefs.getInstance(mApplication).onSystemFontScaleChanged();
recordWhetherSystemAndAppLanguagesDiffer();
updateAcceptLanguages();
mVariationsSession.start(mApplication);
mPowerBroadcastReceiver.onForegroundSessionStart();
// Track the ratio of Chrome startups that are caused by notification clicks.
// TODO(johnme): Add other reasons (and switch to recordEnumeratedHistogram).
RecordHistogram.recordBooleanHistogram(
"Startup.BringToForegroundReason",
NotificationPlatformBridge.wasNotificationRecentlyClicked());
}
/**
* Called when last of Chrome activities is stopped, ending the foreground session. This will
* not be called when a Chrome activity is stopped because another Chrome activity takes over.
* This is ensured by ActivityStatus, which switches to track new activity when its started and
* will not report the old one being stopped (see createStateListener() below).
*/
private void onForegroundSessionEnd() {
if (!mIsStarted) return;
UmaUtils.recordBackgroundTime();
ProfileManagerUtils.flushPersistentDataForAllProfiles();
mIsStarted = false;
mPowerBroadcastReceiver.onForegroundSessionEnd();
IntentHandler.clearPendingReferrer();
IntentHandler.clearPendingIncognitoUrl();
int totalTabCount = 0;
for (Activity activity : ApplicationStatus.getRunningActivities()) {
if (activity instanceof ChromeActivity) {
TabModelSelector tabModelSelector =
((ChromeActivity) activity).getTabModelSelector();
if (tabModelSelector != null) {
totalTabCount += tabModelSelector.getTotalTabCount();
}
}
}
RecordHistogram.recordCountHistogram(
"Tab.TotalTabCount.BeforeLeavingApp", totalTabCount);
}
private void onForegroundActivityDestroyed() {
if (ApplicationStatus.isEveryActivityDestroyed()) {
// These will all be re-initialized when a new Activity starts / upon next use.
PartnerBrowserCustomizations.destroy();
ShareHelper.clearSharedImages();
}
}
private ApplicationStateListener createApplicationStateListener() {
return newState -> {
if (newState == ApplicationState.HAS_STOPPED_ACTIVITIES) {
onForegroundSessionEnd();
} else if (newState == ApplicationState.HAS_DESTROYED_ACTIVITIES) {
onForegroundActivityDestroyed();
}
};
}
/**
* Update the accept languages after changing Android locale setting. Doing so kills the
* Activities but it doesn't kill the Application, so this should be called in
* {@link #onStart} instead of {@link #initialize}.
*/
private void updateAcceptLanguages() {
String localeString = LocaleUtils.getDefaultLocaleListString();
if (hasLocaleChanged(localeString)) {
// Clear cache so that accept-languages change can be applied immediately.
// TODO(changwan): The underlying BrowsingDataRemover::Remove() is an asynchronous call.
// So cache-clearing may not be effective if URL rendering can happen before
// OnBrowsingDataRemoverDone() is called, in which case we may have to reload as well.
// Check if it can happen.
BrowsingDataBridge.getInstance().clearBrowsingData(
null, new int[] {BrowsingDataType.CACHE}, TimePeriod.ALL_TIME);
}
}
private boolean hasLocaleChanged(String newLocale) {
String previousLocale = ContextUtils.getAppSharedPreferences().getString(PREF_LOCALE, null);
if (!TextUtils.equals(previousLocale, newLocale)) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_LOCALE, newLocale);
editor.apply();
PrefServiceBridge.getInstance().resetAcceptLanguages(newLocale);
// We consider writing the initial value to prefs as _not_ changing the locale.
return previousLocale != null;
}
return false;
}
/**
* Honor the Android system setting about showing the last character of a password for a short
* period of time.
*/
private void updatePasswordEchoState() {
boolean systemEnabled = Settings.System.getInt(mApplication.getContentResolver(),
Settings.System.TEXT_SHOW_PASSWORD, 1) == 1;
if (PrefServiceBridge.getInstance().getPasswordEchoEnabled() == systemEnabled) return;
PrefServiceBridge.getInstance().setPasswordEchoEnabled(systemEnabled);
}
/**
* Caches flags that are needed by Activities that launch before the native library is loaded
* and stores them in SharedPreferences. Because this function is called during launch after the
* library has loaded, they won't affect the next launch until Chrome is restarted.
*/
private void cacheNativeFlags() {
if (mIsFinishedCachingNativeFlags) return;
FeatureUtilities.cacheNativeFlags();
mIsFinishedCachingNativeFlags = true;
}
/**
* Records whether Chrome was started in a language other than the system language but we
* support the system language. That can happen if the user changes the system language and the
* required language split cannot be installed in time.
*/
private void recordWhetherSystemAndAppLanguagesDiffer() {
String uiLanguage =
LocaleUtils.toLanguage(ChromeLocalizationUtils.getUiLocaleStringForCompressedPak());
String systemLanguage =
LocaleUtils.toLanguage(LocaleUtils.toLanguageTag(Locale.getDefault()));
boolean isWrongLanguage = !systemLanguage.equals(uiLanguage)
&& LocaleUtils.isLanguageSupported(systemLanguage);
RecordHistogram.recordBooleanHistogram(
"Android.Language.WrongLanguageAfterResume", isWrongLanguage);
}
/**
* @return The PowerBroadcastReceiver for the browser process.
*/
@VisibleForTesting
public PowerBroadcastReceiver getPowerBroadcastReceiverForTesting() {
return mPowerBroadcastReceiver;
}
}