1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.sync.notifier;
7 import android.accounts.Account;
8 import android.app.PendingIntent;
9 import android.content.ContentResolver;
10 import android.content.Intent;
11 import android.os.Bundle;
12 import android.util.Log;
14 import com.google.common.annotations.VisibleForTesting;
15 import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
16 import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
17 import com.google.ipc.invalidation.external.client.types.ErrorInfo;
18 import com.google.ipc.invalidation.external.client.types.Invalidation;
19 import com.google.ipc.invalidation.external.client.types.ObjectId;
20 import com.google.protos.ipc.invalidation.Types.ClientType;
22 import org.chromium.base.ActivityStatus;
23 import org.chromium.base.CollectionUtil;
24 import org.chromium.sync.internal_api.pub.base.ModelType;
25 import org.chromium.sync.notifier.InvalidationIntentProtocol;
26 import org.chromium.sync.notifier.InvalidationPreferences.EditContext;
27 import org.chromium.sync.signin.AccountManagerHelper;
28 import org.chromium.sync.signin.ChromeSigninController;
30 import java.util.Collections;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Random;
36 import javax.annotation.Nullable;
39 * Service that controls notifications for sync.
41 * This service serves two roles. On the one hand, it is a client for the notification system
42 * used to trigger sync. It receives invalidations and converts them into
43 * {@link ContentResolver#requestSync} calls, and it supplies the notification system with the set
44 * of desired registrations when requested.
46 * On the other hand, this class is controller for the notification system. It starts it and stops
47 * it, and it requests that it perform (un)registrations as the set of desired sync types changes.
49 * This class is an {@code IntentService}. All methods are assumed to be executing on its single
52 * @author dsmyers@google.com
54 public class InvalidationService extends AndroidListener {
55 /* This class must be public because it is exposed as a service. */
57 /** Notification client typecode. */
59 static final int CLIENT_TYPE = ClientType.Type.CHROME_SYNC_ANDROID_VALUE;
61 private static final String TAG = "InvalidationService";
63 private static final Random RANDOM = new Random();
66 * Whether the underlying notification client has been started. This boolean is updated when a
67 * start or stop intent is issued to the underlying client, not when the intent is actually
70 private static boolean sIsClientStarted;
73 * The id of the client in use, if any. May be {@code null} if {@link #sIsClientStarted} is
74 * true if the client has not yet gone ready.
76 @Nullable private static byte[] sClientId;
79 public void onHandleIntent(Intent intent) {
80 // Ensure that a client is or is not running, as appropriate, and that it is for the
81 // correct account. ensureAccount will stop the client if account is non-null and doesn't
82 // match the stored account. Then, if a client should be running, ensureClientStartState
83 // will start a new one if needed. I.e., these two functions work together to restart the
84 // client when the account changes.
85 Account account = intent.hasExtra(InvalidationIntentProtocol.EXTRA_ACCOUNT) ?
86 (Account) intent.getParcelableExtra(InvalidationIntentProtocol.EXTRA_ACCOUNT)
88 ensureAccount(account);
89 ensureClientStartState();
92 if (InvalidationIntentProtocol.isStop(intent) && sIsClientStarted) {
93 // If the intent requests that the client be stopped, stop it.
95 } else if (InvalidationIntentProtocol.isRegisteredTypesChange(intent)) {
96 // If the intent requests a change in registrations, change them.
97 List<String> regTypes = intent.getStringArrayListExtra(
98 InvalidationIntentProtocol.EXTRA_REGISTERED_TYPES);
99 setRegisteredTypes(regTypes != null ? new HashSet<String>(regTypes) : null,
100 InvalidationIntentProtocol.getRegisteredObjectIds(intent));
102 // Otherwise, we don't recognize the intent. Pass it to the notification client service.
103 super.onHandleIntent(intent);
108 public void invalidate(Invalidation invalidation, byte[] ackHandle) {
109 byte[] payload = invalidation.getPayload();
110 String payloadStr = (payload == null) ? null : new String(payload);
111 requestSync(invalidation.getObjectId(), invalidation.getVersion(), payloadStr);
112 acknowledge(ackHandle);
116 public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {
117 requestSync(objectId, null, null);
118 acknowledge(ackHandle);
122 public void invalidateAll(byte[] ackHandle) {
123 requestSync(null, null, null);
124 acknowledge(ackHandle);
128 public void informRegistrationFailure(
129 byte[] clientId, ObjectId objectId, boolean isTransient, String errorMessage) {
130 Log.w(TAG, "Registration failure on " + objectId + " ; transient = " + isTransient
131 + ": " + errorMessage);
133 // Retry immediately on transient failures. The base AndroidListener will handle
134 // exponential backoff if there are repeated failures.
135 List<ObjectId> objectIdAsList = CollectionUtil.newArrayList(objectId);
136 if (readRegistrationsFromPrefs().contains(objectId)) {
137 register(clientId, objectIdAsList);
139 unregister(clientId, objectIdAsList);
145 public void informRegistrationStatus(
146 byte[] clientId, ObjectId objectId, RegistrationState regState) {
147 Log.d(TAG, "Registration status for " + objectId + ": " + regState);
148 List<ObjectId> objectIdAsList = CollectionUtil.newArrayList(objectId);
149 boolean registrationisDesired = readRegistrationsFromPrefs().contains(objectId);
150 if (regState == RegistrationState.REGISTERED) {
151 if (!registrationisDesired) {
152 Log.i(TAG, "Unregistering for object we're no longer interested in");
153 unregister(clientId, objectIdAsList);
156 if (registrationisDesired) {
157 Log.i(TAG, "Registering for an object");
158 register(clientId, objectIdAsList);
164 public void informError(ErrorInfo errorInfo) {
165 Log.w(TAG, "Invalidation client error:" + errorInfo);
166 if (!errorInfo.isTransient() && sIsClientStarted) {
167 // It is important not to stop the client if it is already stopped. Otherwise, the
168 // possibility exists to go into an infinite loop if the stop call itself triggers an
169 // error (e.g., because no client actually exists).
175 public void ready(byte[] clientId) {
176 setClientId(clientId);
178 // We might have accumulated some registrations to do while we were waiting for the client
180 reissueRegistrations(clientId);
184 public void reissueRegistrations(byte[] clientId) {
185 Set<ObjectId> desiredRegistrations = readRegistrationsFromPrefs();
186 if (!desiredRegistrations.isEmpty()) {
187 register(clientId, desiredRegistrations);
192 public void requestAuthToken(final PendingIntent pendingIntent,
193 @Nullable String invalidAuthToken) {
194 @Nullable Account account = ChromeSigninController.get(this).getSignedInUser();
195 if (account == null) {
196 // This should never happen, because this code should only be run if a user is
198 Log.w(TAG, "No signed-in user; cannot send message to data center");
202 // Attempt to retrieve a token for the user. This method will also invalidate
203 // invalidAuthToken if it is non-null.
204 AccountManagerHelper.get(this).getNewAuthTokenFromForeground(
205 account, invalidAuthToken, getOAuth2ScopeWithType(),
206 new AccountManagerHelper.GetAuthTokenCallback() {
208 public void tokenAvailable(String token) {
210 setAuthToken(InvalidationService.this.getApplicationContext(),
211 pendingIntent, token, getOAuth2ScopeWithType());
218 public void writeState(byte[] data) {
219 InvalidationPreferences invPreferences = new InvalidationPreferences(this);
220 EditContext editContext = invPreferences.edit();
221 invPreferences.setInternalNotificationClientState(editContext, data);
222 invPreferences.commit(editContext);
226 @Nullable public byte[] readState() {
227 return new InvalidationPreferences(this).getInternalNotificationClientState();
231 * Ensures that the client is running or not running as appropriate, based on the value of
232 * {@link #shouldClientBeRunning}.
234 private void ensureClientStartState() {
235 final boolean shouldClientBeRunning = shouldClientBeRunning();
236 if (!shouldClientBeRunning && sIsClientStarted) {
237 // Stop the client if it should not be running and is.
239 } else if (shouldClientBeRunning && !sIsClientStarted) {
240 // Start the client if it should be running and isn't.
246 * If {@code intendedAccount} is non-{@null} and differs from the account stored in preferences,
247 * then stops the existing client (if any) and updates the stored account.
249 private void ensureAccount(@Nullable Account intendedAccount) {
250 if (intendedAccount == null) {
253 InvalidationPreferences invPrefs = new InvalidationPreferences(this);
254 if (!intendedAccount.equals(invPrefs.getSavedSyncedAccount())) {
255 if (sIsClientStarted) {
258 setAccount(intendedAccount);
263 * Starts a new client, destroying any existing client. {@code owningAccount} is the account
264 * of the user for which the client is being created; it will be persisted using
265 * {@link InvalidationPreferences#setAccount}.
267 private void startClient() {
268 Intent startIntent = AndroidListener.createStartIntent(this, CLIENT_TYPE, getClientName());
269 startService(startIntent);
270 setIsClientStarted(true);
273 /** Stops the notification client. */
274 private void stopClient() {
275 startService(AndroidListener.createStopIntent(this));
276 setIsClientStarted(false);
280 /** Sets the saved sync account in {@link InvalidationPreferences} to {@code owningAccount}. */
281 private void setAccount(Account owningAccount) {
282 InvalidationPreferences invPrefs = new InvalidationPreferences(this);
283 EditContext editContext = invPrefs.edit();
284 invPrefs.setAccount(editContext, owningAccount);
285 invPrefs.commit(editContext);
289 * Reads the saved sync types from storage (if any) and returns a set containing the
290 * corresponding object ids.
292 private Set<ObjectId> readSyncRegistrationsFromPrefs() {
293 Set<String> savedTypes = new InvalidationPreferences(this).getSavedSyncedTypes();
294 if (savedTypes == null) return Collections.emptySet();
295 else return ModelType.syncTypesToObjectIds(savedTypes);
299 * Reads the saved non-sync object ids from storage (if any) and returns a set containing the
300 * corresponding object ids.
302 private Set<ObjectId> readNonSyncRegistrationsFromPrefs() {
303 Set<ObjectId> objectIds = new InvalidationPreferences(this).getSavedObjectIds();
304 if (objectIds == null) return Collections.emptySet();
305 else return objectIds;
309 * Reads the object registrations from storage (if any) and returns a set containing the
310 * corresponding object ids.
313 Set<ObjectId> readRegistrationsFromPrefs() {
314 return joinRegistrations(readSyncRegistrationsFromPrefs(),
315 readNonSyncRegistrationsFromPrefs());
319 * Join Sync object registrations with non-Sync object registrations to get the full set of
320 * desired object registrations.
322 private static Set<ObjectId> joinRegistrations(Set<ObjectId> syncRegistrations,
323 Set<ObjectId> nonSyncRegistrations) {
324 if (nonSyncRegistrations.isEmpty()) {
325 return syncRegistrations;
327 if (syncRegistrations.isEmpty()) {
328 return nonSyncRegistrations;
330 Set<ObjectId> registrations = new HashSet<ObjectId>(
331 syncRegistrations.size() + nonSyncRegistrations.size());
332 registrations.addAll(syncRegistrations);
333 registrations.addAll(nonSyncRegistrations);
334 return registrations;
338 * Sets the types for which notifications are required to {@code syncTypes}. {@code syncTypes}
339 * is either a list of specific types or the special wildcard type
340 * {@link ModelType#ALL_TYPES_TYPE}. Also registers for additional objects specified by
341 * {@code objectIds}. Either parameter may be null if the corresponding registrations are not
346 private void setRegisteredTypes(Set<String> syncTypes, Set<ObjectId> objectIds) {
347 // If we have a ready client and will be making registration change calls on it, then
348 // read the current registrations from preferences before we write the new values, so that
349 // we can take the diff of the two registration sets and determine which registration change
351 Set<ObjectId> existingSyncRegistrations = (sClientId == null) ?
352 null : readSyncRegistrationsFromPrefs();
353 Set<ObjectId> existingNonSyncRegistrations = (sClientId == null) ?
354 null : readNonSyncRegistrationsFromPrefs();
356 // Write the new sync types/object ids to preferences. We do not expand the syncTypes to
357 // take into account the ALL_TYPES_TYPE at this point; we want to persist the wildcard
359 InvalidationPreferences prefs = new InvalidationPreferences(this);
360 EditContext editContext = prefs.edit();
361 if (syncTypes != null) {
362 prefs.setSyncTypes(editContext, syncTypes);
364 if (objectIds != null) {
365 prefs.setObjectIds(editContext, objectIds);
367 prefs.commit(editContext);
369 // If we do not have a ready invalidation client, we cannot change its registrations, so
370 // return. Later, when the client is ready, we will supply the new registrations.
371 if (sClientId == null) {
375 // We do have a ready client. Unregister any existing registrations not present in the
376 // new set and register any elements in the new set not already present. This call does
377 // expansion of the ALL_TYPES_TYPE wildcard.
378 // NOTE: syncTypes MUST NOT be used below this line, since it contains an unexpanded
380 // When computing the desired set of object ids, if only sync types were provided, then
381 // keep the existing non-sync types, and vice-versa.
382 Set<ObjectId> desiredSyncRegistrations = syncTypes != null ?
383 ModelType.syncTypesToObjectIds(syncTypes) : existingSyncRegistrations;
384 Set<ObjectId> desiredNonSyncRegistrations = objectIds != null ?
385 objectIds : existingNonSyncRegistrations;
386 Set<ObjectId> desiredRegistrations = joinRegistrations(desiredNonSyncRegistrations,
387 desiredSyncRegistrations);
388 Set<ObjectId> existingRegistrations = joinRegistrations(existingNonSyncRegistrations,
389 existingSyncRegistrations);
391 Set<ObjectId> unregistrations = new HashSet<ObjectId>();
392 Set<ObjectId> registrations = new HashSet<ObjectId>();
393 computeRegistrationOps(existingRegistrations, desiredRegistrations,
394 registrations, unregistrations);
395 unregister(sClientId, unregistrations);
396 register(sClientId, registrations);
400 * Computes the set of (un)registrations to perform so that the registrations active in the
401 * Ticl will be {@code desiredRegs}, given that {@existingRegs} already exist.
403 * @param regAccumulator registrations to perform
404 * @param unregAccumulator unregistrations to perform.
407 static void computeRegistrationOps(Set<ObjectId> existingRegs, Set<ObjectId> desiredRegs,
408 Set<ObjectId> regAccumulator, Set<ObjectId> unregAccumulator) {
410 // Registrations to do are elements in the new set but not the old set.
411 regAccumulator.addAll(desiredRegs);
412 regAccumulator.removeAll(existingRegs);
414 // Unregistrations to do are elements in the old set but not the new set.
415 unregAccumulator.addAll(existingRegs);
416 unregAccumulator.removeAll(desiredRegs);
420 * Requests that the sync system perform a sync.
422 * @param objectId the object that changed, if known.
423 * @param version the version of the object that changed, if known.
424 * @param payload the payload of the change, if known.
426 private void requestSync(@Nullable ObjectId objectId, @Nullable Long version,
427 @Nullable String payload) {
428 // Construct the bundle to supply to the native sync code.
429 Bundle bundle = new Bundle();
430 if (objectId == null && version == null && payload == null) {
431 // Use an empty bundle in this case for compatibility with the v1 implementation.
433 if (objectId != null) {
434 bundle.putInt("objectSource", objectId.getSource());
435 bundle.putString("objectId", new String(objectId.getName()));
437 // We use "0" as the version if we have an unknown-version invalidation. This is OK
438 // because the native sync code special-cases zero and always syncs for invalidations at
439 // that version (Tango defines a special UNKNOWN_VERSION constant with this value).
440 bundle.putLong("version", (version == null) ? 0 : version);
441 bundle.putString("payload", (payload == null) ? "" : payload);
443 Account account = ChromeSigninController.get(this).getSignedInUser();
444 String contractAuthority = SyncStatusHelper.get(this).getContractAuthority();
445 requestSyncFromContentResolver(bundle, account, contractAuthority);
449 * Calls {@link ContentResolver#requestSync(Account, String, Bundle)} to trigger a sync. Split
450 * into a separate method so that it can be overriden in tests.
453 void requestSyncFromContentResolver(
454 Bundle bundle, Account account, String contractAuthority) {
455 Log.d(TAG, "Request sync: " + account + " / " + contractAuthority + " / "
457 ContentResolver.requestSync(account, contractAuthority, bundle);
461 * Returns whether the notification client should be running, i.e., whether Chrome is in the
462 * foreground and sync is enabled.
465 boolean shouldClientBeRunning() {
466 return isSyncEnabled() && isChromeInForeground();
469 /** Returns whether sync is enabled. LLocal method so it can be overridden in tests. */
471 boolean isSyncEnabled() {
472 return SyncStatusHelper.get(getApplicationContext()).isSyncEnabled();
476 * Returns whether Chrome is in the foreground. Local method so it can be overridden in tests.
479 boolean isChromeInForeground() {
480 return ActivityStatus.isApplicationVisible();
483 /** Returns whether the notification client has been started, for tests. */
485 static boolean getIsClientStartedForTest() {
486 return sIsClientStarted;
489 /** Returns the notification client id, for tests. */
491 @Nullable static byte[] getClientIdForTest() {
495 private static String getOAuth2ScopeWithType() {
496 return "oauth2:" + SyncStatusHelper.CHROME_SYNC_OAUTH2_SCOPE;
499 /** Returns the client name used for the notification client. */
500 private static byte[] getClientName() {
501 // TODO(dsmyers): we should use the same client name as the native sync code.
502 // Bug: https://code.google.com/p/chromium/issues/detail?id=172391
503 return Long.toString(RANDOM.nextLong()).getBytes();
506 private static void setClientId(byte[] clientId) {
507 sClientId = clientId;
510 private static void setIsClientStarted(boolean isStarted) {
511 sIsClientStarted = isStarted;