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. 4 5package org.chromium.sync.notifier; 6 7import android.accounts.Account; 8import android.app.PendingIntent; 9import android.content.ContentResolver; 10import android.content.Intent; 11import android.os.Bundle; 12import android.util.Log; 13 14import com.google.common.annotations.VisibleForTesting; 15import com.google.common.collect.Lists; 16import com.google.common.collect.Sets; 17import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState; 18import com.google.ipc.invalidation.external.client.contrib.AndroidListener; 19import com.google.ipc.invalidation.external.client.types.ErrorInfo; 20import com.google.ipc.invalidation.external.client.types.Invalidation; 21import com.google.ipc.invalidation.external.client.types.ObjectId; 22import com.google.protos.ipc.invalidation.Types.ClientType; 23 24import org.chromium.base.ActivityStatus; 25import org.chromium.sync.internal_api.pub.base.ModelType; 26import org.chromium.sync.notifier.InvalidationController.IntentProtocol; 27import org.chromium.sync.notifier.InvalidationPreferences.EditContext; 28import org.chromium.sync.signin.AccountManagerHelper; 29import org.chromium.sync.signin.ChromeSigninController; 30 31import java.util.Collection; 32import java.util.Collections; 33import java.util.List; 34import java.util.Random; 35import java.util.Set; 36 37import javax.annotation.Nullable; 38 39/** 40 * Service that controls notifications for sync. 41 * <p> 42 * This service serves two roles. On the one hand, it is a client for the notification system 43 * used to trigger sync. It receives invalidations and converts them into 44 * {@link ContentResolver#requestSync} calls, and it supplies the notification system with the set 45 * of desired registrations when requested. 46 * <p> 47 * On the other hand, this class is controller for the notification system. It starts it and stops 48 * it, and it requests that it perform (un)registrations as the set of desired sync types changes. 49 * <p> 50 * This class is an {@code IntentService}. All methods are assumed to be executing on its single 51 * execution thread. 52 * 53 * @author dsmyers@google.com 54 */ 55public class InvalidationService extends AndroidListener { 56 /* This class must be public because it is exposed as a service. */ 57 58 /** Notification client typecode. */ 59 @VisibleForTesting 60 static final int CLIENT_TYPE = ClientType.Type.CHROME_SYNC_ANDROID_VALUE; 61 62 private static final String TAG = "InvalidationService"; 63 64 private static final Random RANDOM = new Random(); 65 66 /** 67 * Whether the underlying notification client has been started. This boolean is updated when a 68 * start or stop intent is issued to the underlying client, not when the intent is actually 69 * processed. 70 */ 71 private static boolean sIsClientStarted; 72 73 /** 74 * The id of the client in use, if any. May be {@code null} if {@link #sIsClientStarted} is 75 * true if the client has not yet gone ready. 76 */ 77 @Nullable private static byte[] sClientId; 78 79 @Override 80 public void onHandleIntent(Intent intent) { 81 // Ensure that a client is or is not running, as appropriate, and that it is for the 82 // correct account. ensureAccount will stop the client if account is non-null and doesn't 83 // match the stored account. Then, if a client should be running, ensureClientStartState 84 // will start a new one if needed. I.e., these two functions work together to restart the 85 // client when the account changes. 86 Account account = intent.hasExtra(IntentProtocol.EXTRA_ACCOUNT) ? 87 (Account) intent.getParcelableExtra(IntentProtocol.EXTRA_ACCOUNT) : null; 88 ensureAccount(account); 89 ensureClientStartState(); 90 91 // Handle the intent. 92 if (IntentProtocol.isStop(intent) && sIsClientStarted) { 93 // If the intent requests that the client be stopped, stop it. 94 stopClient(); 95 } else if (IntentProtocol.isRegisteredTypesChange(intent)) { 96 // If the intent requests a change in registrations, change them. 97 List<String> regTypes = 98 intent.getStringArrayListExtra(IntentProtocol.EXTRA_REGISTERED_TYPES); 99 setRegisteredTypes(Sets.newHashSet(regTypes)); 100 } else { 101 // Otherwise, we don't recognize the intent. Pass it to the notification client service. 102 super.onHandleIntent(intent); 103 } 104 } 105 106 @Override 107 public void invalidate(Invalidation invalidation, byte[] ackHandle) { 108 byte[] payload = invalidation.getPayload(); 109 String payloadStr = (payload == null) ? null : new String(payload); 110 requestSync(invalidation.getObjectId(), invalidation.getVersion(), payloadStr); 111 acknowledge(ackHandle); 112 } 113 114 @Override 115 public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) { 116 requestSync(objectId, null, null); 117 acknowledge(ackHandle); 118 } 119 120 @Override 121 public void invalidateAll(byte[] ackHandle) { 122 requestSync(null, null, null); 123 acknowledge(ackHandle); 124 } 125 126 @Override 127 public void informRegistrationFailure( 128 byte[] clientId, ObjectId objectId, boolean isTransient, String errorMessage) { 129 Log.w(TAG, "Registration failure on " + objectId + " ; transient = " + isTransient 130 + ": " + errorMessage); 131 if (isTransient) { 132 // Retry immediately on transient failures. The base AndroidListener will handle 133 // exponential backoff if there are repeated failures. 134 List<ObjectId> objectIdAsList = Lists.newArrayList(objectId); 135 if (readRegistrationsFromPrefs().contains(objectId)) { 136 register(clientId, objectIdAsList); 137 } else { 138 unregister(clientId, objectIdAsList); 139 } 140 } 141 } 142 143 @Override 144 public void informRegistrationStatus( 145 byte[] clientId, ObjectId objectId, RegistrationState regState) { 146 Log.d(TAG, "Registration status for " + objectId + ": " + regState); 147 List<ObjectId> objectIdAsList = Lists.newArrayList(objectId); 148 boolean registrationisDesired = readRegistrationsFromPrefs().contains(objectId); 149 if (regState == RegistrationState.REGISTERED) { 150 if (!registrationisDesired) { 151 Log.i(TAG, "Unregistering for object we're no longer interested in"); 152 unregister(clientId, objectIdAsList); 153 } 154 } else { 155 if (registrationisDesired) { 156 Log.i(TAG, "Registering for an object"); 157 register(clientId, objectIdAsList); 158 } 159 } 160 } 161 162 @Override 163 public void informError(ErrorInfo errorInfo) { 164 Log.w(TAG, "Invalidation client error:" + errorInfo); 165 if (!errorInfo.isTransient() && sIsClientStarted) { 166 // It is important not to stop the client if it is already stopped. Otherwise, the 167 // possibility exists to go into an infinite loop if the stop call itself triggers an 168 // error (e.g., because no client actually exists). 169 stopClient(); 170 } 171 } 172 173 @Override 174 public void ready(byte[] clientId) { 175 setClientId(clientId); 176 177 // We might have accumulated some registrations to do while we were waiting for the client 178 // to become ready. 179 reissueRegistrations(clientId); 180 } 181 182 @Override 183 public void reissueRegistrations(byte[] clientId) { 184 Set<ObjectId> desiredRegistrations = readRegistrationsFromPrefs(); 185 if (!desiredRegistrations.isEmpty()) { 186 register(clientId, desiredRegistrations); 187 } 188 } 189 190 @Override 191 public void requestAuthToken(final PendingIntent pendingIntent, 192 @Nullable String invalidAuthToken) { 193 @Nullable Account account = ChromeSigninController.get(this).getSignedInUser(); 194 if (account == null) { 195 // This should never happen, because this code should only be run if a user is 196 // signed-in. 197 Log.w(TAG, "No signed-in user; cannot send message to data center"); 198 return; 199 } 200 201 // Attempt to retrieve a token for the user. This method will also invalidate 202 // invalidAuthToken if it is non-null. 203 AccountManagerHelper.get(this).getNewAuthTokenFromForeground( 204 account, invalidAuthToken, SyncStatusHelper.AUTH_TOKEN_TYPE_SYNC, 205 new AccountManagerHelper.GetAuthTokenCallback() { 206 @Override 207 public void tokenAvailable(String token) { 208 if (token != null) { 209 InvalidationService.setAuthToken( 210 InvalidationService.this.getApplicationContext(), pendingIntent, 211 token, SyncStatusHelper.AUTH_TOKEN_TYPE_SYNC); 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 Intent startIntent = AndroidListener.createStartIntent(this, CLIENT_TYPE, getClientName()); 269 startService(startIntent); 270 setIsClientStarted(true); 271 } 272 273 /** Stops the notification client. */ 274 private void stopClient() { 275 startService(AndroidListener.createStopIntent(this)); 276 setIsClientStarted(false); 277 setClientId(null); 278 } 279 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); 286 } 287 288 /** 289 * Reads the saved sync types from storage (if any) and returns a set containing the 290 * corresponding object ids. 291 */ 292 @VisibleForTesting 293 Set<ObjectId> readRegistrationsFromPrefs() { 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 * Sets the types for which notifications are required to {@code syncTypes}. {@code syncTypes} 301 * is either a list of specific types or the special wildcard type 302 * {@link ModelType#ALL_TYPES_TYPE}. 303 * <p> 304 * @param syncTypes 305 */ 306 private void setRegisteredTypes(Set<String> syncTypes) { 307 // If we have a ready client and will be making registration change calls on it, then 308 // read the current registrations from preferences before we write the new values, so that 309 // we can take the diff of the two registration sets and determine which registration change 310 // calls to make. 311 Set<ObjectId> existingRegistrations = (sClientId == null) ? 312 null : readRegistrationsFromPrefs(); 313 314 // Write the new sync types to preferences. We do not expand the syncTypes to take into 315 // account the ALL_TYPES_TYPE at this point; we want to persist the wildcard unexpanded. 316 InvalidationPreferences prefs = new InvalidationPreferences(this); 317 EditContext editContext = prefs.edit(); 318 prefs.setSyncTypes(editContext, syncTypes); 319 prefs.commit(editContext); 320 321 // If we do not have a ready invalidation client, we cannot change its registrations, so 322 // return. Later, when the client is ready, we will supply the new registrations. 323 if (sClientId == null) { 324 return; 325 } 326 327 // We do have a ready client. Unregister any existing registrations not present in the 328 // new set and register any elements in the new set not already present. This call does 329 // expansion of the ALL_TYPES_TYPE wildcard. 330 // NOTE: syncTypes MUST NOT be used below this line, since it contains an unexpanded 331 // wildcard. 332 List<ObjectId> unregistrations = Lists.newArrayList(); 333 List<ObjectId> registrations = Lists.newArrayList(); 334 computeRegistrationOps(existingRegistrations, 335 ModelType.syncTypesToObjectIds(syncTypes), 336 registrations, unregistrations); 337 unregister(sClientId, unregistrations); 338 register(sClientId, registrations); 339 } 340 341 /** 342 * Computes the set of (un)registrations to perform so that the registrations active in the 343 * Ticl will be {@code desiredRegs}, given that {@existingRegs} already exist. 344 * 345 * @param regAccumulator registrations to perform 346 * @param unregAccumulator unregistrations to perform. 347 */ 348 @VisibleForTesting 349 static void computeRegistrationOps(Set<ObjectId> existingRegs, Set<ObjectId> desiredRegs, 350 Collection<ObjectId> regAccumulator, Collection<ObjectId> unregAccumulator) { 351 // Registrations to do are elements in the new set but not the old set. 352 regAccumulator.addAll(Sets.difference(desiredRegs, existingRegs)); 353 354 // Unregistrations to do are elements in the old set but not the new set. 355 unregAccumulator.addAll(Sets.difference(existingRegs, desiredRegs)); 356 } 357 358 /** 359 * Requests that the sync system perform a sync. 360 * 361 * @param objectId the object that changed, if known. 362 * @param version the version of the object that changed, if known. 363 * @param payload the payload of the change, if known. 364 */ 365 private void requestSync(@Nullable ObjectId objectId, @Nullable Long version, 366 @Nullable String payload) { 367 // Construct the bundle to supply to the native sync code. 368 Bundle bundle = new Bundle(); 369 if (objectId == null && version == null && payload == null) { 370 // Use an empty bundle in this case for compatibility with the v1 implementation. 371 } else { 372 if (objectId != null) { 373 bundle.putString("objectId", new String(objectId.getName())); 374 } 375 // We use "0" as the version if we have an unknown-version invalidation. This is OK 376 // because the native sync code special-cases zero and always syncs for invalidations at 377 // that version (Tango defines a special UNKNOWN_VERSION constant with this value). 378 bundle.putLong("version", (version == null) ? 0 : version); 379 bundle.putString("payload", (payload == null) ? "" : payload); 380 } 381 Account account = ChromeSigninController.get(this).getSignedInUser(); 382 String contractAuthority = InvalidationController.get(this).getContractAuthority(); 383 requestSyncFromContentResolver(bundle, account, contractAuthority); 384 } 385 386 /** 387 * Calls {@link ContentResolver#requestSync(Account, String, Bundle)} to trigger a sync. Split 388 * into a separate method so that it can be overriden in tests. 389 */ 390 @VisibleForTesting 391 void requestSyncFromContentResolver( 392 Bundle bundle, Account account, String contractAuthority) { 393 Log.d(TAG, "Request sync: " + account + " / " + contractAuthority + " / " 394 + bundle.keySet()); 395 ContentResolver.requestSync(account, contractAuthority, bundle); 396 } 397 398 /** 399 * Returns whether the notification client should be running, i.e., whether Chrome is in the 400 * foreground and sync is enabled. 401 */ 402 @VisibleForTesting 403 boolean shouldClientBeRunning() { 404 return isSyncEnabled() && isChromeInForeground(); 405 } 406 407 /** Returns whether sync is enabled. LLocal method so it can be overridden in tests. */ 408 @VisibleForTesting 409 boolean isSyncEnabled() { 410 return SyncStatusHelper.get(getApplicationContext()).isSyncEnabled(); 411 } 412 413 /** 414 * Returns whether Chrome is in the foreground. Local method so it can be overridden in tests. 415 */ 416 @VisibleForTesting 417 boolean isChromeInForeground() { 418 switch (ActivityStatus.getState()) { 419 case ActivityStatus.CREATED: 420 case ActivityStatus.STARTED: 421 case ActivityStatus.RESUMED: 422 return true; 423 default: 424 return false; 425 } 426 } 427 428 /** Returns whether the notification client has been started, for tests. */ 429 @VisibleForTesting 430 static boolean getIsClientStartedForTest() { 431 return sIsClientStarted; 432 } 433 434 /** Returns the notification client id, for tests. */ 435 @VisibleForTesting 436 @Nullable static byte[] getClientIdForTest() { 437 return sClientId; 438 } 439 440 /** Returns the client name used for the notification client. */ 441 private static byte[] getClientName() { 442 // TODO(dsmyers): we should use the same client name as the native sync code. 443 // Bug: https://code.google.com/p/chromium/issues/detail?id=172391 444 return Long.toString(RANDOM.nextLong()).getBytes(); 445 } 446 447 private static void setClientId(byte[] clientId) { 448 sClientId = clientId; 449 } 450 451 private static void setIsClientStarted(boolean isStarted) { 452 sIsClientStarted = isStarted; 453 } 454} 455