Upstream version 6.35.121.0
[platform/framework/web/crosswalk.git] / src / sync / android / java / src / org / chromium / sync / notifier / InvalidationService.java
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@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 }