1 // Copyright 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. 4 5 package org.chromium.sync.notifier; 6 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; 13 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; 21 22 import org.chromium.base.ApplicationStatus; 23 import org.chromium.base.CollectionUtil; 24 import org.chromium.sync.internal_api.pub.base.ModelType; 25 import org.chromium.sync.notifier.InvalidationPreferences.EditContext; 26 import org.chromium.sync.signin.AccountManagerHelper; 27 import org.chromium.sync.signin.ChromeSigninController; 28 29 import java.util.Collections; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Random; 33 import java.util.Set; 34 35 import javax.annotation.Nullable; 36 37 /** 38 * Service that controls notifications for sync. 39 * <p> 40 * This service serves two roles. On the one hand, it is a client for the notification system 41 * used to trigger sync. It receives invalidations and converts them into 42 * {@link ContentResolver#requestSync} calls, and it supplies the notification system with the set 43 * of desired registrations when requested. 44 * <p> 45 * On the other hand, this class is controller for the notification system. It starts it and stops 46 * it, and it requests that it perform (un)registrations as the set of desired sync types changes. 47 * <p> 48 * This class is an {@code IntentService}. All methods are assumed to be executing on its single 49 * execution thread. 50 * 51 * @author dsmyers (at) google.com 52 */ 53 public class InvalidationService extends AndroidListener { 54 /* This class must be public because it is exposed as a service. */ 55 56 /** Notification client typecode. */ 57 @VisibleForTesting 58 static final int CLIENT_TYPE = ClientType.Type.CHROME_SYNC_ANDROID_VALUE; 59 60 private static final String TAG = "InvalidationService"; 61 62 private static final Random RANDOM = new Random(); 63 64 /** 65 * Whether the underlying notification client has been started. This boolean is updated when a 66 * start or stop intent is issued to the underlying client, not when the intent is actually 67 * processed. 68 */ 69 private static boolean sIsClientStarted; 70 71 /** 72 * The id of the client in use, if any. May be {@code null} if {@link #sIsClientStarted} is 73 * true if the client has not yet gone ready. 74 */ 75 @Nullable private static byte[] sClientId; 76 77 @Override 78 public void onHandleIntent(Intent intent) { 79 // Ensure that a client is or is not running, as appropriate, and that it is for the 80 // correct account. ensureAccount will stop the client if account is non-null and doesn't 81 // match the stored account. Then, if a client should be running, ensureClientStartState 82 // will start a new one if needed. I.e., these two functions work together to restart the 83 // client when the account changes. 84 Account account = intent.hasExtra(InvalidationIntentProtocol.EXTRA_ACCOUNT) ? 85 (Account) intent.getParcelableExtra(InvalidationIntentProtocol.EXTRA_ACCOUNT) 86 : null; 87 88 ensureAccount(account); 89 ensureClientStartState(); 90 91 // Handle the intent. 92 if (InvalidationIntentProtocol.isStop(intent) && sIsClientStarted) { 93 // If the intent requests that the client be stopped, stop it. 94 stopClient(); 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)); 101 } else { 102 // Otherwise, we don't recognize the intent. Pass it to the notification client service. 103 super.onHandleIntent(intent); 104 } 105 } 106 107 @Override 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); 113 } 114 115 @Override 116 public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) { 117 requestSync(objectId, null, null); 118 acknowledge(ackHandle); 119 } 120 121 @Override 122 public void invalidateAll(byte[] ackHandle) { 123 requestSync(null, null, null); 124 acknowledge(ackHandle); 125 } 126 127 @Override 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); 132 if (isTransient) { 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); 138 } else { 139 unregister(clientId, objectIdAsList); 140 } 141 } 142 } 143 144 @Override 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); 154 } 155 } else { 156 if (registrationisDesired) { 157 Log.i(TAG, "Registering for an object"); 158 register(clientId, objectIdAsList); 159 } 160 } 161 } 162 163 @Override 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). 170 stopClient(); 171 } 172 } 173 174 @Override 175 public void ready(byte[] clientId) { 176 setClientId(clientId); 177 178 // We might have accumulated some registrations to do while we were waiting for the client 179 // to become ready. 180 reissueRegistrations(clientId); 181 } 182 183 @Override 184 public void reissueRegistrations(byte[] clientId) { 185 Set<ObjectId> desiredRegistrations = readRegistrationsFromPrefs(); 186 if (!desiredRegistrations.isEmpty()) { 187 register(clientId, desiredRegistrations); 188 } 189 } 190 191 @Override 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 197 // signed-in. 198 Log.w(TAG, "No signed-in user; cannot send message to data center"); 199 return; 200 } 201 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() { 207 @Override 208 public void tokenAvailable(String token) { 209 if (token != null) { 210 setAuthToken(InvalidationService.this.getApplicationContext(), 211 pendingIntent, token, getOAuth2ScopeWithType()); 212 } 213 } 214 }); 215 } 216 217 @Override 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); 223 } 224 225 @Override 226 @Nullable public byte[] readState() { 227 return new InvalidationPreferences(this).getInternalNotificationClientState(); 228 } 229 230 /** 231 * Ensures that the client is running or not running as appropriate, based on the value of 232 * {@link #shouldClientBeRunning}. 233 */ 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. 238 stopClient(); 239 } else if (shouldClientBeRunning && !sIsClientStarted) { 240 // Start the client if it should be running and isn't. 241 startClient(); 242 } 243 } 244 245 /** 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. 248 */ 249 private void ensureAccount(@Nullable Account intendedAccount) { 250 if (intendedAccount == null) { 251 return; 252 } 253 InvalidationPreferences invPrefs = new InvalidationPreferences(this); 254 if (!intendedAccount.equals(invPrefs.getSavedSyncedAccount())) { 255 if (sIsClientStarted) { 256 stopClient(); 257 } 258 setAccount(intendedAccount); 259 } 260 } 261 262 /** 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}. 266 */ 267 private void startClient() { 268 byte[] clientName = InvalidationClientNameProvider.get().getInvalidatorClientName(); 269 Intent startIntent = AndroidListener.createStartIntent(this, CLIENT_TYPE, clientName); 270 startService(startIntent); 271 setIsClientStarted(true); 272 } 273 274 /** Stops the notification client. */ 275 private void stopClient() { 276 startService(AndroidListener.createStopIntent(this)); 277 setIsClientStarted(false); 278 setClientId(null); 279 } 280 281 /** Sets the saved sync account in {@link InvalidationPreferences} to {@code owningAccount}. */ 282 private void setAccount(Account owningAccount) { 283 InvalidationPreferences invPrefs = new InvalidationPreferences(this); 284 EditContext editContext = invPrefs.edit(); 285 invPrefs.setAccount(editContext, owningAccount); 286 invPrefs.commit(editContext); 287 } 288 289 /** 290 * Reads the saved sync types from storage (if any) and returns a set containing the 291 * corresponding object ids. 292 */ 293 private Set<ObjectId> readSyncRegistrationsFromPrefs() { 294 Set<String> savedTypes = new InvalidationPreferences(this).getSavedSyncedTypes(); 295 if (savedTypes == null) return Collections.emptySet(); 296 else return ModelType.syncTypesToObjectIds(savedTypes); 297 } 298 299 /** 300 * Reads the saved non-sync object ids from storage (if any) and returns a set containing the 301 * corresponding object ids. 302 */ 303 private Set<ObjectId> readNonSyncRegistrationsFromPrefs() { 304 Set<ObjectId> objectIds = new InvalidationPreferences(this).getSavedObjectIds(); 305 if (objectIds == null) return Collections.emptySet(); 306 else return objectIds; 307 } 308 309 /** 310 * Reads the object registrations from storage (if any) and returns a set containing the 311 * corresponding object ids. 312 */ 313 @VisibleForTesting 314 Set<ObjectId> readRegistrationsFromPrefs() { 315 return joinRegistrations(readSyncRegistrationsFromPrefs(), 316 readNonSyncRegistrationsFromPrefs()); 317 } 318 319 /** 320 * Join Sync object registrations with non-Sync object registrations to get the full set of 321 * desired object registrations. 322 */ 323 private static Set<ObjectId> joinRegistrations(Set<ObjectId> syncRegistrations, 324 Set<ObjectId> nonSyncRegistrations) { 325 if (nonSyncRegistrations.isEmpty()) { 326 return syncRegistrations; 327 } 328 if (syncRegistrations.isEmpty()) { 329 return nonSyncRegistrations; 330 } 331 Set<ObjectId> registrations = new HashSet<ObjectId>( 332 syncRegistrations.size() + nonSyncRegistrations.size()); 333 registrations.addAll(syncRegistrations); 334 registrations.addAll(nonSyncRegistrations); 335 return registrations; 336 } 337 338 /** 339 * Sets the types for which notifications are required to {@code syncTypes}. {@code syncTypes} 340 * is either a list of specific types or the special wildcard type 341 * {@link ModelType#ALL_TYPES_TYPE}. Also registers for additional objects specified by 342 * {@code objectIds}. Either parameter may be null if the corresponding registrations are not 343 * changing. 344 * <p> 345 * @param syncTypes 346 */ 347 private void setRegisteredTypes(Set<String> syncTypes, Set<ObjectId> objectIds) { 348 // If we have a ready client and will be making registration change calls on it, then 349 // read the current registrations from preferences before we write the new values, so that 350 // we can take the diff of the two registration sets and determine which registration change 351 // calls to make. 352 Set<ObjectId> existingSyncRegistrations = (sClientId == null) ? 353 null : readSyncRegistrationsFromPrefs(); 354 Set<ObjectId> existingNonSyncRegistrations = (sClientId == null) ? 355 null : readNonSyncRegistrationsFromPrefs(); 356 357 // Write the new sync types/object ids to preferences. We do not expand the syncTypes to 358 // take into account the ALL_TYPES_TYPE at this point; we want to persist the wildcard 359 // unexpanded. 360 InvalidationPreferences prefs = new InvalidationPreferences(this); 361 EditContext editContext = prefs.edit(); 362 if (syncTypes != null) { 363 prefs.setSyncTypes(editContext, syncTypes); 364 } 365 if (objectIds != null) { 366 prefs.setObjectIds(editContext, objectIds); 367 } 368 prefs.commit(editContext); 369 370 // If we do not have a ready invalidation client, we cannot change its registrations, so 371 // return. Later, when the client is ready, we will supply the new registrations. 372 if (sClientId == null) { 373 return; 374 } 375 376 // We do have a ready client. Unregister any existing registrations not present in the 377 // new set and register any elements in the new set not already present. This call does 378 // expansion of the ALL_TYPES_TYPE wildcard. 379 // NOTE: syncTypes MUST NOT be used below this line, since it contains an unexpanded 380 // wildcard. 381 // When computing the desired set of object ids, if only sync types were provided, then 382 // keep the existing non-sync types, and vice-versa. 383 Set<ObjectId> desiredSyncRegistrations = syncTypes != null ? 384 ModelType.syncTypesToObjectIds(syncTypes) : existingSyncRegistrations; 385 Set<ObjectId> desiredNonSyncRegistrations = objectIds != null ? 386 objectIds : existingNonSyncRegistrations; 387 Set<ObjectId> desiredRegistrations = joinRegistrations(desiredNonSyncRegistrations, 388 desiredSyncRegistrations); 389 Set<ObjectId> existingRegistrations = joinRegistrations(existingNonSyncRegistrations, 390 existingSyncRegistrations); 391 392 Set<ObjectId> unregistrations = new HashSet<ObjectId>(); 393 Set<ObjectId> registrations = new HashSet<ObjectId>(); 394 computeRegistrationOps(existingRegistrations, desiredRegistrations, 395 registrations, unregistrations); 396 unregister(sClientId, unregistrations); 397 register(sClientId, registrations); 398 } 399 400 /** 401 * Computes the set of (un)registrations to perform so that the registrations active in the 402 * Ticl will be {@code desiredRegs}, given that {@existingRegs} already exist. 403 * 404 * @param regAccumulator registrations to perform 405 * @param unregAccumulator unregistrations to perform. 406 */ 407 @VisibleForTesting 408 static void computeRegistrationOps(Set<ObjectId> existingRegs, Set<ObjectId> desiredRegs, 409 Set<ObjectId> regAccumulator, Set<ObjectId> unregAccumulator) { 410 411 // Registrations to do are elements in the new set but not the old set. 412 regAccumulator.addAll(desiredRegs); 413 regAccumulator.removeAll(existingRegs); 414 415 // Unregistrations to do are elements in the old set but not the new set. 416 unregAccumulator.addAll(existingRegs); 417 unregAccumulator.removeAll(desiredRegs); 418 } 419 420 /** 421 * Requests that the sync system perform a sync. 422 * 423 * @param objectId the object that changed, if known. 424 * @param version the version of the object that changed, if known. 425 * @param payload the payload of the change, if known. 426 */ 427 private void requestSync(@Nullable ObjectId objectId, @Nullable Long version, 428 @Nullable String payload) { 429 // Construct the bundle to supply to the native sync code. 430 Bundle bundle = new Bundle(); 431 if (objectId == null && version == null && payload == null) { 432 // Use an empty bundle in this case for compatibility with the v1 implementation. 433 } else { 434 if (objectId != null) { 435 bundle.putInt("objectSource", objectId.getSource()); 436 bundle.putString("objectId", new String(objectId.getName())); 437 } 438 // We use "0" as the version if we have an unknown-version invalidation. This is OK 439 // because the native sync code special-cases zero and always syncs for invalidations at 440 // that version (Tango defines a special UNKNOWN_VERSION constant with this value). 441 bundle.putLong("version", (version == null) ? 0 : version); 442 bundle.putString("payload", (payload == null) ? "" : payload); 443 } 444 Account account = ChromeSigninController.get(this).getSignedInUser(); 445 String contractAuthority = SyncStatusHelper.get(this).getContractAuthority(); 446 requestSyncFromContentResolver(bundle, account, contractAuthority); 447 } 448 449 /** 450 * Calls {@link ContentResolver#requestSync(Account, String, Bundle)} to trigger a sync. Split 451 * into a separate method so that it can be overriden in tests. 452 */ 453 @VisibleForTesting 454 void requestSyncFromContentResolver( 455 Bundle bundle, Account account, String contractAuthority) { 456 Log.d(TAG, "Request sync: " + account + " / " + contractAuthority + " / " 457 + bundle.keySet()); 458 ContentResolver.requestSync(account, contractAuthority, bundle); 459 } 460 461 /** 462 * Returns whether the notification client should be running, i.e., whether Chrome is in the 463 * foreground and sync is enabled. 464 */ 465 @VisibleForTesting 466 boolean shouldClientBeRunning() { 467 return isSyncEnabled() && isChromeInForeground(); 468 } 469 470 /** Returns whether sync is enabled. LLocal method so it can be overridden in tests. */ 471 @VisibleForTesting 472 boolean isSyncEnabled() { 473 return SyncStatusHelper.get(getApplicationContext()).isSyncEnabled(); 474 } 475 476 /** 477 * Returns whether Chrome is in the foreground. Local method so it can be overridden in tests. 478 */ 479 @VisibleForTesting 480 boolean isChromeInForeground() { 481 return ApplicationStatus.hasVisibleActivities(); 482 } 483 484 /** Returns whether the notification client has been started, for tests. */ 485 @VisibleForTesting 486 static boolean getIsClientStartedForTest() { 487 return sIsClientStarted; 488 } 489 490 /** Returns the notification client id, for tests. */ 491 @VisibleForTesting 492 @Nullable static byte[] getClientIdForTest() { 493 return sClientId; 494 } 495 496 private static String getOAuth2ScopeWithType() { 497 return "oauth2:" + SyncStatusHelper.CHROME_SYNC_OAUTH2_SCOPE; 498 } 499 500 private static void setClientId(byte[] clientId) { 501 sClientId = clientId; 502 } 503 504 private static void setIsClientStarted(boolean isStarted) { 505 sIsClientStarted = isStarted; 506 } 507 } 508