EMMA Coverage Report (generated Fri Aug 23 16:39:17 PDT 2013)
[all classes][org.chromium.sync.notifier]

COVERAGE SUMMARY FOR SOURCE FILE [InvalidationService.java]

nameclass, %method, %block, %line, %
InvalidationService.java50%  (1/2)85%  (29/34)90%  (506/560)90%  (128/142)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class InvalidationService$10%   (0/1)0%   (0/2)0%   (0/20)0%   (0/4)
InvalidationService$1 (InvalidationService, PendingIntent): void 0%   (0/1)0%   (0/9)0%   (0/1)
tokenAvailable (String): void 0%   (0/1)0%   (0/11)0%   (0/3)
     
class InvalidationService100% (1/1)91%  (29/32)94%  (506/540)93%  (128/138)
isChromeInForeground (): boolean 0%   (0/1)0%   (0/6)0%   (0/3)
isSyncEnabled (): boolean 0%   (0/1)0%   (0/5)0%   (0/1)
requestAuthToken (PendingIntent, String): void 0%   (0/1)0%   (0/23)0%   (0/6)
<static initializer> 100% (1/1)100% (5/5)100% (1/1)
InvalidationService (): void 100% (1/1)100% (3/3)100% (1/1)
computeRegistrationOps (Set, Set, Set, Set): void 100% (1/1)100% (17/17)100% (5/5)
ensureAccount (Account): void 100% (1/1)100% (21/21)100% (8/8)
ensureClientStartState (): void 100% (1/1)100% (17/17)100% (6/6)
getClientIdForTest (): byte [] 100% (1/1)100% (2/2)100% (1/1)
getClientName (): byte [] 100% (1/1)100% (5/5)100% (1/1)
getIsClientStartedForTest (): boolean 100% (1/1)100% (2/2)100% (1/1)
informError (ErrorInfo): void 100% (1/1)100% (19/19)100% (4/4)
informRegistrationFailure (byte [], ObjectId, boolean, String): void 100% (1/1)100% (44/44)100% (7/7)
informRegistrationStatus (byte [], ObjectId, InvalidationListener$Registratio... 100% (1/1)100% (53/53)100% (11/11)
invalidate (Invalidation, byte []): void 100% (1/1)100% (24/24)100% (5/5)
invalidateAll (byte []): void 100% (1/1)100% (9/9)100% (3/3)
invalidateUnknownVersion (ObjectId, byte []): void 100% (1/1)100% (9/9)100% (3/3)
onHandleIntent (Intent): void 100% (1/1)100% (42/42)100% (11/11)
readRegistrationsFromPrefs (): Set 100% (1/1)100% (13/13)100% (3/3)
readState (): byte [] 100% (1/1)100% (6/6)100% (1/1)
ready (byte []): void 100% (1/1)100% (6/6)100% (3/3)
reissueRegistrations (byte []): void 100% (1/1)100% (11/11)100% (4/4)
requestSync (ObjectId, Long, String): void 100% (1/1)100% (52/52)100% (10/10)
requestSyncFromContentResolver (Bundle, Account, String): void 100% (1/1)100% (25/25)100% (3/3)
setAccount (Account): void 100% (1/1)100% (17/17)100% (5/5)
setClientId (byte []): void 100% (1/1)100% (3/3)100% (2/2)
setIsClientStarted (boolean): void 100% (1/1)100% (3/3)100% (2/2)
setRegisteredTypes (Set): void 100% (1/1)100% (49/49)100% (13/13)
shouldClientBeRunning (): boolean 100% (1/1)100% (10/10)100% (1/1)
startClient (): void 100% (1/1)100% (12/12)100% (4/4)
stopClient (): void 100% (1/1)100% (10/10)100% (4/4)
writeState (byte []): void 100% (1/1)100% (17/17)100% (5/5)

1// Copyright (c) 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4 
5package org.chromium.sync.notifier;
6 
7import android.accounts.Account;
8import android.app.PendingIntent;
9import android.content.ContentResolver;
10import android.content.Intent;
11import android.os.Bundle;
12import android.util.Log;
13 
14import com.google.common.annotations.VisibleForTesting;
15import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
16import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
17import com.google.ipc.invalidation.external.client.types.ErrorInfo;
18import com.google.ipc.invalidation.external.client.types.Invalidation;
19import com.google.ipc.invalidation.external.client.types.ObjectId;
20import com.google.protos.ipc.invalidation.Types.ClientType;
21 
22import org.chromium.base.ActivityStatus;
23import org.chromium.base.CollectionUtil;
24import org.chromium.sync.internal_api.pub.base.ModelType;
25import org.chromium.sync.notifier.InvalidationController.IntentProtocol;
26import org.chromium.sync.notifier.InvalidationPreferences.EditContext;
27import org.chromium.sync.signin.AccountManagerHelper;
28import org.chromium.sync.signin.ChromeSigninController;
29 
30import java.util.Collections;
31import java.util.HashSet;
32import java.util.List;
33import java.util.Random;
34import java.util.Set;
35 
36import 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 */
54public 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}

[all classes][org.chromium.sync.notifier]
EMMA 2.0.5312 (C) Vladimir Roubtsov