Home | History | Annotate | Download | only in notifier
      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