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