// Copyright 2013 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.invalidation;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.SystemClock;
import com.google.ipc.invalidation.ticl.android2.channel.AndroidGcmController;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.FieldTrialList;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.components.invalidation.InvalidationClientService;
import org.chromium.components.signin.ChromeSigninController;
import org.chromium.components.sync.AndroidSyncSettings;
import org.chromium.components.sync.ModelType;
import org.chromium.components.sync.notifier.InvalidationIntentProtocol;
import java.util.HashSet;
/**
* Controller used to send start, stop, and registration-change commands to the invalidation
* 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;
private final Context mContext;
/**
* 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 int mNumRecentTabPages;
/**
* Whether GCM Upstream should be used for sending upstream messages.
*/
private boolean mUseGcmUpstream;
/**
* Whether GCM has been initialized for Invalidations.
*/
private boolean mGcmInitialized;
/**
* 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() {
ProfileSyncService syncService = ProfileSyncService.get();
if (syncService == null) return;
mStarted = true;
// Ensure GCM has been initialized.
ensureGcmIsInitialized();
// Do not apply changes to {@link #mSessionInvalidationsEnabled} yet because the timer task
// may be scheduled far into the future.
mEnableSessionInvalidationsTimer.resume();
HashSet<Integer> typesToRegister = new HashSet<Integer>();
typesToRegister.addAll(syncService.getPreferredDataTypes());
if (!mSessionInvalidationsEnabled) {
typesToRegister.remove(ModelType.SESSIONS);
typesToRegister.remove(ModelType.FAVICON_TRACKING);
typesToRegister.remove(ModelType.FAVICON_IMAGES);
}
Intent registerIntent = InvalidationIntentProtocol.createRegisterIntent(
ChromeSigninController.get(mContext).getSignedInUser(),
typesToRegister);
registerIntent.setClass(
mContext, InvalidationClientService.getRegisteredClass());
mContext.startService(registerIntent);
}
/**
* Registers for Google Cloud Messaging (GCM) for Invalidations.
*/
private void ensureGcmIsInitialized() {
if (mGcmInitialized) return;
mGcmInitialized = true;
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... arg0) {
AndroidGcmController.get(mContext).initializeGcm(mUseGcmUpstream);
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@VisibleForTesting
public boolean isGcmInitialized() {
return mGcmInitialized;
}
/**
* Starts the invalidation client without updating the registered invalidation types.
*/
private void start() {
mStarted = true;
mEnableSessionInvalidationsTimer.resume();
Intent intent = new Intent(
mContext, InvalidationClientService.getRegisteredClass());
mContext.startService(intent);
}
/**
* Stops the invalidation client.
*/
public void stop() {
mStarted = false;
mEnableSessionInvalidationsTimer.pause();
Intent intent = new Intent(
mContext, InvalidationClientService.getRegisteredClass());
intent.putExtra(InvalidationIntentProtocol.EXTRA_STOP, true);
mContext.startService(intent);
}
/**
* Returns whether the invalidation client has been started.
*/
public boolean isStarted() {
return mStarted;
}
/**
* 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.
*/
public static InvalidationController get(Context context) {
synchronized (LOCK) {
if (sInstance == null) {
// The PageRevisitInstrumentation trial needs sessions invalidations to be on such
// that local session data is current and can be used to perform checks.
boolean requireInvalidationsForInstrumentation =
FieldTrialList.findFullName("PageRevisitInstrumentation").equals("Enabled");
boolean canDisableSessionInvalidations = !requireInvalidationsForInstrumentation;
boolean canUseGcmUpstream =
FieldTrialList.findFullName("InvalidationsGCMUpstream").equals("Enabled");
sInstance = new InvalidationController(
context, canDisableSessionInvalidations, canUseGcmUpstream);
}
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 canDisableSessionInvalidations, boolean canUseGcmUpstream) {
Context appContext = context.getApplicationContext();
if (appContext == null) throw new NullPointerException("Unable to get application context");
mContext = appContext;
mUseGcmUpstream = canUseGcmUpstream;
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();
} else if (newState == ApplicationState.HAS_PAUSED_ACTIVITIES) {
stop();
}
}
}
}