// 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.app.Activity;
import android.content.SharedPreferences;
import android.provider.Settings;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ApplicationStateListener;
import org.chromium.base.ContextUtils;
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.share.ShareHelper;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.util.FeatureUtilities;
import org.chromium.content.browser.ChildProcessLauncher;
import java.lang.ref.WeakReference;
import java.util.Locale;
/**
* Tracks the foreground session state for the Chrome activities.
*/
public class ChromeActivitySessionTracker {
private static final String PREF_LOCALE = "locale";
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 ChromeApplication 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 = (ChromeApplication) ContextUtils.getApplicationContext();
}
/**
* 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());
mVariationsSession = mApplication.createVariationsSession();
}
/**
* 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, FullscreenActivity) 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();
ChildProcessLauncher.onBroughtToForeground();
updatePasswordEchoState();
FontSizePrefs.getInstance(mApplication).onSystemFontScaleChanged();
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;
ChromeApplication.flushPersistentData();
mIsStarted = false;
mPowerBroadcastReceiver.onForegroundSessionEnd();
ChildProcessLauncher.onSentToBackground();
IntentHandler.clearPendingReferrer();
IntentHandler.clearPendingIncognitoUrl();
int totalTabCount = 0;
for (WeakReference<Activity> reference : ApplicationStatus.getRunningActivities()) {
Activity activity = reference.get();
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 new ApplicationStateListener() {
@Override
public void onApplicationStateChange(int 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() {
PrefServiceBridge instance = PrefServiceBridge.getInstance();
String localeString = Locale.getDefault().toString(); // ex) en_US, de_DE, zh_CN_#Hans
if (hasLocaleChanged(localeString)) {
instance.resetAcceptLanguages(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.
instance.clearBrowsingData(
null, new int[]{ BrowsingDataType.CACHE }, TimePeriod.ALL_TIME);
}
}
private boolean hasLocaleChanged(String newLocale) {
String previousLocale = ContextUtils.getAppSharedPreferences().getString(
PREF_LOCALE, "");
if (!previousLocale.equals(newLocale)) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(PREF_LOCALE, newLocale);
editor.apply();
return true;
}
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;
}
/**
* @return The PowerBroadcastReceiver for the browser process.
*/
@VisibleForTesting
public PowerBroadcastReceiver getPowerBroadcastReceiverForTesting() {
return mPowerBroadcastReceiver;
}
}