diff options
author | dsmyers@chromium.org <dsmyers@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-26 05:54:23 +0000 |
---|---|---|
committer | dsmyers@chromium.org <dsmyers@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-01-26 05:54:23 +0000 |
commit | 8290db69025a2e16162aef76144e134210beb5dc (patch) | |
tree | 60491ecc72219556a4391d75a709d3ff37a58727 /sync | |
parent | 481d5a081cdce2361607c44a414def7ddb1c2faf (diff) | |
download | chromium_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')
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', |