Index: chrome/android/java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java b/chrome/android/java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java |
index 7c9f8ee37ad2bc56bc911cffdf5f667d8b3d1f49..b2bf677110ee60d165c946801f40dd6eb816af56 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/invalidation/InvalidationController.java |
@@ -6,6 +6,8 @@ package org.chromium.chrome.browser.invalidation; |
import android.content.Context; |
import android.content.Intent; |
+import android.os.Handler; |
+import android.os.SystemClock; |
import org.chromium.base.ApplicationState; |
import org.chromium.base.ApplicationStatus; |
@@ -25,6 +27,98 @@ import java.util.HashSet; |
* client library used by Sync. |
*/ |
public class InvalidationController implements ApplicationStatus.ApplicationStateListener { |
+ /** |
+ * Timer which can be paused. When the timer is paused, the execution of its scheduled task is |
+ * delayed till the timer is resumed. |
+ */ |
+ private static class Timer { |
+ private Handler mHandler; |
+ |
+ /** |
+ * Runnable which is added to the handler's message queue. |
+ */ |
+ private Runnable mHandlerRunnable; |
+ |
+ /** |
+ * User provided task. |
+ */ |
+ private Runnable mRunnable; |
+ |
+ /** |
+ * Time at which the task is scheduled. |
+ */ |
+ private long mScheduledTime; |
+ |
+ public Timer() { |
+ mHandler = new Handler(); |
+ } |
+ |
+ /** |
+ * Sets the task to run. The task will run after the delay or once {@link #resume()} is |
+ * called, whichever occurs last. The previously scheduled task, if any, is cancelled. |
+ * @param r Task to run. |
+ * @param delayMs Delay in milliseconds after which to run the task. |
+ */ |
+ public void setRunnable(Runnable r, long delayMs) { |
+ cancel(); |
+ mRunnable = r; |
+ mScheduledTime = SystemClock.elapsedRealtime() + delayMs; |
+ } |
+ |
+ /** |
+ * Blocks the task from being run. |
+ */ |
+ public void pause() { |
+ if (mHandlerRunnable == null) return; |
+ |
+ mHandler.removeCallbacks(mHandlerRunnable); |
+ mHandlerRunnable = null; |
+ } |
+ |
+ /** |
+ * Unblocks the task from being run. If the task was scheduled for a time in the past, runs |
+ * the task. Does nothing if no task is scheduled. |
+ */ |
+ public void resume() { |
+ if (mRunnable == null || mHandlerRunnable != null) return; |
+ |
+ long delayMs = Math.max(mScheduledTime - SystemClock.elapsedRealtime(), 0); |
+ mHandlerRunnable = new Runnable() { |
+ @Override |
+ public void run() { |
+ Runnable r = mRunnable; |
+ mRunnable = null; |
+ mHandlerRunnable = null; |
+ r.run(); |
+ } |
+ }; |
+ mHandler.postDelayed(mHandlerRunnable, delayMs); |
+ } |
+ |
+ /** |
+ * Cancels the scheduled task, if any. |
+ */ |
+ public void cancel() { |
+ pause(); |
+ mRunnable = null; |
+ } |
+ } |
+ |
+ /** |
+ * The amount of time after the RecentTabsPage is opened to register for session sync |
+ * invalidations. The delay is designed so that only users who linger on the RecentTabsPage |
+ * register for session sync invalidations. How long users spend on the RecentTabsPage is |
+ * measured by the NewTabPage.RecentTabsPage.TimeVisibleAndroid UMA metric. |
+ */ |
+ private static final int REGISTER_FOR_SESSION_SYNC_INVALIDATIONS_DELAY_MS = 20000; |
+ |
+ /** |
+ * The amount of time after the RecentTabsPage is closed to unregister for session sync |
+ * invalidations. The delay is long to avoid registering and unregistering a lot if the user |
+ * visits the RecentTabsPage a lot. |
+ */ |
+ private static final int UNREGISTER_FOR_SESSION_SYNC_INVALIDATIONS_DELAY_MS = 3600000; // 1hr |
+ |
private static final Object LOCK = new Object(); |
private static InvalidationController sInstance; |
@@ -32,18 +126,43 @@ public class InvalidationController implements ApplicationStatus.ApplicationStat |
private final Context mContext; |
/** |
- * Whether session sync invalidations should be disabled. |
+ * Whether session sync invalidations can be disabled. |
+ */ |
+ private final boolean mCanDisableSessionInvalidations; |
+ |
+ /** |
+ * Whether the controller was started. |
+ */ |
+ private boolean mStarted; |
+ |
+ /** |
+ * Used to schedule tasks to enable and disable session sync invalidations. |
+ */ |
+ private Timer mEnableSessionInvalidationsTimer; |
+ |
+ /** |
+ * Whether session sync invalidations are enabled. |
+ */ |
+ private boolean mSessionInvalidationsEnabled; |
+ |
+ /** |
+ * The number of open RecentTabsPages |
*/ |
- private final boolean mDisableSessionInvalidations; |
+ private int mNumRecentTabPages; |
/** |
* Updates the sync invalidation types that the client is registered for based on the preferred |
* sync types. Starts the client if needed. |
*/ |
public void ensureStartedAndUpdateRegisteredTypes() { |
+ mStarted = true; |
+ // Do not apply changes to {@link #mSessionInvalidationsEnabled} yet because the timer task |
+ // may be scheduled far into the future. |
+ mEnableSessionInvalidationsTimer.resume(); |
+ |
HashSet<ModelType> typesToRegister = new HashSet<ModelType>(); |
typesToRegister.addAll(ProfileSyncService.get(mContext).getPreferredDataTypes()); |
- if (mDisableSessionInvalidations) { |
+ if (!mSessionInvalidationsEnabled) { |
typesToRegister.remove(ModelType.SESSION); |
typesToRegister.remove(ModelType.FAVICON_TRACKING); |
typesToRegister.remove(ModelType.FAVICON_IMAGE); |
@@ -60,6 +179,8 @@ public class InvalidationController implements ApplicationStatus.ApplicationStat |
* Starts the invalidation client without updating the registered invalidation types. |
*/ |
private void start() { |
+ mStarted = true; |
+ mEnableSessionInvalidationsTimer.resume(); |
Intent intent = new Intent(mContext, InvalidationClientService.class); |
mContext.startService(intent); |
} |
@@ -68,12 +189,39 @@ public class InvalidationController implements ApplicationStatus.ApplicationStat |
* Stops the invalidation client. |
*/ |
public void stop() { |
+ mStarted = false; |
+ mEnableSessionInvalidationsTimer.pause(); |
Intent intent = new Intent(mContext, InvalidationClientService.class); |
intent.putExtra(InvalidationIntentProtocol.EXTRA_STOP, true); |
mContext.startService(intent); |
} |
/** |
+ * Called when a RecentTabsPage is opened. |
+ */ |
+ public void onRecentTabsPageOpened() { |
+ if (!mCanDisableSessionInvalidations) return; |
+ |
+ ++mNumRecentTabPages; |
+ if (mNumRecentTabPages == 1) { |
+ setSessionInvalidationsEnabled(true, REGISTER_FOR_SESSION_SYNC_INVALIDATIONS_DELAY_MS); |
+ } |
+ } |
+ |
+ /** |
+ * Called when a RecentTabsPage is closed. |
+ */ |
+ public void onRecentTabsPageClosed() { |
+ if (!mCanDisableSessionInvalidations) return; |
+ |
+ --mNumRecentTabPages; |
+ if (mNumRecentTabPages == 0) { |
+ setSessionInvalidationsEnabled( |
+ false, UNREGISTER_FOR_SESSION_SYNC_INVALIDATIONS_DELAY_MS); |
+ } |
+ } |
+ |
+ /** |
* Returns the instance that will use {@code context} to issue intents. |
* |
* Calling this method will create the instance if it does not yet exist. |
@@ -81,29 +229,56 @@ public class InvalidationController implements ApplicationStatus.ApplicationStat |
public static InvalidationController get(Context context) { |
synchronized (LOCK) { |
if (sInstance == null) { |
- boolean disableSessionInvalidations = |
+ boolean canDisableSessionInvalidations = |
FieldTrialList.findFullName("AndroidSessionNotifications") |
.equals("Disabled"); |
- sInstance = new InvalidationController(context, disableSessionInvalidations); |
+ sInstance = new InvalidationController(context, canDisableSessionInvalidations); |
} |
return sInstance; |
} |
} |
/** |
+ * Schedules a task to enable/disable session sync invalidations. Cancels any previously |
+ * scheduled tasks to enable/disable session sync invalidations. |
+ * @param enabled whether to enable or disable session sync invalidations. |
+ * @param delayMs Delay in milliseconds after which to apply change. |
+ */ |
+ private void setSessionInvalidationsEnabled(final boolean enabled, long delayMs) { |
+ mEnableSessionInvalidationsTimer.cancel(); |
+ if (mSessionInvalidationsEnabled == enabled) return; |
+ |
+ mEnableSessionInvalidationsTimer.setRunnable(new Runnable() { |
+ @Override |
+ public void run() { |
+ mSessionInvalidationsEnabled = enabled; |
+ ensureStartedAndUpdateRegisteredTypes(); |
+ } |
+ }, delayMs); |
+ if (mStarted) { |
+ mEnableSessionInvalidationsTimer.resume(); |
+ } |
+ } |
+ |
+ /** |
* Creates an instance using {@code context} to send intents. |
*/ |
@VisibleForTesting |
- InvalidationController(Context context, boolean disableSessionInvalidations) { |
+ InvalidationController(Context context, boolean canDisableSessionInvalidations) { |
Context appContext = context.getApplicationContext(); |
if (appContext == null) throw new NullPointerException("Unable to get application context"); |
mContext = appContext; |
- mDisableSessionInvalidations = disableSessionInvalidations; |
+ mCanDisableSessionInvalidations = canDisableSessionInvalidations; |
+ mSessionInvalidationsEnabled = !mCanDisableSessionInvalidations; |
+ mEnableSessionInvalidationsTimer = new Timer(); |
+ |
ApplicationStatus.registerApplicationStateListener(this); |
} |
@Override |
public void onApplicationStateChange(int newState) { |
+ // The isSyncEnabled() check is used to check whether the InvalidationController would be |
+ // started if it did not stop itself when the application is paused. |
if (AndroidSyncSettings.isSyncEnabled(mContext)) { |
if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES) { |
start(); |