Commit c2d10739 authored by Justin DeWitt's avatar Justin DeWitt Committed by Commit Bot

Add Chrome Inactivity Tracker.

This factors out the code that saves and reads the preference for
the last suspended time, so that other activities besides
ChromeTabbedActivity can use it.

Bug: 941192
Change-Id: I42fb24d2b1a158f5557dfdd7fe6dcce8a53e8f2e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1534358
Commit-Queue: Justin DeWitt <dewittj@chromium.org>
Reviewed-by: default avatarMichael Thiessen <mthiesse@chromium.org>
Reviewed-by: default avatarYusuf Ozuysal <yusufo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#646521}
parent 9e5b1cfe
......@@ -27,6 +27,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/ChromeBaseAppCompatActivity.java",
"java/src/org/chromium/chrome/browser/ChromeFeatureList.java",
"java/src/org/chromium/chrome/browser/ChromeHttpAuthHandler.java",
"java/src/org/chromium/chrome/browser/ChromeInactivityTracker.java",
"java/src/org/chromium/chrome/browser/ChromeKeyboardVisibilityDelegate.java",
"java/src/org/chromium/chrome/browser/ChromeLocalizationUtils.java",
"java/src/org/chromium/chrome/browser/ChromeStrictMode.java",
......
// Copyright 2019 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.text.format.DateUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.init.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.Destroyable;
import org.chromium.chrome.browser.lifecycle.StartStopWithNativeObserver;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/**
* Manages pref that can track the delay since the last stop of the tracked activity.
*/
public class ChromeInactivityTracker implements StartStopWithNativeObserver, Destroyable {
private static final String TAG = "InactivityTracker";
private static final String FEATURE_NAME = ChromeFeatureList.NTP_LAUNCH_AFTER_INACTIVITY;
private static final String NTP_LAUNCH_DELAY_IN_MINS_PARAM = "delay_in_mins";
private static final long UNKNOWN_LAST_BACKGROUNDED_TIME = -1;
private static final int UNKNOWN_LAUNCH_DELAY_MINS = -1;
private static final int DEFAULT_LAUNCH_DELAY_IN_MINS = 5;
// Only true if the feature is enabled.
private final boolean mIsEnabled;
private final String mPrefName;
private int mNtpLaunchDelayInMins = 1;
private final ActivityLifecycleDispatcher mLifecycleDispatcher;
private final Runnable mInactiveCallback;
private CancelableRunnableTask mCurrentlyPostedInactiveCallback;
private static class CancelableRunnableTask implements Runnable {
private boolean mIsRunnable = true;
private final Runnable mTask;
private CancelableRunnableTask(Runnable task) {
mTask = task;
}
@Override
public void run() {
if (mIsRunnable) {
mTask.run();
}
}
public void cancel() {
mIsRunnable = false;
}
}
/**
* Creates an inactivity tracker without a timeout callback. This is useful if clients only
* want to query the inactivity state manually.
* @param prefName the location in shared preferences that the timestamp is stored.
* @param lifecycleDispatcher tracks the lifecycle of the Activity of interest, and calls
* observer methods on ChromeInactivityTracker.
*/
public ChromeInactivityTracker(
String prefName, ActivityLifecycleDispatcher lifecycleDispatcher) {
this(prefName, lifecycleDispatcher, () -> {});
}
/**
* Creates an inactivity tracker that stores a timestamp in prefs, and sets a timeout. If the
* timeout expires while the activity that is tracked is still stopped, then the callback is
* executed. If the activity otherwise starts up, it can check whether the timeout has expired
* using #inactivityThresholdPassed and perform the appropriate behavior.
* @param prefName the location in shared preferences that the timestamp is stored.
* @param lifecycleDispatcher tracks the lifecycle of the Activity of interest, and calls
* observer methods on ChromeInactivityTracker.
* @param inactiveCallback called if the activity is stopped for longer than the configured
* inactivity timeout.
*/
public ChromeInactivityTracker(String prefName, ActivityLifecycleDispatcher lifecycleDispatcher,
Runnable inactiveCallback) {
mPrefName = prefName;
mInactiveCallback = inactiveCallback;
mNtpLaunchDelayInMins = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
FEATURE_NAME, NTP_LAUNCH_DELAY_IN_MINS_PARAM, DEFAULT_LAUNCH_DELAY_IN_MINS);
mIsEnabled = ChromeFeatureList.isEnabled(FEATURE_NAME);
if (mIsEnabled) {
mLifecycleDispatcher = lifecycleDispatcher;
mLifecycleDispatcher.register(this);
} else {
mLifecycleDispatcher = null;
}
}
/**
* Updates the shared preferences to contain the given time. Used internally and for tests.
* @param timeInMillis the time to record.
*/
@VisibleForTesting
public void setLastBackgroundedTimeInPrefs(long timeInMillis) {
ContextUtils.getAppSharedPreferences().edit().putLong(mPrefName, timeInMillis).apply();
}
private long getLastBackgroundedTimeMs() {
return ContextUtils.getAppSharedPreferences().getLong(
mPrefName, UNKNOWN_LAST_BACKGROUNDED_TIME);
}
/**
* @return the time interval in millis since the last backgrounded time.
*/
public long getTimeSinceLastBackgroundedMs() {
long lastBackgroundedTimeMs = getLastBackgroundedTimeMs();
if (lastBackgroundedTimeMs == UNKNOWN_LAST_BACKGROUNDED_TIME) {
return UNKNOWN_LAST_BACKGROUNDED_TIME;
}
return System.currentTimeMillis() - lastBackgroundedTimeMs;
}
/**
* @return true if the timestamp in prefs is older than the configured timeout.
*/
public boolean inactivityThresholdPassed() {
if (!mIsEnabled) {
return false;
}
long lastBackgroundedTimeMs = getLastBackgroundedTimeMs();
if (lastBackgroundedTimeMs == UNKNOWN_LAST_BACKGROUNDED_TIME) return false;
long backgroundDurationMinutes =
getTimeSinceLastBackgroundedMs() / DateUtils.MINUTE_IN_MILLIS;
if (backgroundDurationMinutes < mNtpLaunchDelayInMins) {
Log.i(TAG, "Not launching NTP due to inactivity, background time: %d, launch delay: %d",
backgroundDurationMinutes, mNtpLaunchDelayInMins);
return false;
}
Log.i(TAG, "Forcing NTP due to inactivity.");
return true;
}
@Override
public void onStartWithNative() {
setLastBackgroundedTimeInPrefs(UNKNOWN_LAST_BACKGROUNDED_TIME);
cancelCurrentTask();
}
@Override
public void onStopWithNative() {
Log.i(TAG, "onStop, scheduling for " + mNtpLaunchDelayInMins + " minutes");
cancelCurrentTask();
if (mNtpLaunchDelayInMins == UNKNOWN_LAUNCH_DELAY_MINS) {
Log.w(TAG, "Configured with unknown launch delay, disabling.");
return;
}
mCurrentlyPostedInactiveCallback = new CancelableRunnableTask(mInactiveCallback);
setLastBackgroundedTimeInPrefs(System.currentTimeMillis());
org.chromium.base.task.PostTask.postDelayedTask(UiThreadTaskTraits.DEFAULT,
mCurrentlyPostedInactiveCallback,
mNtpLaunchDelayInMins * DateUtils.MINUTE_IN_MILLIS);
}
@Override
public void destroy() {
mLifecycleDispatcher.unregister(this);
cancelCurrentTask();
}
private void cancelCurrentTask() {
if (mCurrentlyPostedInactiveCallback != null) {
mCurrentlyPostedInactiveCallback.cancel();
mCurrentlyPostedInactiveCallback = null;
}
}
}
......@@ -231,9 +231,7 @@ public class ChromeTabbedActivity
"com.google.android.apps.chrome.ACTION_CLOSE_TABS";
@VisibleForTesting
public static final String LAST_BACKGROUNDED_TIME_MS_PREF =
"ChromeTabbedActivity.BackgroundTimeMs";
private static final String NTP_LAUNCH_DELAY_IN_MINS_PARAM = "delay_in_mins";
static final String LAST_BACKGROUNDED_TIME_MS_PREF = "ChromeTabbedActivity.BackgroundTimeMs";
@VisibleForTesting
public static final String STARTUP_UMA_HISTOGRAM_SUFFIX = ".Tabbed";
......@@ -298,6 +296,11 @@ public class ChromeTabbedActivity
*/
private boolean mCreatedTabOnStartup;
/**
* Keeps track of the pref for the last time since this activity was stopped.
*/
private ChromeInactivityTracker mInactivityTracker;
// Whether or not chrome was launched with an intent to open a tab.
private boolean mIntentWithEffect;
......@@ -834,11 +837,6 @@ public class ChromeTabbedActivity
mTabModelSelectorImpl.saveState();
mActivityStopMetrics.onStopWithNative(this);
ContextUtils.getAppSharedPreferences()
.edit()
.putLong(LAST_BACKGROUNDED_TIME_MS_PREF, System.currentTimeMillis())
.apply();
}
@Override
......@@ -1029,7 +1027,11 @@ public class ChromeTabbedActivity
private void logMainIntentBehavior(Intent intent) {
assert isMainIntentFromLauncher(intent);
mMainIntentMetrics.onMainIntentWithNative(getTimeSinceLastBackgroundedMs());
// TODO(tedchoc): We should cache the last visible time and reuse it to avoid different
// values of this depending on when it is called after the activity was
// shown.
mMainIntentMetrics.onMainIntentWithNative(
mInactivityTracker.getTimeSinceLastBackgroundedMs());
}
/** Access the main intent metrics for test validation. */
......@@ -1038,6 +1040,11 @@ public class ChromeTabbedActivity
return mMainIntentMetrics;
}
@VisibleForTesting
public ChromeInactivityTracker getInactivityTrackerForTesting() {
return mInactivityTracker;
}
/**
* Determines if the intent should trigger an NTP and launches it if applicable. If Chrome Home
* is enabled, we reset the bottom sheet state to half after some time being backgrounded.
......@@ -1050,29 +1057,7 @@ public class ChromeTabbedActivity
if (!mIntentHandler.isIntentUserVisible()) return false;
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_LAUNCH_AFTER_INACTIVITY)) {
return false;
}
int ntpLaunchDelayInMins = ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.NTP_LAUNCH_AFTER_INACTIVITY, NTP_LAUNCH_DELAY_IN_MINS_PARAM, -1);
if (ntpLaunchDelayInMins == -1) {
Log.e(TAG, "No NTP launch delay specified despite enabled field trial");
return false;
}
long lastBackgroundedTimeMs =
ContextUtils.getAppSharedPreferences().getLong(LAST_BACKGROUNDED_TIME_MS_PREF, -1);
if (lastBackgroundedTimeMs == -1) return false;
long backgroundDurationMinutes =
(System.currentTimeMillis() - lastBackgroundedTimeMs) / DateUtils.MINUTE_IN_MILLIS;
if (backgroundDurationMinutes < ntpLaunchDelayInMins) {
Log.i(TAG, "Not launching NTP due to inactivity, background time: %d, launch delay: %d",
backgroundDurationMinutes, ntpLaunchDelayInMins);
return false;
}
if (!mInactivityTracker.inactivityThresholdPassed()) return false;
if (isInOverviewMode() && !isTablet()) {
mOverviewModeController.hideOverview(false);
......@@ -1116,19 +1101,6 @@ public class ChromeTabbedActivity
return true;
}
/**
* Returns the number of milliseconds since Chrome was last backgrounded.
*/
private long getTimeSinceLastBackgroundedMs() {
// TODO(tedchoc): We should cache the last visible time and reuse it to avoid different
// values of this depending on when it is called after the activity was
// shown.
long currentTime = System.currentTimeMillis();
long lastBackgroundedTimeMs = ContextUtils.getAppSharedPreferences().getLong(
LAST_BACKGROUNDED_TIME_MS_PREF, currentTime);
return currentTime - lastBackgroundedTimeMs;
}
@Override
public void initializeState() {
// This method goes through 3 steps:
......@@ -1167,6 +1139,8 @@ public class ChromeTabbedActivity
mTabModelSelectorImpl.loadState(ignoreIncognitoFiles);
}
mInactivityTracker = new ChromeInactivityTracker(
LAST_BACKGROUNDED_TIME_MS_PREF, this.getLifecycleDispatcher());
mIntentWithEffect = false;
if (getSavedInstanceState() == null && intent != null) {
if (!mIntentHandler.shouldIgnoreIntent(intent)) {
......
......@@ -291,11 +291,11 @@ public class MainIntentBehaviorMetricsIntegrationTest {
private void assertBackgroundDurationLogged(long duration, String expectedMetric) {
startActivity(false);
mActionTester = new UserActionTester();
ContextUtils.getAppSharedPreferences()
.edit()
.putLong(ChromeTabbedActivity.LAST_BACKGROUNDED_TIME_MS_PREF,
System.currentTimeMillis() - duration)
.commit();
TestThreadUtils.runOnUiThreadBlocking(() -> {
mActivityTestRule.getActivity()
.getInactivityTrackerForTesting()
.setLastBackgroundedTimeInPrefs(System.currentTimeMillis() - duration);
});
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
......
......@@ -4,15 +4,20 @@
package org.chromium.chrome.browser.touchless;
import static org.chromium.chrome.browser.UrlConstants.NTP_URL;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.KeyEvent;
import android.view.ViewGroup;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeInactivityTracker;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.IntentHandler.IntentHandlerDelegate;
import org.chromium.chrome.browser.IntentHandler.TabOpenType;
......@@ -40,6 +45,9 @@ import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
public class NoTouchActivity extends SingleTabActivity {
private static final String BUNDLE_TAB_ID = "tabId";
@VisibleForTesting
static final String LAST_BACKGROUNDED_TIME_MS_PREF = "NoTouchActivity.BackgroundTimeMs";
// Time at which an intent was received and handled.
private long mIntentHandlingTimeMs;
......@@ -54,6 +62,9 @@ public class NoTouchActivity extends SingleTabActivity {
/** The class that controls the UI for touchless devices. */
private TouchlessUiController mUiController;
/** The class that finishes this activity after a timeout */
private ChromeInactivityTracker mInactivityTracker;
/**
* Internal class which performs the intent handling operations delegated by IntentHandler.
*/
......@@ -121,6 +132,7 @@ public class NoTouchActivity extends SingleTabActivity {
(ViewGroup) findViewById(android.R.id.content), null /* controlContainer */);
getFullscreenManager().setTab(getActivityTab());
mUiController = AppHooks.get().createTouchlessUiController(this);
super.finishNativeInitialization();
}
......@@ -133,6 +145,8 @@ public class NoTouchActivity extends SingleTabActivity {
mProgressBarCoordinator =
new ProgressBarCoordinator(mProgressBarView, getActivityTabProvider());
mTouchlessZoomHelper = new TouchlessZoomHelper(getActivityTabProvider());
mInactivityTracker = new ChromeInactivityTracker(
LAST_BACKGROUNDED_TIME_MS_PREF, this.getLifecycleDispatcher());
// By this point if we were going to restore a URL from savedInstanceState we would already
// have done so.
......@@ -156,6 +170,18 @@ public class NoTouchActivity extends SingleTabActivity {
super.onNewIntent(intent);
}
@Override
public void onNewIntentWithNative(Intent intent) {
if (mInactivityTracker.inactivityThresholdPassed()) {
if (!mIntentHandler.shouldIgnoreIntent(intent)) {
if (!NTP_URL.equals(getActivityTab().getUrl())) {
intent.setData(Uri.parse(NTP_URL));
}
}
}
super.onNewIntentWithNative(intent);
}
@Override
protected IntentHandlerDelegate createIntentHandlerDelegate() {
return new InternalIntentDelegate();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment