blob: f4b1187ecdefb8390045b3758181ddabd3a6c89f [file] [log] [blame]
// 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.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.Destroyable;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
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, PauseResumeWithNativeObserver, Destroyable {
private static final String TAG = "InactivityTracker";
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;
@VisibleForTesting
public static final String FEATURE_NAME = ChromeFeatureList.NTP_LAUNCH_AFTER_INACTIVITY;
@VisibleForTesting
public static final String NTP_LAUNCH_DELAY_IN_MINS_PARAM = "delay_in_mins";
// 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);
mLifecycleDispatcher = lifecycleDispatcher;
mLifecycleDispatcher.register(this);
}
/**
* 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();
}
/**
* Updates shared preferences such that the last backgrounded time is no
* longer valid. This will prevent multiple new intents from firing.
*/
private void clearLastBackgroundedTimeInPrefs() {
setLastBackgroundedTimeInPrefs(UNKNOWN_LAST_BACKGROUNDED_TIME);
}
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() {
cancelCurrentTask();
}
@Override
public void onResumeWithNative() {
// We clear the backgrounded time here, rather than in #onStartWithNative, to give
// handlers the chance to respond to inactivity during any onStartWithNative handler
// regardless of ordering. onResume is always called after onStart, and it should be fine to
// consider Chrome active if it reaches onResume.
clearLastBackgroundedTimeInPrefs();
}
@Override
public void onPauseWithNative() {}
@Override
public void onStopWithNative() {
// Always track the last backgrounded time in case others are using the pref.
long timeInMillis = System.currentTimeMillis();
setLastBackgroundedTimeInPrefs(timeInMillis);
if (!mIsEnabled) return;
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);
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;
}
}
}