2 * Copyright 2011 Google Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.google.ipc.invalidation.external.client.contrib;
18 import com.google.ipc.invalidation.external.client.InvalidationClient;
19 import com.google.ipc.invalidation.external.client.InvalidationClientConfig;
20 import com.google.ipc.invalidation.external.client.InvalidationListener;
21 import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
22 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
23 import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
24 import com.google.ipc.invalidation.external.client.types.AckHandle;
25 import com.google.ipc.invalidation.external.client.types.ErrorInfo;
26 import com.google.ipc.invalidation.external.client.types.Invalidation;
27 import com.google.ipc.invalidation.external.client.types.ObjectId;
28 import com.google.ipc.invalidation.ticl.InvalidationClientCore;
29 import com.google.ipc.invalidation.ticl.ProtoWrapperConverter;
30 import com.google.ipc.invalidation.ticl.android2.AndroidClock;
31 import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerIntentMapper;
32 import com.google.ipc.invalidation.ticl.android2.ProtocolIntents;
33 import com.google.ipc.invalidation.ticl.android2.channel.AndroidChannelConstants.AuthTokenConstants;
34 import com.google.ipc.invalidation.ticl.proto.AndroidListenerProtocol;
35 import com.google.ipc.invalidation.ticl.proto.AndroidListenerProtocol.RegistrationCommand;
36 import com.google.ipc.invalidation.ticl.proto.AndroidListenerProtocol.StartCommand;
37 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ClientConfigP;
38 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.InvalidationMessage;
39 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.InvalidationP;
40 import com.google.ipc.invalidation.ticl.proto.ClientProtocol.ObjectIdP;
41 import com.google.ipc.invalidation.util.Bytes;
42 import com.google.ipc.invalidation.util.Preconditions;
43 import com.google.ipc.invalidation.util.ProtoWrapper.ValidationException;
45 import android.app.IntentService;
46 import android.app.PendingIntent;
47 import android.content.BroadcastReceiver;
48 import android.content.Context;
49 import android.content.Intent;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.concurrent.TimeUnit;
57 * Simplified listener contract for Android clients. Takes care of exponential back-off when
58 * register or unregister are called for an object after a failure has occurred. Also suppresses
59 * redundant register requests.
61 * <p>A sample implementation of an {@link AndroidListener} is shown below:
64 * class ExampleListener extends AndroidListener {
66 * public void reissueRegistrations(byte[] clientId) {
67 * List<ObjectId> desiredRegistrations = ...;
68 * register(clientId, desiredRegistrations);
72 * public void invalidate(Invalidation invalidation, final byte[] ackHandle) {
73 * // Track the most recent version of the object (application-specific) and then acknowledge
74 * // the invalidation.
76 * acknowledge(ackHandle);
80 * public void informRegistrationFailure(byte[] clientId, ObjectId objectId,
81 * boolean isTransient, String errorMessage) {
82 * // Try again if there is a transient failure and we still care whether the object is
83 * // registered or not.
85 * boolean shouldRetry = ...;
87 * boolean shouldBeRegistered = ...;
88 * if (shouldBeRegistered) {
89 * register(clientId, ImmutableList.of(objectId));
91 * unregister(clientId, ImmutableList.of(objectId));
101 * <p>See {@link com.google.ipc.invalidation.examples.android2} for a complete sample.
104 public abstract class AndroidListener extends IntentService {
106 /** External alarm receiver that allows the listener to respond to alarm intents. */
107 public static final class AlarmReceiver extends BroadcastReceiver {
109 public void onReceive(Context context, Intent intent) {
110 Preconditions.checkNotNull(context);
111 Preconditions.checkNotNull(intent);
112 if (intent.hasExtra(AndroidListenerIntents.EXTRA_REGISTRATION)) {
113 AndroidListenerIntents.issueAndroidListenerIntent(context, intent);
119 private static final Logger logger = AndroidLogger.forPrefix("");
121 /** Initial retry delay for exponential backoff (1 minute). */
123 static int initialMaxDelayMs = (int) TimeUnit.SECONDS.toMillis(60);
125 /** Maximum delay factor for exponential backoff (6 hours). */
127 static int maxDelayFactor = 6 * 60;
129 /** The last client ID passed to the ready up-call. */
131 static Bytes lastClientIdForTest;
134 * Invalidation listener implementation. We implement the interface on a private field rather
135 * than directly to avoid leaking methods that should not be directly called by the client
136 * application. The listener must be called only on intent service thread.
138 private final InvalidationListener invalidationListener = new InvalidationListener() {
140 public final void ready(final InvalidationClient client) {
141 Bytes clientId = state.getClientId();
142 AndroidListener.lastClientIdForTest = clientId;
143 AndroidListener.this.ready(clientId.getByteArray());
147 public final void reissueRegistrations(final InvalidationClient client, byte[] prefix,
149 AndroidListener.this.reissueRegistrations(state.getClientId().getByteArray());
153 public final void informRegistrationStatus(final InvalidationClient client,
154 final ObjectId objectId, final RegistrationState regState) {
155 state.informRegistrationSuccess(objectId);
156 AndroidListener.this.informRegistrationStatus(state.getClientId().getByteArray(), objectId,
161 public final void informRegistrationFailure(final InvalidationClient client,
162 final ObjectId objectId, final boolean isTransient, final String errorMessage) {
163 state.informRegistrationFailure(objectId, isTransient);
164 AndroidListener.this.informRegistrationFailure(state.getClientId().getByteArray(), objectId,
165 isTransient, errorMessage);
169 public void invalidate(InvalidationClient client, Invalidation invalidation,
170 AckHandle ackHandle) {
171 AndroidListener.this.invalidate(invalidation, ackHandle.getHandleData());
175 public void invalidateUnknownVersion(InvalidationClient client, ObjectId objectId,
176 AckHandle ackHandle) {
177 AndroidListener.this.invalidateUnknownVersion(objectId, ackHandle.getHandleData());
181 public void invalidateAll(InvalidationClient client, AckHandle ackHandle) {
182 AndroidListener.this.invalidateAll(ackHandle.getHandleData());
186 public void informError(InvalidationClient client, ErrorInfo errorInfo) {
187 AndroidListener.this.informError(errorInfo);
192 * The internal state of the listener. Lazy initialization, triggered by {@link #onHandleIntent}.
194 private AndroidListenerState state;
196 /** The clock to use when scheduling retry call-backs. */
197 private final AndroidClock clock = new AndroidClock.SystemClock();
200 * The mapper used to route intents to the invalidation listener. Lazy initialization triggered
201 * by {@link #onCreate}.
203 private AndroidInvalidationListenerIntentMapper intentMapper;
205 /** Initializes {@link AndroidListener}. */
206 protected AndroidListener() {
209 // If the process dies before an intent is handled, setIntentRedelivery(true) ensures that the
210 // last intent is redelivered. This optimization is not necessary for correctness: on restart,
211 // all registrations will be reissued and unacked invalidations will be resent anyways.
212 setIntentRedelivery(true);
215 /** See specs for {@link InvalidationClient#start}. */
216 public static Intent createStartIntent(Context context, InvalidationClientConfig config) {
217 Preconditions.checkNotNull(context);
218 Preconditions.checkNotNull(config);
219 Preconditions.checkNotNull(config.clientName);
221 return AndroidListenerIntents.createStartIntent(context, config.clientType,
222 Bytes.fromByteArray(config.clientName), config.allowSuppression);
225 /** See specs for {@link InvalidationClient#start}. */
226 public static Intent createStartIntent(Context context, int clientType, byte[] clientName) {
227 Preconditions.checkNotNull(context);
228 Preconditions.checkNotNull(clientName);
230 final boolean allowSuppression = true;
231 return AndroidListenerIntents.createStartIntent(context, clientType,
232 Bytes.fromByteArray(clientName), allowSuppression);
235 /** See specs for {@link InvalidationClient#stop}. */
236 public static Intent createStopIntent(Context context) {
237 Preconditions.checkNotNull(context);
239 return AndroidListenerIntents.createStopIntent(context);
243 * See specs for {@link InvalidationClient#register}.
245 * @param context the context
246 * @param clientId identifier for the client service for which we are registering
247 * @param objectIds the object ids being registered
249 public static Intent createRegisterIntent(Context context, byte[] clientId,
250 Iterable<ObjectId> objectIds) {
251 Preconditions.checkNotNull(context);
252 Preconditions.checkNotNull(clientId);
253 Preconditions.checkNotNull(objectIds);
255 final boolean isRegister = true;
256 return AndroidListenerIntents.createRegistrationIntent(context, Bytes.fromByteArray(clientId),
257 objectIds, isRegister);
261 * See specs for {@link InvalidationClient#register}.
263 * @param clientId identifier for the client service for which we are registering
264 * @param objectIds the object ids being registered
266 public void register(byte[] clientId, Iterable<ObjectId> objectIds) {
267 Preconditions.checkNotNull(clientId);
268 Preconditions.checkNotNull(objectIds);
270 Context context = getApplicationContext();
271 context.startService(createRegisterIntent(context, clientId, objectIds));
275 * See specs for {@link InvalidationClient#unregister}.
277 * @param context the context
278 * @param clientId identifier for the client service for which we are unregistering
279 * @param objectIds the object ids being unregistered
281 public static Intent createUnregisterIntent(Context context, byte[] clientId,
282 Iterable<ObjectId> objectIds) {
283 Preconditions.checkNotNull(context);
284 Preconditions.checkNotNull(clientId);
285 Preconditions.checkNotNull(objectIds);
287 final boolean isRegister = false;
288 return AndroidListenerIntents.createRegistrationIntent(context, Bytes.fromByteArray(clientId),
289 objectIds, isRegister);
293 * Sets the authorization token and type used by the invalidation client. Call in response to
294 * {@link #requestAuthToken} calls.
296 * @param pendingIntent pending intent passed to {@link #requestAuthToken}
297 * @param authToken authorization token
298 * @param authType authorization token typo
300 public static void setAuthToken(Context context, PendingIntent pendingIntent, String authToken,
302 Preconditions.checkNotNull(pendingIntent);
303 Preconditions.checkNotNull(authToken);
304 Preconditions.checkNotNull(authType);
306 AndroidListenerIntents.issueAuthTokenResponse(context, pendingIntent, authToken, authType);
310 * See specs for {@link InvalidationClient#unregister}.
312 * @param clientId identifier for the client service for which we are registering
313 * @param objectIds the object ids being unregistered
315 public void unregister(byte[] clientId, Iterable<ObjectId> objectIds) {
316 Preconditions.checkNotNull(clientId);
317 Preconditions.checkNotNull(objectIds);
319 Context context = getApplicationContext();
320 context.startService(createUnregisterIntent(context, clientId, objectIds));
323 /** See specs for {@link InvalidationClient#acknowledge}. */
324 public static Intent createAcknowledgeIntent(Context context, byte[] ackHandle) {
325 Preconditions.checkNotNull(context);
326 Preconditions.checkNotNull(ackHandle);
328 return AndroidListenerIntents.createAckIntent(context, ackHandle);
331 /** See specs for {@link InvalidationClient#acknowledge}. */
332 public void acknowledge(byte[] ackHandle) {
333 Preconditions.checkNotNull(ackHandle);
335 Context context = getApplicationContext();
336 context.startService(createAcknowledgeIntent(context, ackHandle));
340 * See specs for {@link InvalidationListener#ready}.
342 * @param clientId the client identifier that must be passed to {@link #createRegisterIntent}
343 * and {@link #createUnregisterIntent}
345 public abstract void ready(byte[] clientId);
348 * See specs for {@link InvalidationListener#reissueRegistrations}.
350 * @param clientId the client identifier that must be passed to {@link #createRegisterIntent}
351 * and {@link #createUnregisterIntent}
353 public abstract void reissueRegistrations(byte[] clientId);
356 * See specs for {@link InvalidationListener#informError}.
358 public abstract void informError(ErrorInfo errorInfo);
361 * See specs for {@link InvalidationListener#invalidate}.
363 * @param invalidation the invalidation
364 * @param ackHandle event acknowledgment handle
366 public abstract void invalidate(Invalidation invalidation, byte[] ackHandle);
369 * See specs for {@link InvalidationListener#invalidateUnknownVersion}.
371 * @param objectId identifier for the object with unknown version
372 * @param ackHandle event acknowledgment handle
374 public abstract void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle);
377 * See specs for {@link InvalidationListener#invalidateAll}.
379 * @param ackHandle event acknowledgment handle
381 public abstract void invalidateAll(byte[] ackHandle);
384 * Read listener state.
386 * @return serialized state or {@code null} if it is not available
388 public abstract byte[] readState();
390 /** Write listener state to some location. */
391 public abstract void writeState(byte[] data);
394 * See specs for {@link InvalidationListener#informRegistrationFailure}.
396 public abstract void informRegistrationFailure(byte[] clientId, ObjectId objectId,
397 boolean isTransient, String errorMessage);
400 * See specs for (@link InvalidationListener#informRegistrationStatus}.
402 public abstract void informRegistrationStatus(byte[] clientId, ObjectId objectId,
403 RegistrationState regState);
406 * Called when an authorization token is needed. Respond by calling {@link #setAuthToken}.
408 * @param pendingIntent pending intent that must be used in {@link #setAuthToken} response.
409 * @param invalidAuthToken the existing invalid token or null if none exists. Implementation
410 * should invalidate the token.
412 public abstract void requestAuthToken(PendingIntent pendingIntent,
413 String invalidAuthToken);
416 * Handles invalidations received while the client is stopped. An implementation may choose to
417 * do work in response to these invalidations (delivered best-effort by the invalidation system).
418 * Not intended for use by most client implementations.
420 protected void backgroundInvalidateForInternalUse(
421 @SuppressWarnings("unused") Iterable<Invalidation> invalidations) {
422 // Ignore background invalidations by default.
426 public void onCreate() {
429 // Initialize the intent mapper (now that context is available).
430 intentMapper = new AndroidInvalidationListenerIntentMapper(invalidationListener, this);
434 * Derived classes may override this method to handle custom intents. This is a recommended
435 * pattern for invalidation-related intents, e.g. for registration and unregistration. Derived
436 * classes should call {@code super.onHandleIntent(intent)} for any intents they did not
437 * handle on their own.
440 protected void onHandleIntent(Intent intent) {
441 if (intent == null) {
445 // We lazily initialize state in calls to onHandleIntent rather than initializing in onCreate
446 // because onCreate runs on the UI thread and initializeState performs I/O.
451 // Handle any intents specific to the AndroidListener. For other intents, defer to the
452 // intentMapper, which handles listener upcalls corresponding to the InvalidationListener
454 if (!tryHandleAuthTokenRequestIntent(intent) &&
455 !tryHandleRegistrationIntent(intent) &&
456 !tryHandleStartIntent(intent) &&
457 !tryHandleStopIntent(intent) &&
458 !tryHandleAckIntent(intent) &&
459 !tryHandleBackgroundInvalidationsIntent(intent)) {
460 intentMapper.handleIntent(intent);
463 // Always check to see if we need to persist state changes after handling an intent.
464 if (state.getIsDirty()) {
465 writeState(state.marshal().toByteArray());
466 state.resetIsDirty();
470 /** Returns invalidation client that can be used to trigger intents against the TICL service. */
471 private InvalidationClient getClient() {
472 return intentMapper.client;
476 * Initializes listener state either from persistent proto (if available) or from scratch.
478 private void initializeState() {
479 AndroidListenerProtocol.AndroidListenerState proto = getPersistentState();
481 state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor, proto);
483 state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor);
488 * Reads and parses persistent state for the listener. Returns {@code null} if the state does not
489 * exist or is invalid.
491 private AndroidListenerProtocol.AndroidListenerState getPersistentState() {
492 // Defer to application code to read the blob containing the state proto.
493 byte[] stateData = readState();
495 if (null != stateData) {
496 AndroidListenerProtocol.AndroidListenerState state =
497 AndroidListenerProtocol.AndroidListenerState.parseFrom(stateData);
498 if (!AndroidListenerProtos.isValidAndroidListenerState(state)) {
499 logger.warning("Invalid listener state.");
504 } catch (ValidationException exception) {
505 logger.warning("Failed to parse listener state: %s", exception);
511 * Tries to handle a request for an authorization token. Returns {@code true} iff the intent is
512 * an auth token request.
514 private boolean tryHandleAuthTokenRequestIntent(Intent intent) {
515 if (!AndroidListenerIntents.isAuthTokenRequest(intent)) {
519 // Check for invalid auth token. Subclass may have to invalidate it if it exists in the call
520 // to getNewAuthToken.
521 String invalidAuthToken = intent.getStringExtra(
522 AuthTokenConstants.EXTRA_INVALIDATE_AUTH_TOKEN);
523 // Intent also includes a pending intent that we can use to pass back our response.
524 PendingIntent pendingIntent = intent.getParcelableExtra(
525 AuthTokenConstants.EXTRA_PENDING_INTENT);
526 if (pendingIntent == null) {
527 logger.warning("Authorization request without pending intent extra.");
529 // Delegate to client application to figure out what the new token should be and the auth
531 requestAuthToken(pendingIntent, invalidAuthToken);
536 /** Tries to handle a stop intent. Returns {@code true} iff the intent is a stop intent. */
537 private boolean tryHandleStopIntent(Intent intent) {
538 if (!AndroidListenerIntents.isStopIntent(intent)) {
546 * Tries to handle a registration intent. Returns {@code true} iff the intent is a registration
549 private boolean tryHandleRegistrationIntent(Intent intent) {
550 RegistrationCommand command = AndroidListenerIntents.findRegistrationCommand(intent);
551 if ((command == null) || !AndroidListenerProtos.isValidRegistrationCommand(command)) {
554 // Make sure the registration is intended for this client. If not, we ignore it (suggests
555 // there is a new client now).
556 if (!command.getClientId().equals(state.getClientId())) {
557 logger.warning("Ignoring registration request for old client. Old ID = %s, New ID = %s",
558 command.getClientId(), state.getClientId());
561 boolean isRegister = command.getIsRegister();
562 for (ObjectIdP objectIdP : command.getObjectId()) {
563 ObjectId objectId = ProtoWrapperConverter.convertFromObjectIdProto(objectIdP);
564 // We may need to delay the registration command (if it is not already delayed).
566 if (!command.getIsDelayed()) {
567 delayMs = state.getNextDelay(objectId);
570 issueRegistration(objectId, isRegister);
572 AndroidListenerIntents.issueDelayedRegistrationIntent(getApplicationContext(), clock,
573 state.getClientId(), objectId, isRegister, delayMs, state.getNextRequestCode());
580 * Called when the client application requests a new registration. If a redundant register request
581 * is made -- i.e. when the application attempts to register an object that is already in the
582 * {@code AndroidListenerState#desiredRegistrations} collection -- the method returns immediately.
583 * Unregister requests are never ignored since we can't reliably determine whether an unregister
584 * request is redundant: our policy on failures of any kind is to remove the registration from
585 * the {@code AndroidListenerState#desiredRegistrations} collection.
587 private void issueRegistration(ObjectId objectId, boolean isRegister) {
589 if (state.addDesiredRegistration(objectId)) {
590 // Don't bother if we think it's already registered. Note that we remove the object from the
591 // collection when there is a failure.
592 getClient().register(objectId);
595 // Remove the object ID from the desired registration collection so that subsequent attempts
596 // to re-register are not ignored.
597 state.removeDesiredRegistration(objectId);
598 getClient().unregister(objectId);
602 /** Tries to handle a start intent. Returns {@code true} iff the intent is a start intent. */
603 private boolean tryHandleStartIntent(Intent intent) {
604 StartCommand command = AndroidListenerIntents.findStartCommand(intent);
605 if ((command == null) || !AndroidListenerProtos.isValidStartCommand(command)) {
608 // Reset the state so that we make no assumptions about desired registrations and can ignore
609 // messages directed at the wrong instance.
610 state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor);
611 boolean skipStartForTest = false;
612 ClientConfigP clientConfig = InvalidationClientCore.createConfig();
613 if (command.getAllowSuppression() != clientConfig.getAllowSuppression()) {
614 ClientConfigP.Builder clientConfigBuilder = clientConfig.toBuilder();
615 clientConfigBuilder.allowSuppression = command.getAllowSuppression();
616 clientConfig = clientConfigBuilder.build();
618 Intent startIntent = ProtocolIntents.InternalDowncalls.newCreateClientIntent(
619 command.getClientType(), command.getClientName(), clientConfig, skipStartForTest);
620 AndroidListenerIntents.issueTiclIntent(getApplicationContext(), startIntent);
624 /** Tries to handle an ack intent. Returns {@code true} iff the intent is an ack intent. */
625 private boolean tryHandleAckIntent(Intent intent) {
626 byte[] data = AndroidListenerIntents.findAckHandle(intent);
630 getClient().acknowledge(AckHandle.newInstance(data));
635 * Tries to handle a background invalidation intent. Returns {@code true} iff the intent is a
636 * background invalidation intent.
638 private boolean tryHandleBackgroundInvalidationsIntent(Intent intent) {
639 byte[] data = intent.getByteArrayExtra(ProtocolIntents.BACKGROUND_INVALIDATION_KEY);
644 InvalidationMessage invalidationMessage = InvalidationMessage.parseFrom(data);
645 List<Invalidation> invalidations = new ArrayList<Invalidation>();
646 for (InvalidationP invalidation : invalidationMessage.getInvalidation()) {
647 invalidations.add(ProtoWrapperConverter.convertFromInvalidationProto(invalidation));
649 backgroundInvalidateForInternalUse(invalidations);
650 } catch (ValidationException exception) {
651 logger.info("Failed to parse background invalidation intent payload: %s",
652 exception.getMessage());
657 /** Returns the current state of the listener, for tests. */
658 AndroidListenerState getStateForTest() {