- add sources.
[platform/framework/web/crosswalk.git] / src / sync / android / java / src / org / chromium / sync / notifier / InvalidationService.java
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.InvalidationIntentProtocol;
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(InvalidationIntentProtocol.EXTRA_ACCOUNT) ?
86                 (Account) intent.getParcelableExtra(InvalidationIntentProtocol.EXTRA_ACCOUNT)
87                 : null;
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         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     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);
296     }
297
298     /**
299      * Reads the saved non-sync object ids from storage (if any) and returns a set containing the
300      * corresponding object ids.
301      */
302     private Set<ObjectId> readNonSyncRegistrationsFromPrefs() {
303         Set<ObjectId> objectIds = new InvalidationPreferences(this).getSavedObjectIds();
304         if (objectIds == null) return Collections.emptySet();
305         else return objectIds;
306     }
307
308     /**
309      * Reads the object registrations from storage (if any) and returns a set containing the
310      * corresponding object ids.
311      */
312     @VisibleForTesting
313     Set<ObjectId> readRegistrationsFromPrefs() {
314         return joinRegistrations(readSyncRegistrationsFromPrefs(),
315                 readNonSyncRegistrationsFromPrefs());
316     }
317
318     /**
319      * Join Sync object registrations with non-Sync object registrations to get the full set of
320      * desired object registrations.
321      */
322     private static Set<ObjectId> joinRegistrations(Set<ObjectId> syncRegistrations,
323                                                    Set<ObjectId> nonSyncRegistrations) {
324         if (nonSyncRegistrations.isEmpty()) {
325             return syncRegistrations;
326         }
327         if (syncRegistrations.isEmpty()) {
328             return nonSyncRegistrations;
329         }
330         Set<ObjectId> registrations = new HashSet<ObjectId>(
331                 syncRegistrations.size() + nonSyncRegistrations.size());
332         registrations.addAll(syncRegistrations);
333         registrations.addAll(nonSyncRegistrations);
334         return registrations;
335     }
336
337     /**
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
342      * changing.
343      * <p>
344      * @param syncTypes
345      */
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
350         // calls to make.
351         Set<ObjectId> existingSyncRegistrations = (sClientId == null) ?
352                 null : readSyncRegistrationsFromPrefs();
353         Set<ObjectId> existingNonSyncRegistrations = (sClientId == null) ?
354                 null : readNonSyncRegistrationsFromPrefs();
355
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
358         // unexpanded.
359         InvalidationPreferences prefs = new InvalidationPreferences(this);
360         EditContext editContext = prefs.edit();
361         if (syncTypes != null) {
362             prefs.setSyncTypes(editContext, syncTypes);
363         }
364         if (objectIds != null) {
365             prefs.setObjectIds(editContext, objectIds);
366         }
367         prefs.commit(editContext);
368
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) {
372             return;
373         }
374
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
379         // wildcard.
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);
390
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);
397     }
398
399     /**
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.
402      *
403      * @param regAccumulator registrations to perform
404      * @param unregAccumulator unregistrations to perform.
405      */
406     @VisibleForTesting
407     static void computeRegistrationOps(Set<ObjectId> existingRegs, Set<ObjectId> desiredRegs,
408             Set<ObjectId> regAccumulator, Set<ObjectId> unregAccumulator) {
409
410         // Registrations to do are elements in the new set but not the old set.
411         regAccumulator.addAll(desiredRegs);
412         regAccumulator.removeAll(existingRegs);
413
414         // Unregistrations to do are elements in the old set but not the new set.
415         unregAccumulator.addAll(existingRegs);
416         unregAccumulator.removeAll(desiredRegs);
417     }
418
419     /**
420      * Requests that the sync system perform a sync.
421      *
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.
425      */
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.
432         } else {
433             if (objectId != null) {
434                 bundle.putInt("objectSource", objectId.getSource());
435                 bundle.putString("objectId", new String(objectId.getName()));
436             }
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);
442         }
443         Account account = ChromeSigninController.get(this).getSignedInUser();
444         String contractAuthority = SyncStatusHelper.get(this).getContractAuthority();
445         requestSyncFromContentResolver(bundle, account, contractAuthority);
446     }
447
448     /**
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.
451      */
452     @VisibleForTesting
453     void requestSyncFromContentResolver(
454             Bundle bundle, Account account, String contractAuthority) {
455         Log.d(TAG, "Request sync: " + account + " / " + contractAuthority + " / "
456             + bundle.keySet());
457         ContentResolver.requestSync(account, contractAuthority, bundle);
458     }
459
460     /**
461      * Returns whether the notification client should be running, i.e., whether Chrome is in the
462      * foreground and sync is enabled.
463      */
464     @VisibleForTesting
465     boolean shouldClientBeRunning() {
466         return isSyncEnabled() && isChromeInForeground();
467     }
468
469     /** Returns whether sync is enabled. LLocal method so it can be overridden in tests. */
470     @VisibleForTesting
471     boolean isSyncEnabled() {
472         return SyncStatusHelper.get(getApplicationContext()).isSyncEnabled();
473     }
474
475     /**
476      * Returns whether Chrome is in the foreground. Local method so it can be overridden in tests.
477      */
478     @VisibleForTesting
479     boolean isChromeInForeground() {
480         return ActivityStatus.isApplicationVisible();
481     }
482
483     /** Returns whether the notification client has been started, for tests. */
484     @VisibleForTesting
485     static boolean getIsClientStartedForTest() {
486         return sIsClientStarted;
487     }
488
489     /** Returns the notification client id, for tests. */
490     @VisibleForTesting
491     @Nullable static byte[] getClientIdForTest() {
492         return sClientId;
493     }
494
495     private static String getOAuth2ScopeWithType() {
496         return "oauth2:" + SyncStatusHelper.CHROME_SYNC_OAUTH2_SCOPE;
497     }
498
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();
504     }
505
506     private static void setClientId(byte[] clientId) {
507         sClientId = clientId;
508     }
509
510     private static void setIsClientStarted(boolean isStarted) {
511         sIsClientStarted = isStarted;
512     }
513 }