summaryrefslogtreecommitdiffstats
path: root/sync
diff options
context:
space:
mode:
authordsmyers@chromium.org <dsmyers@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-01-26 05:54:23 +0000
committerdsmyers@chromium.org <dsmyers@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-01-26 05:54:23 +0000
commit8290db69025a2e16162aef76144e134210beb5dc (patch)
tree60491ecc72219556a4391d75a709d3ff37a58727 /sync
parent481d5a081cdce2361607c44a414def7ddb1c2faf (diff)
downloadchromium_src-8290db69025a2e16162aef76144e134210beb5dc.zip
chromium_src-8290db69025a2e16162aef76144e134210beb5dc.tar.gz
chromium_src-8290db69025a2e16162aef76144e134210beb5dc.tar.bz2
Adds a client for v2 of the notification library.
BUG=159221 Review URL: https://chromiumcodereview.appspot.com/12051074 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@179048 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'sync')
-rw-r--r--sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java18
-rw-r--r--sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java26
-rw-r--r--sync/android/java/src/org/chromium/sync/notifier/InvalidationPreferences.java36
-rw-r--r--sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java444
-rw-r--r--sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java3
-rw-r--r--sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java3
-rw-r--r--sync/android/javatests/src/org/chromium/sync/notifier/InvalidationPreferencesTest.java6
-rw-r--r--sync/android/javatests/src/org/chromium/sync/notifier/InvalidationServiceTest.java577
-rw-r--r--sync/android/javatests/src/org/chromium/sync/notifier/TestableInvalidationService.java97
-rw-r--r--sync/sync.gyp1
10 files changed, 1195 insertions, 16 deletions
diff --git a/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java b/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java
index 2d309be..33c3f61 100644
--- a/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java
+++ b/sync/android/java/src/org/chromium/sync/internal_api/pub/base/ModelType.java
@@ -80,4 +80,22 @@ public enum ModelType {
return modelTypes;
}
}
+
+ /** Converts a set of {@link ModelType} to a set of {@link ObjectId}. */
+ public static Set<ObjectId> modelTypesToObjectIds(Set<ModelType> modelTypes) {
+ Set<ObjectId> objectIds = Sets.newHashSetWithExpectedSize(modelTypes.size());
+ for (ModelType modelType : modelTypes) {
+ objectIds.add(modelType.toObjectId());
+ }
+ return objectIds;
+ }
+
+ /** Converts a set of {@link ModelType} to a set of string names. */
+ public static Set<String> modelTypesToSyncTypes(Set<ModelType> modelTypes) {
+ Set<String> objectIds = Sets.newHashSetWithExpectedSize(modelTypes.size());
+ for (ModelType modelType : modelTypes) {
+ objectIds.add(modelType.toString());
+ }
+ return objectIds;
+ }
}
diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java
index df7f14f..0f4b7d4 100644
--- a/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java
+++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java
@@ -76,6 +76,16 @@ public class InvalidationController {
return registerIntent;
}
+ /** Returns whether {@code intent} is a stop intent. */
+ public static boolean isStop(Intent intent) {
+ return intent.getBooleanExtra(EXTRA_STOP, false);
+ }
+
+ /** Returns whether {@code intent} is a registered types change intent. */
+ public static boolean isRegisteredTypesChange(Intent intent) {
+ return intent.hasExtra(EXTRA_REGISTERED_TYPES);
+ }
+
private IntentProtocol() {
// Disallow instantiation.
}
@@ -93,7 +103,7 @@ public class InvalidationController {
*/
private static final String TAG = InvalidationController.class.getSimpleName();
- private final Context context;
+ private final Context mContext;
/**
* Sets the types for which the client should register for notifications.
@@ -105,7 +115,7 @@ public class InvalidationController {
public void setRegisteredTypes(Account account, boolean allTypes, Set<ModelType> types) {
Intent registerIntent = IntentProtocol.createRegisterIntent(account, allTypes, types);
setDestinationClassName(registerIntent);
- context.startService(registerIntent);
+ mContext.startService(registerIntent);
}
/**
@@ -113,7 +123,7 @@ public class InvalidationController {
*/
public void start() {
Intent intent = setDestinationClassName(new Intent());
- context.startService(intent);
+ mContext.startService(intent);
}
/**
@@ -122,14 +132,14 @@ public class InvalidationController {
public void stop() {
Intent intent = setDestinationClassName(new Intent());
intent.putExtra(IntentProtocol.EXTRA_STOP, true);
- context.startService(intent);
+ mContext.startService(intent);
}
/**
* Returns the contract authority to use when requesting sync.
*/
public String getContractAuthority() {
- return context.getPackageName();
+ return mContext.getPackageName();
}
/**
@@ -143,7 +153,7 @@ public class InvalidationController {
* Creates an instance using {@code context} to send intents.
*/
private InvalidationController(Context context) {
- this.context = Preconditions.checkNotNull(context.getApplicationContext());
+ this.mContext = Preconditions.checkNotNull(context.getApplicationContext());
}
/**
@@ -154,9 +164,9 @@ public class InvalidationController {
* @return {@code intent}
*/
private Intent setDestinationClassName(Intent intent) {
- String className = getDestinationClassName(context);
+ String className = getDestinationClassName(mContext);
if (className != null) {
- intent.setClassName(context, className);
+ intent.setClassName(mContext, className);
}
return intent;
}
diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationPreferences.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationPreferences.java
index 43eec81..befaaeb 100644
--- a/sync/android/java/src/org/chromium/sync/notifier/InvalidationPreferences.java
+++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationPreferences.java
@@ -8,6 +8,7 @@ import android.accounts.Account;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
+import android.util.Base64;
import android.util.Log;
import com.google.common.annotations.VisibleForTesting;
@@ -60,6 +61,9 @@ public class InvalidationPreferences {
/** Shared preference key to store the type of account in use. */
static final String SYNC_ACCT_TYPE = "sync_acct_type";
+
+ /** Shared preference key to store internal notification client library state. */
+ static final String SYNC_TANGO_INTERNAL_STATE = "sync_tango_internal_state";
}
private static final String TAG = InvalidationPreferences.class.getSimpleName();
@@ -90,11 +94,18 @@ public class InvalidationPreferences {
}
/** Returns the saved sync types, or {@code null} if none exist. */
- @Nullable public Collection<String> getSavedSyncedTypes() {
+ @Nullable public Set<String> getSavedSyncedTypes() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
return preferences.getStringSet(PrefKeys.SYNC_TANGO_TYPES, null);
}
+ /** Sets the saved sync types to {@code syncTypes} in {@code editContext}. */
+ public void setSyncTypes(EditContext editContext, Collection<String> syncTypes) {
+ Preconditions.checkNotNull(syncTypes);
+ Set<String> selectedTypesSet = new HashSet<String>(syncTypes);
+ editContext.editor.putStringSet(PrefKeys.SYNC_TANGO_TYPES, selectedTypesSet);
+ }
+
/** Returns the saved account, or {@code null} if none exists. */
@Nullable public Account getSavedSyncedAccount() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
@@ -106,16 +117,25 @@ public class InvalidationPreferences {
return new Account(accountName, accountType);
}
- /** Sets the saved sync types to {@code syncTypes} in {@code editContext}. */
- public void setSyncTypes(EditContext editContext, Collection<String> syncTypes) {
- Preconditions.checkNotNull(syncTypes);
- Set<String> selectedTypesSet = new HashSet<String>(syncTypes);
- editContext.editor.putStringSet(PrefKeys.SYNC_TANGO_TYPES, selectedTypesSet);
- }
-
/** Sets the saved account to {@code account} in {@code editContext}. */
public void setAccount(EditContext editContext, Account account) {
editContext.editor.putString(PrefKeys.SYNC_ACCT_NAME, account.name);
editContext.editor.putString(PrefKeys.SYNC_ACCT_TYPE, account.type);
}
+
+ /** Returns the notification client internal state. */
+ @Nullable public byte[] getInternalNotificationClientState() {
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
+ String base64State = preferences.getString(PrefKeys.SYNC_TANGO_INTERNAL_STATE, null);
+ if (base64State == null) {
+ return null;
+ }
+ return Base64.decode(base64State, Base64.DEFAULT);
+ }
+
+ /** Sets the notification client internal state to {@code state}. */
+ public void setInternalNotificationClientState(EditContext editContext, byte[] state) {
+ editContext.editor.putString(PrefKeys.SYNC_TANGO_INTERNAL_STATE,
+ Base64.encodeToString(state, Base64.DEFAULT));
+ }
}
diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java
new file mode 100644
index 0000000..c7f1e16
--- /dev/null
+++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java
@@ -0,0 +1,444 @@
+// Copyright (c) 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.sync.notifier;
+
+import android.accounts.Account;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
+import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
+import com.google.ipc.invalidation.external.client.types.ErrorInfo;
+import com.google.ipc.invalidation.external.client.types.Invalidation;
+import com.google.ipc.invalidation.external.client.types.ObjectId;
+import com.google.protos.ipc.invalidation.Types.ClientType;
+
+import org.chromium.base.ActivityStatus;
+import org.chromium.sync.internal_api.pub.base.ModelType;
+import org.chromium.sync.notifier.InvalidationController.IntentProtocol;
+import org.chromium.sync.notifier.InvalidationPreferences.EditContext;
+import org.chromium.sync.signin.AccountManagerHelper;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Service that controls notifications for sync.
+ * <p>
+ * This service serves two roles. On the one hand, it is a client for the notification system
+ * used to trigger sync. It receives invalidations and converts them into
+ * {@link ContentResolver#requestSync} calls, and it supplies the notification system with the set
+ * of desired registrations when requested.
+ * <p>
+ * On the other hand, this class is controller for the notification system. It starts it and stops
+ * it, and it requests that it perform (un)registrations as the set of desired sync types changes.
+ * <p>
+ * This class is an {@code IntentService}. All methods are assumed to be executing on its single
+ * execution thread.
+ *
+ * @author dsmyers@google.com
+ */
+public class InvalidationService extends AndroidListener {
+ /* This class must be public because it is exposed as a service. */
+
+ /** Notification client typecode. */
+ @VisibleForTesting
+ static final int CLIENT_TYPE = ClientType.Type.CHROME_SYNC_ANDROID_VALUE;
+
+ private static final String TAG = InvalidationService.class.getSimpleName();
+
+ private static final Random RANDOM = new Random();
+
+ /**
+ * Whether the underlying notification client has been started. This boolean is updated when a
+ * start or stop intent is issued to the underlying client, not when the intent is actually
+ * processed.
+ */
+ private static boolean sIsClientStarted;
+
+ /**
+ * The id of the client in use, if any. May be {@code null} if {@link #sIsClientStarted} is
+ * true if the client has not yet gone ready.
+ */
+ @Nullable private static byte[] sClientId;
+
+ @Override
+ public void onHandleIntent(Intent intent) {
+ // Ensure that a client is or is not running, as appropriate, and that it is for the
+ // correct account. ensureAccount will stop the client if account is non-null and doesn't
+ // match the stored account. Then, if a client should be running, ensureClientStartState
+ // will start a new one if needed. I.e., these two functions work together to restart the
+ // client when the account changes.
+ Account account = intent.hasExtra(IntentProtocol.EXTRA_ACCOUNT) ?
+ (Account) intent.getParcelableExtra(IntentProtocol.EXTRA_ACCOUNT) : null;
+ ensureAccount(account);
+ ensureClientStartState();
+
+ // Handle the intent.
+ if (IntentProtocol.isStop(intent) && sIsClientStarted) {
+ // If the intent requests that the client be stopped, stop it.
+ stopClient();
+ } else if (IntentProtocol.isRegisteredTypesChange(intent)) {
+ // If the intent requests a change in registrations, change them.
+ List<String> regTypes =
+ intent.getStringArrayListExtra(IntentProtocol.EXTRA_REGISTERED_TYPES);
+ setRegisteredTypes(Sets.newHashSet(regTypes));
+ } else {
+ // Otherwise, we don't recognize the intent. Pass it to the notification client service.
+ super.onHandleIntent(intent);
+ }
+ }
+
+ @Override
+ public void invalidate(Invalidation invalidation, byte[] ackHandle) {
+ requestSync(invalidation.getObjectId(), invalidation.getVersion(),
+ new String(invalidation.getPayload()));
+ acknowledge(ackHandle);
+ }
+
+ @Override
+ public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {
+ requestSync(objectId, null, null);
+ acknowledge(ackHandle);
+ }
+
+ @Override
+ public void invalidateAll(byte[] ackHandle) {
+ requestSync(null, null, null);
+ acknowledge(ackHandle);
+ }
+
+ @Override
+ public void informRegistrationFailure(
+ byte[] clientId, ObjectId objectId, boolean isTransient, String errorMessage) {
+ Log.w(TAG, "Registration failure on " + objectId + " ; transient = " + isTransient
+ + ": " + errorMessage);
+ if (isTransient) {
+ // Retry immediately on transient failures. The base AndroidListener will handle
+ // exponential backoff if there are repeated failures.
+ List<ObjectId> objectIdAsList = Lists.newArrayList(objectId);
+ if (readRegistrationsFromPrefs().contains(objectId)) {
+ register(clientId, objectIdAsList);
+ } else {
+ unregister(clientId, objectIdAsList);
+ }
+ }
+ }
+
+ @Override
+ public void informRegistrationStatus(
+ byte[] clientId, ObjectId objectId, RegistrationState regState) {
+ Log.d(TAG, "Registration status for " + objectId + ": " + regState);
+ List<ObjectId> objectIdAsList = Lists.newArrayList(objectId);
+ boolean registrationisDesired = readRegistrationsFromPrefs().contains(objectId);
+ if (regState == RegistrationState.REGISTERED) {
+ if (!registrationisDesired) {
+ Log.i(TAG, "Unregistering for object we're no longer interested in");
+ unregister(clientId, objectIdAsList);
+ }
+ } else {
+ if (registrationisDesired) {
+ Log.i(TAG, "Registering for an object");
+ register(clientId, objectIdAsList);
+ }
+ }
+ }
+
+ @Override
+ public void informError(ErrorInfo errorInfo) {
+ Log.w(TAG, "Invalidation client error:" + errorInfo);
+ if (!errorInfo.isTransient() && sIsClientStarted) {
+ // It is important not to stop the client if it is already stopped. Otherwise, the
+ // possibility exists to go into an infinite loop if the stop call itself triggers an
+ // error (e.g., because no client actually exists).
+ stopClient();
+ }
+ }
+
+ @Override
+ public void reissueRegistrations(byte[] clientId) {
+ Set<ObjectId> desiredRegistrations = readRegistrationsFromPrefs();
+ if (!desiredRegistrations.isEmpty()) {
+ register(clientId, desiredRegistrations);
+ }
+ // TODO(dsmyers): [misc] reissue registrations is not guaranteed to be called by spec,
+ // although it will happen in practice. This code should be changed not to rely on it.
+ // Bug: https://code.google.com/p/chromium/issues/detail?id=172390
+ setClientId(clientId);
+ }
+
+ @Override
+ public void requestAuthToken(PendingIntent pendingIntent, @Nullable String invalidAuthToken) {
+ @Nullable Account account = SyncStatusHelper.get(this).getSignedInUser();
+ if (account == null) {
+ // This should never happen, because this code should only be run if a user is
+ // signed-in.
+ Log.w(TAG, "No signed-in user; cannot send message to data center");
+ return;
+ }
+
+ // Attempt to retrieve a token for the user. This method will also invalidate
+ // invalidAuthToken if it is non-null.
+ String authToken = AccountManagerHelper.get(this).getNewAuthToken(account, invalidAuthToken,
+ SyncStatusHelper.AUTH_TOKEN_TYPE_SYNC);
+ if (authToken != null) {
+ setAuthToken(this, pendingIntent, authToken, SyncStatusHelper.AUTH_TOKEN_TYPE_SYNC);
+ }
+ }
+
+ @Override
+ public void writeState(byte[] data) {
+ InvalidationPreferences invPreferences = new InvalidationPreferences(this);
+ EditContext editContext = invPreferences.edit();
+ invPreferences.setInternalNotificationClientState(editContext, data);
+ invPreferences.commit(editContext);
+ }
+
+ @Override
+ @Nullable public byte[] readState() {
+ return new InvalidationPreferences(this).getInternalNotificationClientState();
+ }
+
+ /**
+ * Ensures that the client is running or not running as appropriate, based on the value of
+ * {@link #shouldClientBeRunning}.
+ */
+ private void ensureClientStartState() {
+ final boolean shouldClientBeRunning = shouldClientBeRunning();
+ if (!shouldClientBeRunning && sIsClientStarted) {
+ // Stop the client if it should not be running and is.
+ stopClient();
+ } else if (shouldClientBeRunning && !sIsClientStarted) {
+ // Start the client if it should be running and isn't.
+ startClient();
+ }
+ }
+
+ /**
+ * If {@code intendedAccount} is non-{@null} and differs from the account stored in preferences,
+ * then stops the existing client (if any) and updates the stored account.
+ */
+ private void ensureAccount(@Nullable Account intendedAccount) {
+ if (intendedAccount == null) {
+ return;
+ }
+ InvalidationPreferences invPrefs = new InvalidationPreferences(this);
+ if (!intendedAccount.equals(invPrefs.getSavedSyncedAccount())) {
+ if (sIsClientStarted) {
+ stopClient();
+ }
+ setAccount(intendedAccount);
+ }
+ }
+
+ /**
+ * Starts a new client, destroying any existing client. {@code owningAccount} is the account
+ * of the user for which the client is being created; it will be persisted using
+ * {@link InvalidationPreferences#setAccount}.
+ */
+ private void startClient() {
+ Intent startIntent = AndroidListener.createStartIntent(this, CLIENT_TYPE, getClientName());
+ startService(startIntent);
+ setIsClientStarted(true);
+ }
+
+ /** Stops the notification client. */
+ private void stopClient() {
+ startService(AndroidListener.createStopIntent(this));
+ setIsClientStarted(false);
+ setClientId(null);
+ }
+
+ /** Sets the saved sync account in {@link InvalidationPreferences} to {@code owningAccount}. */
+ private void setAccount(Account owningAccount) {
+ InvalidationPreferences invPrefs = new InvalidationPreferences(this);
+ EditContext editContext = invPrefs.edit();
+ invPrefs.setAccount(editContext, owningAccount);
+ invPrefs.commit(editContext);
+ }
+
+ /**
+ * Reads the saved sync types from storage (if any) and returns a set containing the
+ * corresponding object ids.
+ */
+ @VisibleForTesting
+ Set<ObjectId> readRegistrationsFromPrefs() {
+ Set<String> savedTypes = new InvalidationPreferences(this).getSavedSyncedTypes();
+ if (savedTypes == null) {
+ return Collections.emptySet();
+ } else {
+ Set<ModelType> modelTypes = ModelType.syncTypesToModelTypes(savedTypes);
+ Set<ObjectId> objectIds = Sets.newHashSetWithExpectedSize(modelTypes.size());
+ for (ModelType modelType : modelTypes) {
+ objectIds.add(modelType.toObjectId());
+ }
+ return objectIds;
+ }
+ }
+
+ /**
+ * Sets the types for which notifications are required to {@code syncTypes}. {@code syncTypes}
+ * is either a list of specific types or the special wildcard type
+ * {@link ModelType#ALL_TYPES_TYPE}.
+ * <p>
+ * @param syncTypes
+ */
+ private void setRegisteredTypes(Set<String> syncTypes) {
+ // If we have a ready client and will be making registration change calls on it, then
+ // read the current registrations from preferences before we write the new values, so that
+ // we can take the diff of the two registration sets and determine which registration change
+ // calls to make.
+ Set<ObjectId> existingRegistrations = (sClientId == null) ?
+ null : readRegistrationsFromPrefs();
+
+ // Write the new sync types to preferences. We do not expand the syncTypes to take into
+ // account the ALL_TYPES_TYPE at this point; we want to persist the wildcard unexpanded.
+ InvalidationPreferences prefs = new InvalidationPreferences(this);
+ EditContext editContext = prefs.edit();
+ prefs.setSyncTypes(editContext, syncTypes);
+ prefs.commit(editContext);
+
+ // If we do not have a ready invalidation client, we cannot change its registrations, so
+ // return. Later, when the client is ready, we will get a reissueRegistrations upcall and
+ // will supply the new registrations then.
+ if (sClientId == null) {
+ return;
+ }
+
+ // We do have a ready client. Unregister any existing registrations not present in the
+ // new set and register any elements in the new set not already present. This call does
+ // expansion of the ALL_TYPES_TYPE wildcard.
+ // NOTE: syncTypes MUST NOT be used below this line, since it contains an unexpanded
+ // wildcard.
+ Set<ModelType> newRegisteredTypes = ModelType.syncTypesToModelTypes(syncTypes);
+
+ List<ObjectId> unregistrations = Lists.newArrayList();
+ List<ObjectId> registrations = Lists.newArrayList();
+ computeRegistrationOps(existingRegistrations,
+ ModelType.modelTypesToObjectIds(newRegisteredTypes), registrations,
+ unregistrations);
+ unregister(sClientId, unregistrations);
+ register(sClientId, registrations);
+ }
+
+ /**
+ * Computes the set of (un)registrations to perform so that the registrations active in the
+ * Ticl will be {@code desiredRegs}, given that {@existingRegs} already exist.
+ *
+ * @param regAccumulator registrations to perform
+ * @param unregAccumulator unregistrations to perform.
+ */
+ @VisibleForTesting
+ static void computeRegistrationOps(Set<ObjectId> existingRegs, Set<ObjectId> desiredRegs,
+ Collection<ObjectId> regAccumulator, Collection<ObjectId> unregAccumulator) {
+ // Registrations to do are elements in the new set but not the old set.
+ regAccumulator.addAll(Sets.difference(desiredRegs, existingRegs));
+
+ // Unregistrations to do are elements in the old set but not the new set.
+ unregAccumulator.addAll(Sets.difference(existingRegs, desiredRegs));
+ }
+
+ /**
+ * Requests that the sync system perform a sync.
+ *
+ * @param objectId the object that changed, if known.
+ * @param version the version of the object that changed, if known.
+ * @param payload the payload of the change, if known.
+ */
+ private void requestSync(@Nullable ObjectId objectId, @Nullable Long version,
+ @Nullable String payload) {
+ // Construct the bundle to supply to the native sync code.
+ Bundle bundle = new Bundle();
+ if (objectId == null && version == null && payload == null) {
+ // Use an empty bundle in this case for compatibility with the v1 implementation.
+ } else {
+ if (objectId != null) {
+ bundle.putString("objectId", new String(objectId.getName()));
+ }
+ // We use "0" as the version if we have an unknown-version invalidation. This is OK
+ // because the native sync code special-cases zero and always syncs for invalidations at
+ // that version (Tango defines a special UNKNOWN_VERSION constant with this value).
+ bundle.putLong("version", (version == null) ? 0 : version);
+ bundle.putString("payload", (payload == null) ? "" : payload);
+ }
+ Account account = SyncStatusHelper.get(this).getSignedInUser();
+ String contractAuthority = InvalidationController.newInstance(this).getContractAuthority();
+ requestSyncFromContentResolver(bundle, account, contractAuthority);
+ }
+
+ /**
+ * Calls {@link ContentResolver#requestSync(Account, String, Bundle)} to trigger a sync. Split
+ * into a separate method so that it can be overriden in tests.
+ */
+ @VisibleForTesting
+ void requestSyncFromContentResolver(
+ Bundle bundle, Account account, String contractAuthority) {
+ Log.d(TAG, "Request sync: " + account + " / " + contractAuthority + " / "
+ + bundle.keySet());
+ ContentResolver.requestSync(account, contractAuthority, bundle);
+ }
+
+ /**
+ * Returns whether the notification client should be running, i.e., whether Chrome is in the
+ * foreground and sync is enabled.
+ */
+ @VisibleForTesting
+ boolean shouldClientBeRunning() {
+ return isSyncEnabled() && isChromeInForeground();
+ }
+
+ /** Returns whether sync is enabled. LLocal method so it can be overridden in tests. */
+ @VisibleForTesting
+ boolean isSyncEnabled() {
+ return SyncStatusHelper.get(getApplicationContext()).isSyncEnabled();
+ }
+
+ /**
+ * Returns whether Chrome is in the foreground. Local method so it can be overridden in tests.
+ */
+ @VisibleForTesting
+ boolean isChromeInForeground() {
+ switch (ActivityStatus.getState()) {
+ case ActivityStatus.CREATED:
+ case ActivityStatus.STARTED:
+ case ActivityStatus.RESUMED:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** Returns whether the notification client has been started, for tests. */
+ @VisibleForTesting
+ static boolean getIsClientStartedForTest() {
+ return sIsClientStarted;
+ }
+
+ /** Returns the client name used for the notification client. */
+ private static byte[] getClientName() {
+ // TODO(dsmyers): we should use the same client name as the native sync code.
+ // Bug: https://code.google.com/p/chromium/issues/detail?id=172391
+ return Long.toString(RANDOM.nextLong()).getBytes();
+ }
+
+ private static void setClientId(byte[] clientId) {
+ sClientId = clientId;
+ }
+
+ private static void setIsClientStarted(boolean isStarted) {
+ sIsClientStarted = isStarted;
+ }
+}
diff --git a/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java b/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java
index 2239d5c..4fc63581 100644
--- a/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java
+++ b/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java
@@ -36,6 +36,9 @@ public class SyncStatusHelper {
void onClearSignedInUser();
}
+ // TODO(dsmyers): remove the downstream version of this constant.
+ public static final String AUTH_TOKEN_TYPE_SYNC = "chromiumsync";
+
@VisibleForTesting
public static final String SIGNED_IN_ACCOUNT_KEY = "google.services.username";
diff --git a/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java b/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java
index 889b194..063289b 100644
--- a/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java
+++ b/sync/android/java/src/org/chromium/sync/signin/AccountManagerHelper.java
@@ -207,6 +207,9 @@ public class AccountManagerHelper {
* - Should not be called on the main thread.
*/
public String getNewAuthToken(Account account, String authToken, String authTokenType) {
+ // TODO(dsmyers): consider reimplementing using an AccountManager function with an
+ // explicit timeout.
+ // Bug: https://code.google.com/p/chromium/issues/detail?id=172394.
if (authToken != null && !authToken.isEmpty()) {
mAccountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, authToken);
}
diff --git a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationPreferencesTest.java b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationPreferencesTest.java
index ddaca6c..4586513 100644
--- a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationPreferencesTest.java
+++ b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationPreferencesTest.java
@@ -16,6 +16,7 @@ import org.chromium.base.test.util.Feature;
import org.chromium.sync.internal_api.pub.base.ModelType;
import org.chromium.sync.notifier.InvalidationPreferences;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -68,6 +69,7 @@ public class InvalidationPreferencesTest extends InstrumentationTestCase {
InvalidationPreferences invPreferences = new InvalidationPreferences(mContext);
assertNull(invPreferences.getSavedSyncedAccount());
assertNull(invPreferences.getSavedSyncedTypes());
+ assertNull(invPreferences.getInternalNotificationClientState());
}
@SmallTest
@@ -84,8 +86,10 @@ public class InvalidationPreferencesTest extends InstrumentationTestCase {
// with them here to ensure that preferences are not interpreting the written data.
Set<String> syncTypes = Sets.newHashSet("BOOKMARK", ModelType.ALL_TYPES_TYPE);
Account account = new Account("test@example.com", "bogus");
+ byte[] internalClientState = new byte[]{100,101,102};
invPreferences.setSyncTypes(editContext, syncTypes);
invPreferences.setAccount(editContext, account);
+ invPreferences.setInternalNotificationClientState(editContext, internalClientState);
// Nothing should yet have been written.
assertNull(invPreferences.getSavedSyncedAccount());
@@ -95,5 +99,7 @@ public class InvalidationPreferencesTest extends InstrumentationTestCase {
invPreferences.commit(editContext);
assertEquals(account, invPreferences.getSavedSyncedAccount());
assertEquals(syncTypes, invPreferences.getSavedSyncedTypes());
+ assertTrue(Arrays.equals(
+ internalClientState, invPreferences.getInternalNotificationClientState()));
}
}
diff --git a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationServiceTest.java b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationServiceTest.java
new file mode 100644
index 0000000..cfb8c0c
--- /dev/null
+++ b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationServiceTest.java
@@ -0,0 +1,577 @@
+// Copyright (c) 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.sync.notifier;
+
+import android.accounts.Account;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.test.ServiceTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
+import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
+import com.google.ipc.invalidation.external.client.types.ErrorInfo;
+import com.google.ipc.invalidation.external.client.types.Invalidation;
+import com.google.ipc.invalidation.external.client.types.ObjectId;
+
+import org.chromium.base.test.util.AdvancedMockContext;
+import org.chromium.base.test.util.Feature;
+import org.chromium.sync.internal_api.pub.base.ModelType;
+import org.chromium.sync.notifier.InvalidationController.IntentProtocol;
+import org.chromium.sync.notifier.InvalidationPreferences.EditContext;
+import org.chromium.sync.signin.AccountManagerHelper;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for the {@link InvalidationService}.
+ *
+ * @author dsmyers@google.com (Daniel Myers)
+ */
+public class InvalidationServiceTest extends ServiceTestCase<TestableInvalidationService> {
+ /** Id used when creating clients. */
+ private static final byte[] CLIENT_ID = new byte[]{0, 4, 7};
+
+ /** Intents provided to {@link #startService}. */
+ private List<Intent> mStartServiceIntents;
+
+ public InvalidationServiceTest() {
+ super(TestableInvalidationService.class);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mStartServiceIntents = Lists.newArrayList();
+ setContext(new AdvancedMockContext(getContext()) {
+ @Override
+ public ComponentName startService(Intent intent) {
+ mStartServiceIntents.add(intent);
+ return new ComponentName(this, InvalidationServiceTest.class);
+ }
+ });
+ setupService();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ if (InvalidationService.getIsClientStartedForTest()) {
+ Intent stopIntent = new Intent().putExtra(IntentProtocol.EXTRA_STOP, true);
+ getService().onHandleIntent(stopIntent);
+ }
+ assertFalse(InvalidationService.getIsClientStartedForTest());
+ super.tearDown();
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testComputeRegistrationOps() {
+ /*
+ * Test plan: compute the set of registration operations resulting from various combinations
+ * of existing and desired registrations. Verifying that they are correct.
+ */
+ List<ObjectId> regAccumulator = Lists.newArrayList();
+ List<ObjectId> unregAccumulator = Lists.newArrayList();
+
+ // Empty existing and desired registrations should yield empty operation sets.
+ InvalidationService.computeRegistrationOps(
+ ModelType.modelTypesToObjectIds(
+ Sets.newHashSet(ModelType.BOOKMARK, ModelType.SESSION)),
+ ModelType.modelTypesToObjectIds(
+ Sets.newHashSet(ModelType.BOOKMARK, ModelType.SESSION)),
+ regAccumulator, unregAccumulator);
+ assertEquals(0, regAccumulator.size());
+ assertEquals(0, unregAccumulator.size());
+
+ // Equal existing and desired registrations should yield empty operation sets.
+ InvalidationService.computeRegistrationOps(Sets.<ObjectId>newHashSet(),
+ Sets.<ObjectId>newHashSet(), regAccumulator, unregAccumulator);
+ assertEquals(0, regAccumulator.size());
+ assertEquals(0, unregAccumulator.size());
+
+ // Empty existing and non-empty desired registrations should yield desired registrations
+ // as the registration operations to do and no unregistrations.
+ Set<ObjectId> desiredTypes =
+ Sets.newHashSet(ModelType.BOOKMARK.toObjectId(), ModelType.SESSION.toObjectId());
+ InvalidationService.computeRegistrationOps(
+ Sets.<ObjectId>newHashSet(),
+ desiredTypes,
+ regAccumulator, unregAccumulator);
+ assertEquals(
+ Sets.newHashSet(ModelType.BOOKMARK.toObjectId(), ModelType.SESSION.toObjectId()),
+ Sets.newHashSet(regAccumulator));
+ assertEquals(0, unregAccumulator.size());
+ regAccumulator.clear();
+
+ // Unequal existing and desired registrations should yield both registrations and
+ // unregistrations. We should unregister TYPED_URL and register BOOKMARK, keeping SESSION.
+ InvalidationService.computeRegistrationOps(
+ Sets.<ObjectId>newHashSet(
+ ModelType.SESSION.toObjectId(), ModelType.TYPED_URL.toObjectId()),
+ Sets.<ObjectId>newHashSet(
+ ModelType.BOOKMARK.toObjectId(), ModelType.SESSION.toObjectId()),
+ regAccumulator, unregAccumulator);
+ assertEquals(Lists.newArrayList(ModelType.BOOKMARK.toObjectId()), regAccumulator);
+ assertEquals(Lists.newArrayList(ModelType.TYPED_URL.toObjectId()), unregAccumulator);
+ regAccumulator.clear();
+ unregAccumulator.clear();
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testReissueRegistrations() {
+ /*
+ * Test plan: call the reissueRegistrations method of the listener with both empty and
+ * non-empty sets of desired registrations stored in preferences. Verify that no register
+ * intent is set in the first case and that the appropriate register intent is sent in
+ * the second.
+ */
+
+ // No persisted registrations.
+ getService().reissueRegistrations(CLIENT_ID);
+ assertTrue(getService().mRegistrations.isEmpty());
+
+ // Persist some registrations.
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ EditContext editContext = invPrefs.edit();
+ invPrefs.setSyncTypes(editContext, Lists.newArrayList("BOOKMARK", "SESSION"));
+ assertTrue(invPrefs.commit(editContext));
+
+ // Reissue registrations and verify that the appropriate registrations are issued.
+ getService().reissueRegistrations(CLIENT_ID);
+ assertEquals(1, getService().mRegistrations.size());
+ assertEquals(
+ Sets.newHashSet(ModelType.BOOKMARK.toObjectId(), ModelType.SESSION.toObjectId()),
+ Sets.newHashSet(getService().mRegistrations.get(0)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInformRegistrationStatus() {
+ /*
+ * Test plan: call inform registration status under a variety of circumstances and verify
+ * that the appropriate (un)register calls are issued.
+ *
+ * 1. Registration of desired object. No calls issued.
+ * 2. Unregistration of undesired object. No calls issued.
+ * 3. Registration of undesired object. Unregistration issued.
+ * 4. Unregistration of desired object. Registration issued.
+ */
+ // Initial test setup: persist a single registration into preferences.
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ EditContext editContext = invPrefs.edit();
+ invPrefs.setSyncTypes(editContext, Lists.newArrayList("SESSION"));
+ assertTrue(invPrefs.commit(editContext));
+
+ // Cases 1 and 2: calls matching desired state cause no actions.
+ getService().informRegistrationStatus(CLIENT_ID, ModelType.SESSION.toObjectId(),
+ RegistrationState.REGISTERED);
+ getService().informRegistrationStatus(CLIENT_ID, ModelType.BOOKMARK.toObjectId(),
+ RegistrationState.UNREGISTERED);
+ assertTrue(getService().mRegistrations.isEmpty());
+ assertTrue(getService().mUnregistrations.isEmpty());
+
+ // Case 3: registration of undesired object triggers an unregistration.
+ getService().informRegistrationStatus(CLIENT_ID, ModelType.BOOKMARK.toObjectId(),
+ RegistrationState.REGISTERED);
+ assertEquals(1, getService().mUnregistrations.size());
+ assertEquals(0, getService().mRegistrations.size());
+ assertEquals(Lists.newArrayList(ModelType.BOOKMARK.toObjectId()),
+ getService().mUnregistrations.get(0));
+
+ // Case 4: unregistration of a desired object triggers a registration.
+ getService().informRegistrationStatus(CLIENT_ID, ModelType.SESSION.toObjectId(),
+ RegistrationState.UNREGISTERED);
+ assertEquals(1, getService().mUnregistrations.size());
+ assertEquals(1, getService().mRegistrations.size());
+ assertEquals(Lists.newArrayList(ModelType.SESSION.toObjectId()),
+ getService().mRegistrations.get(0));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInformRegistrationFailure() {
+ /*
+ * Test plan: call inform registration failure under a variety of circumstances and verify
+ * that the appropriate (un)register calls are issued.
+ *
+ * 1. Transient registration failure for an object that should be registered. Register
+ * should be called.
+ * 2. Permanent registration failure for an object that should be registered. No calls.
+ * 3. Transient registration failure for an object that should not be registered. Unregister
+ * should be called.
+ * 4. Permanent registration failure for an object should not be registered. No calls.
+ */
+
+ // Initial test setup: persist a single registration into preferences.
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ EditContext editContext = invPrefs.edit();
+ invPrefs.setSyncTypes(editContext, Lists.newArrayList("SESSION"));
+ assertTrue(invPrefs.commit(editContext));
+
+ // Cases 2 and 4: permanent registration failures never cause calls to be made.
+ getService().informRegistrationFailure(CLIENT_ID, ModelType.SESSION.toObjectId(), false,
+ "");
+ getService().informRegistrationFailure(CLIENT_ID, ModelType.BOOKMARK.toObjectId(), false,
+ "");
+ assertTrue(getService().mRegistrations.isEmpty());
+ assertTrue(getService().mUnregistrations.isEmpty());
+
+ // Case 1: transient failure of a desired registration results in re-registration.
+ getService().informRegistrationFailure(CLIENT_ID, ModelType.SESSION.toObjectId(), true, "");
+ assertEquals(1, getService().mRegistrations.size());
+ assertTrue(getService().mUnregistrations.isEmpty());
+ assertEquals(Lists.newArrayList(ModelType.SESSION.toObjectId()),
+ getService().mRegistrations.get(0));
+
+ // Case 3: transient failure of an undesired registration results in unregistration.
+ getService().informRegistrationFailure(CLIENT_ID, ModelType.BOOKMARK.toObjectId(), true,
+ "");
+ assertEquals(1, getService().mRegistrations.size());
+ assertEquals(1, getService().mUnregistrations.size());
+ assertEquals(Lists.newArrayList(ModelType.BOOKMARK.toObjectId()),
+ getService().mUnregistrations.get(0));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInformError() {
+ /*
+ * Test plan: call informError with both permanent and transient errors. Verify that
+ * the transient error causes no action to be taken and that the permanent error causes
+ * the client to be stopped.
+ */
+
+ // Client needs to be started for the permament error to trigger and stop.
+ getService().setShouldRunStates(true, true);
+ getService().onCreate();
+ getService().onHandleIntent(new Intent());
+ getService().mStartedServices.clear(); // Discard start intent.
+
+ // Transient error.
+ getService().informError(ErrorInfo.newInstance(0, true, "transient", null));
+ assertTrue(getService().mStartedServices.isEmpty());
+
+ // Permanent error.
+ getService().informError(ErrorInfo.newInstance(0, false, "permanent", null));
+ assertEquals(1, getService().mStartedServices.size());
+ Intent sentIntent = getService().mStartedServices.get(0);
+ Intent stopIntent = AndroidListener.createStopIntent(getContext());
+ assertTrue(stopIntent.filterEquals(sentIntent));
+ assertEquals(stopIntent.getExtras().keySet(), sentIntent.getExtras().keySet());
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testReadWriteState() {
+ /*
+ * Test plan: read, write, and read the internal notification client persistent state.
+ * Verify appropriate return values.
+ */
+ assertNull(getService().readState());
+ byte[] writtenState = new byte[]{7,4,0};
+ getService().writeState(writtenState);
+ assertTrue(Arrays.equals(writtenState, getService().readState()));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInvalidate() {
+ /*
+ * Test plan: call invalidate(). Verify the produced bundle has the correct fields.
+ */
+ // Call invalidate.
+ String payload = "payload";
+ int version = 4747;
+ ObjectId objectId = ModelType.BOOKMARK.toObjectId();
+ Invalidation invalidation = Invalidation.newInstance(objectId, version, payload.getBytes());
+ byte[] ackHandle = "testInvalidate".getBytes();
+ getService().invalidate(invalidation, ackHandle);
+
+ // Validate bundle.
+ assertEquals(1, getService().mRequestedSyncs.size());
+ Bundle syncBundle = getService().mRequestedSyncs.get(0);
+ assertEquals("BOOKMARK", syncBundle.getString("objectId"));
+ assertEquals(version, syncBundle.getLong("version"));
+ assertEquals(payload, syncBundle.getString("payload"));
+
+ // Ensure acknowledged.
+ assertSingleAcknowledgement(ackHandle);
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInvalidateUnknownVersion() {
+ /*
+ * Test plan: call invalidateUnknownVersion(). Verify the produced bundle has the correct
+ * fields.
+ */
+ ObjectId objectId = ModelType.BOOKMARK.toObjectId();
+ byte[] ackHandle = "testInvalidateUV".getBytes();
+ getService().invalidateUnknownVersion(objectId, ackHandle);
+
+ // Validate bundle.
+ assertEquals(1, getService().mRequestedSyncs.size());
+ Bundle syncBundle = getService().mRequestedSyncs.get(0);
+ assertEquals("BOOKMARK", syncBundle.getString("objectId"));
+ assertEquals(0, syncBundle.getLong("version"));
+ assertEquals("", syncBundle.getString("payload"));
+
+ // Ensure acknowledged.
+ assertSingleAcknowledgement(ackHandle);
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testInvalidateAll() {
+ /*
+ * Test plan: call invalidateAll(). Verify the produced bundle has the correct fields.
+ */
+ byte[] ackHandle = "testInvalidateAll".getBytes();
+ getService().invalidateAll(ackHandle);
+
+ // Validate bundle.
+ assertEquals(1, getService().mRequestedSyncs.size());
+ Bundle syncBundle = getService().mRequestedSyncs.get(0);
+ assertEquals(0, syncBundle.keySet().size());
+
+ // Ensure acknowledged.
+ assertSingleAcknowledgement(ackHandle);
+ }
+
+ /** Asserts that the service received a single acknowledgement with handle {@code ackHandle}. */
+ private void assertSingleAcknowledgement(byte[] ackHandle) {
+ assertEquals(1, getService().mAcknowledgements.size());
+ assertTrue(Arrays.equals(ackHandle, getService().mAcknowledgements.get(0)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testShouldClientBeRunning() {
+ /*
+ * Test plan: call shouldClientBeRunning with various combinations of
+ * in-foreground/sync-enabled. Verify appropriate return values.
+ */
+ getService().setShouldRunStates(false, false);
+ assertFalse(getService().shouldClientBeRunning());
+
+ getService().setShouldRunStates(false, true);
+ assertFalse(getService().shouldClientBeRunning());
+
+ getService().setShouldRunStates(true, false);
+ assertFalse(getService().shouldClientBeRunning());
+
+ // Should only be running if both in the foreground and sync is enabled.
+ getService().setShouldRunStates(true, true);
+ assertTrue(getService().shouldClientBeRunning());
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testStartAndStopClient() {
+ /*
+ * Test plan: with Chrome configured so that the client should run, send it an empty
+ * intent. Even though no owning account is known, the client should still start. Send
+ * it a stop intent and verify that it stops.
+ */
+
+ // Note: we are manipulating the service object directly, rather than through startService,
+ // because otherwise we would need to handle the asynchronous execution model of the
+ // underlying IntentService.
+ getService().setShouldRunStates(true, true);
+ getService().onCreate();
+
+ Intent startIntent = new Intent();
+ getService().onHandleIntent(startIntent);
+ assertTrue(InvalidationService.getIsClientStartedForTest());
+
+ Intent stopIntent = new Intent().putExtra(IntentProtocol.EXTRA_STOP, true);
+ getService().onHandleIntent(stopIntent);
+ assertFalse(InvalidationService.getIsClientStartedForTest());
+
+ // The issued intents should have been an AndroidListener start intent followed by an
+ // AndroidListener stop intent.
+ assertEquals(2, mStartServiceIntents.size());
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(0)));
+ assertTrue(isAndroidListenerStopIntent(mStartServiceIntents.get(1)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testClientStopsWhenShouldNotBeRunning() {
+ /*
+ * Test plan: start the client. Then, change the configuration so that Chrome should not
+ * be running. Send an intent to the service and verify that it stops.
+ */
+ getService().setShouldRunStates(true, true);
+ getService().onCreate();
+
+ // Start the service.
+ Intent startIntent = new Intent();
+ getService().onHandleIntent(startIntent);
+ assertTrue(InvalidationService.getIsClientStartedForTest());
+
+ // Change configuration.
+ getService().setShouldRunStates(false, false);
+
+ // Send an Intent and verify that the service stops.
+ getService().onHandleIntent(startIntent);
+ assertFalse(InvalidationService.getIsClientStartedForTest());
+
+ // The issued intents should have been an AndroidListener start intent followed by an
+ // AndroidListener stop intent.
+ assertEquals(2, mStartServiceIntents.size());
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(0)));
+ assertTrue(isAndroidListenerStopIntent(mStartServiceIntents.get(1)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testRegistrationIntent() {
+ /*
+ * Test plan: send a registration-change intent. Verify that it starts the client and
+ * sets both the account and registrations in shared preferences.
+ */
+ getService().setShouldRunStates(true, true);
+ getService().onCreate();
+
+ // Send register Intent.
+ ImmutableSet<ModelType> desiredRegistrations =
+ ImmutableSet.of(ModelType.BOOKMARK, ModelType.SESSION);
+ Account account = AccountManagerHelper.createAccountFromName("test@example.com");
+ Intent registrationIntent = IntentProtocol.createRegisterIntent(account, false,
+ desiredRegistrations);
+ getService().onHandleIntent(registrationIntent);
+
+ // Verify client started and state written.
+ assertTrue(InvalidationService.getIsClientStartedForTest());
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ assertEquals(account, invPrefs.getSavedSyncedAccount());
+ assertEquals(ModelType.modelTypesToSyncTypes(desiredRegistrations),
+ invPrefs.getSavedSyncedTypes());
+ assertEquals(1, mStartServiceIntents.size());
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(0)));
+
+ // Send another registration-change intent, this type with all-types set to true, and
+ // verify that the on-disk state is updated and that no addition Intents are issued.
+ getService().onHandleIntent(IntentProtocol.createRegisterIntent(account, true, null));
+ assertEquals(account, invPrefs.getSavedSyncedAccount());
+ assertEquals(ImmutableSet.of(ModelType.ALL_TYPES_TYPE), invPrefs.getSavedSyncedTypes());
+ assertEquals(1, mStartServiceIntents.size());
+
+ // Finally, send one more registration-change intent, this time with a different account,
+ // and verify that it both updates the account, stops thye existing client, and
+ // starts a new client.
+ Account account2 = AccountManagerHelper.createAccountFromName("test2@example.com");
+ getService().onHandleIntent(IntentProtocol.createRegisterIntent(account2, true, null));
+ assertEquals(account2, invPrefs.getSavedSyncedAccount());
+ assertEquals(3, mStartServiceIntents.size());
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(0)));
+ assertTrue(isAndroidListenerStopIntent(mStartServiceIntents.get(1)));
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(2)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testRegistrationIntentWhenClientShouldNotBeRunning() {
+ /*
+ * Test plan: send a registration change event when the client should not be running.
+ * Verify that the service updates the on-disk state but does not start the client.
+ */
+ getService().onCreate();
+
+ // Send register Intent.
+ Account account = AccountManagerHelper.createAccountFromName("test@example.com");
+ ImmutableSet<ModelType> desiredRegistrations =
+ ImmutableSet.of(ModelType.BOOKMARK, ModelType.SESSION);
+ Intent registrationIntent = IntentProtocol.createRegisterIntent(account, false,
+ desiredRegistrations);
+ getService().onHandleIntent(registrationIntent);
+
+ // Verify state written but client not started.
+ assertFalse(InvalidationService.getIsClientStartedForTest());
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ assertEquals(account, invPrefs.getSavedSyncedAccount());
+ assertEquals(ModelType.modelTypesToSyncTypes(desiredRegistrations),
+ invPrefs.getSavedSyncedTypes());
+ assertEquals(0, mStartServiceIntents.size());
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testDeferredRegistrationsIssued() {
+ /*
+ * Test plan: send a registration-change intent. Verify that the client issues a start
+ * intent but makes no registration calls. Issue a reissueRegistrations call and verify
+ * that the client does issue the appropriate registrations.
+ */
+ getService().setShouldRunStates(true, true);
+ getService().onCreate();
+
+ // Send register Intent. Verify client started but no registrations issued.
+ Account account = AccountManagerHelper.createAccountFromName("test@example.com");
+ ImmutableSet<ModelType> desiredRegistrations =
+ ImmutableSet.of(ModelType.BOOKMARK, ModelType.SESSION);
+ Set<ObjectId> desiredObjectIds = ModelType.modelTypesToObjectIds(desiredRegistrations);
+
+ Intent registrationIntent = IntentProtocol.createRegisterIntent(account, false,
+ desiredRegistrations);
+ getService().onHandleIntent(registrationIntent);
+ assertTrue(InvalidationService.getIsClientStartedForTest());
+ assertEquals(1, mStartServiceIntents.size());
+ assertTrue(isAndroidListenerStartIntent(mStartServiceIntents.get(0)));
+ InvalidationPreferences invPrefs = new InvalidationPreferences(getContext());
+ assertEquals(ModelType.modelTypesToSyncTypes(desiredRegistrations),
+ invPrefs.getSavedSyncedTypes());
+ assertEquals(desiredObjectIds, getService().readRegistrationsFromPrefs());
+
+ // Issue reissueRegistrations; verify registration intent issues.
+ getService().reissueRegistrations(CLIENT_ID);
+ assertEquals(2, mStartServiceIntents.size());
+ Intent expectedRegisterIntent = AndroidListener.createRegisterIntent(
+ getContext(),
+ CLIENT_ID,
+ desiredObjectIds);
+ Intent actualRegisterIntent = mStartServiceIntents.get(1);
+ assertTrue(expectedRegisterIntent.filterEquals(actualRegisterIntent));
+ assertEquals(expectedRegisterIntent.getExtras().keySet(),
+ actualRegisterIntent.getExtras().keySet());
+ assertEquals(
+ desiredObjectIds,
+ Sets.newHashSet(getService().mRegistrations.get(0)));
+ }
+
+ @SmallTest
+ @Feature({"Sync"})
+ public void testRegistrationRetries() {
+ /*
+ * Test plan: validate that the alarm receiver used by the AndroidListener underlying
+ * InvalidationService is correctly configured in the manifest and retries registrations
+ * with exponential backoff. May need to be implemented as a downstream Chrome for Android
+ * test.
+ */
+ // TODO(dsmyers): implement.
+ // Bug: https://code.google.com/p/chromium/issues/detail?id=172398
+ }
+
+ /** Returns whether {@code intent} is an {@link AndroidListener} start intent. */
+ private boolean isAndroidListenerStartIntent(Intent intent) {
+ Intent startIntent = AndroidListener.createStartIntent(getContext(),
+ InvalidationService.CLIENT_TYPE, "unused".getBytes());
+ return intent.getExtras().keySet().equals(startIntent.getExtras().keySet());
+ }
+
+ /** Returns whether {@code intent} is an {@link AndroidListener} stop intent. */
+ private boolean isAndroidListenerStopIntent(Intent intent) {
+ Intent stopIntent = AndroidListener.createStopIntent(getContext());
+ return intent.getExtras().keySet().equals(stopIntent.getExtras().keySet());
+ }
+}
diff --git a/sync/android/javatests/src/org/chromium/sync/notifier/TestableInvalidationService.java b/sync/android/javatests/src/org/chromium/sync/notifier/TestableInvalidationService.java
new file mode 100644
index 0000000..9614a10
--- /dev/null
+++ b/sync/android/javatests/src/org/chromium/sync/notifier/TestableInvalidationService.java
@@ -0,0 +1,97 @@
+// Copyright (c) 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.sync.notifier;
+
+import com.google.common.collect.Lists;
+import com.google.ipc.invalidation.external.client.types.AckHandle;
+import com.google.ipc.invalidation.external.client.types.ObjectId;
+
+import android.accounts.Account;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+
+import java.util.List;
+
+/**
+ * Subclass of {@link InvalidationService} that captures events and allows controlling
+ * whether or not Chrome is in the foreground and sync is enabled.
+ *
+ * @author dsmyers@google.com (Daniel Myers)
+ */
+public class TestableInvalidationService extends InvalidationService {
+ /** Object ids given to {@link #register}, one list element per call. */
+ final List<List<ObjectId>> mRegistrations = Lists.newArrayList();
+
+ /** Object ids given to {@link #unregister}, one list element per call. */
+ final List<List<ObjectId>> mUnregistrations = Lists.newArrayList();
+
+ /** Intents given to {@link #startService}. */
+ final List<Intent> mStartedServices = Lists.newArrayList();
+
+ /** Bundles given to {@link #requestSyncFromContentResolver}. */
+ final List<Bundle> mRequestedSyncs = Lists.newArrayList();
+
+ final List<byte[]> mAcknowledgements = Lists.newArrayList();
+
+ /** Whether Chrome is in the foreground. */
+ private boolean mIsChromeInForeground = false;
+
+ /** Whether sync is enabled. */
+ private boolean mIsSyncEnabled = false;
+
+ public TestableInvalidationService() {
+ }
+
+ @Override
+ public void acknowledge(byte[] ackHandle) {
+ mAcknowledgements.add(ackHandle);
+ }
+
+ @Override
+ public void register(byte[] clientId, Iterable<ObjectId> objectIds) {
+ mRegistrations.add(Lists.newArrayList(objectIds));
+ super.register(clientId, objectIds);
+ }
+
+ @Override
+ public void unregister(byte[] clientId, Iterable<ObjectId> objectIds) {
+ mUnregistrations.add(Lists.newArrayList(objectIds));
+ super.unregister(clientId, objectIds);
+ }
+
+ @Override
+ public ComponentName startService(Intent intent) {
+ mStartedServices.add(intent);
+ return super.startService(intent);
+ }
+
+ @Override
+ public void requestSyncFromContentResolver(Bundle bundle, Account account,
+ String contractAuthority) {
+ mRequestedSyncs.add(bundle);
+ super.requestSyncFromContentResolver(bundle, account, contractAuthority);
+ }
+
+ @Override
+ boolean isChromeInForeground() {
+ return mIsChromeInForeground;
+ }
+
+ @Override
+ boolean isSyncEnabled() {
+ return mIsSyncEnabled;
+ }
+
+ /**
+ * Sets the variables used to control whether or not a notification client should be running.
+ * @param isChromeInForeground whether Chrome is in the foreground
+ * @param isSyncEnabled whether sync is enabled
+ */
+ void setShouldRunStates(boolean isChromeInForeground, boolean isSyncEnabled) {
+ this.mIsChromeInForeground = isChromeInForeground;
+ this.mIsSyncEnabled = isSyncEnabled;
+ }
+}
diff --git a/sync/sync.gyp b/sync/sync.gyp
index 17201cd..12f5267 100644
--- a/sync/sync.gyp
+++ b/sync/sync.gyp
@@ -1016,6 +1016,7 @@
'java_in_dir': '../sync/android/java',
},
'dependencies': [
+ '../base/base.gyp:base_java',
'../third_party/cacheinvalidation/cacheinvalidation.gyp:cacheinvalidation_javalib',
'../third_party/guava/guava.gyp:guava_javalib',
'../third_party/jsr-305/jsr-305.gyp:jsr_305_javalib',