/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.ipc.invalidation.external.client.contrib; import com.google.common.base.Preconditions; import com.google.ipc.invalidation.external.client.InvalidationClient; import com.google.ipc.invalidation.external.client.InvalidationListener; import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; import com.google.ipc.invalidation.external.client.types.AckHandle; import com.google.ipc.invalidation.external.client.types.ErrorInfo; import com.google.ipc.invalidation.external.client.types.Invalidation; import com.google.ipc.invalidation.external.client.types.ObjectId; import com.google.ipc.invalidation.ticl.ProtoConverter; import com.google.ipc.invalidation.ticl.android2.AndroidClock; import com.google.ipc.invalidation.ticl.android2.AndroidInvalidationListenerIntentMapper; import com.google.ipc.invalidation.ticl.android2.ProtocolIntents; import com.google.ipc.invalidation.ticl.android2.channel.AndroidChannelConstants.AuthTokenConstants; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protos.ipc.invalidation.AndroidListenerProtocol; import com.google.protos.ipc.invalidation.AndroidListenerProtocol.RegistrationCommand; import com.google.protos.ipc.invalidation.AndroidListenerProtocol.StartCommand; import com.google.protos.ipc.invalidation.ClientProtocol.ClientConfigP; import com.google.protos.ipc.invalidation.ClientProtocol.ObjectIdP; import android.app.IntentService; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import java.util.concurrent.TimeUnit; /** * Simplified listener contract for Android clients. Takes care of exponential back-off when * register or unregister are called for an object after a failure has occurred. Also suppresses * redundant register requests. * * <p>A sample implementation of an {@link AndroidListener} is shown below: * * <p><code> * class ExampleListener extends AndroidListener { * @Override * public void reissueRegistrations(byte[] clientId) { * List<ObjectId> desiredRegistrations = ...; * register(clientId, desiredRegistrations); * } * * @Override * public void invalidate(Invalidation invalidation, final byte[] ackHandle) { * // Track the most recent version of the object (application-specific) and then acknowledge * // the invalidation. * ... * acknowledge(ackHandle); * } * * @Override * public void informRegistrationFailure(byte[] clientId, ObjectId objectId, * boolean isTransient, String errorMessage) { * // Try again if there is a transient failure and we still care whether the object is * // registered or not. * if (isTransient) { * boolean shouldRetry = ...; * if (shouldRetry) { * boolean shouldBeRegistered = ...; * if (shouldBeRegistered) { * register(clientId, ImmutableList.of(objectId)); * } else { * unregister(clientId, ImmutableList.of(objectId)); * } * } * } * } * * ... * } * </code> * * <p>See {@link com.google.ipc.invalidation.examples.android2} for a complete sample. * */ public abstract class AndroidListener extends IntentService { /** External alarm receiver that allows the listener to respond to alarm intents. */ public static final class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(intent); if (intent.hasExtra(AndroidListenerIntents.EXTRA_REGISTRATION)) { AndroidListenerIntents.issueAndroidListenerIntent(context, intent); } } } /** The logger. */ private static final Logger logger = AndroidLogger.forPrefix(""); /** Initial retry delay for exponential backoff (1 minute). */ static int initialMaxDelayMs = (int) TimeUnit.SECONDS.toMillis(60); /** Maximum delay factor for exponential backoff (6 hours). */ static int maxDelayFactor = 6 * 60; /** The last client ID passed to the ready up-call. */ static byte[] lastClientIdForTest; /** * Invalidation listener implementation. We implement the interface on a private field rather * than directly to avoid leaking methods that should not be directly called by the client * application. The listener must be called only on intent service thread. */ private final InvalidationListener invalidationListener = new InvalidationListener() { @Override public final void ready(final InvalidationClient client) { byte[] clientId = state.getClientId().toByteArray(); AndroidListener.lastClientIdForTest = clientId; AndroidListener.this.ready(clientId); } @Override public final void reissueRegistrations(final InvalidationClient client, byte[] prefix, int prefixLength) { AndroidListener.this.reissueRegistrations(state.getClientId().toByteArray()); } @Override public final void informRegistrationStatus(final InvalidationClient client, final ObjectId objectId, final RegistrationState regState) { state.informRegistrationSuccess(objectId); AndroidListener.this.informRegistrationStatus(state.getClientId().toByteArray(), objectId, regState); } @Override public final void informRegistrationFailure(final InvalidationClient client, final ObjectId objectId, final boolean isTransient, final String errorMessage) { state.informRegistrationFailure(objectId, isTransient); AndroidListener.this.informRegistrationFailure(state.getClientId().toByteArray(), objectId, isTransient, errorMessage); } @Override public void invalidate(InvalidationClient client, Invalidation invalidation, AckHandle ackHandle) { AndroidListener.this.invalidate(invalidation, ackHandle.getHandleData()); } @Override public void invalidateUnknownVersion(InvalidationClient client, ObjectId objectId, AckHandle ackHandle) { AndroidListener.this.invalidateUnknownVersion(objectId, ackHandle.getHandleData()); } @Override public void invalidateAll(InvalidationClient client, AckHandle ackHandle) { AndroidListener.this.invalidateAll(ackHandle.getHandleData()); } @Override public void informError(InvalidationClient client, ErrorInfo errorInfo) { AndroidListener.this.informError(errorInfo); } }; /** * The internal state of the listener. Lazy initialization, triggered by {@link #onHandleIntent}. */ private AndroidListenerState state; /** The clock to use when scheduling retry call-backs. */ private final AndroidClock clock = new AndroidClock.SystemClock(); /** * The mapper used to route intents to the invalidation listener. Lazy initialization triggered * by {@link #onCreate}. */ private AndroidInvalidationListenerIntentMapper intentMapper; /** Initializes {@link AndroidListener}. */ protected AndroidListener() { super(""); // If the process dies before an intent is handled, setIntentRedelivery(true) ensures that the // last intent is redelivered. This optimization is not necessary for correctness: on restart, // all registrations will be reissued and unacked invalidations will be resent anyways. setIntentRedelivery(true); } /** See specs for {@link InvalidationClient#start}. */ public static Intent createStartIntent(Context context, int clientType, byte[] clientName) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(clientName); return AndroidListenerIntents.createStartIntent(context, clientType, clientName); } /** See specs for {@link InvalidationClient#stop}. */ public static Intent createStopIntent(Context context) { Preconditions.checkNotNull(context); return AndroidListenerIntents.createStopIntent(context); } /** * See specs for {@link InvalidationClient#register}. * * @param context the context * @param clientId identifier for the client service for which we are registering * @param objectIds the object ids being registered */ public static Intent createRegisterIntent(Context context, byte[] clientId, Iterable<ObjectId> objectIds) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(clientId); Preconditions.checkNotNull(objectIds); final boolean isRegister = true; return AndroidListenerIntents.createRegistrationIntent(context, clientId, objectIds, isRegister); } /** * See specs for {@link InvalidationClient#register}. * * @param clientId identifier for the client service for which we are registering * @param objectIds the object ids being registered */ public void register(byte[] clientId, Iterable<ObjectId> objectIds) { Preconditions.checkNotNull(clientId); Preconditions.checkNotNull(objectIds); Context context = getApplicationContext(); context.startService(createRegisterIntent(context, clientId, objectIds)); } /** * See specs for {@link InvalidationClient#unregister}. * * @param context the context * @param clientId identifier for the client service for which we are unregistering * @param objectIds the object ids being unregistered */ public static Intent createUnregisterIntent(Context context, byte[] clientId, Iterable<ObjectId> objectIds) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(clientId); Preconditions.checkNotNull(objectIds); final boolean isRegister = false; return AndroidListenerIntents.createRegistrationIntent(context, clientId, objectIds, isRegister); } /** * Sets the authorization token and type used by the invalidation client. Call in response to * {@link #requestAuthToken} calls. * * @param pendingIntent pending intent passed to {@link #requestAuthToken} * @param authToken authorization token * @param authType authorization token typo */ public static void setAuthToken(Context context, PendingIntent pendingIntent, String authToken, String authType) { Preconditions.checkNotNull(pendingIntent); Preconditions.checkNotNull(authToken); Preconditions.checkNotNull(authType); AndroidListenerIntents.issueAuthTokenResponse(context, pendingIntent, authToken, authType); } /** * See specs for {@link InvalidationClient#unregister}. * * @param clientId identifier for the client service for which we are registering * @param objectIds the object ids being unregistered */ public void unregister(byte[] clientId, Iterable<ObjectId> objectIds) { Preconditions.checkNotNull(clientId); Preconditions.checkNotNull(objectIds); Context context = getApplicationContext(); context.startService(createUnregisterIntent(context, clientId, objectIds)); } /** See specs for {@link InvalidationClient#acknowledge}. */ public static Intent createAcknowledgeIntent(Context context, byte[] ackHandle) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(ackHandle); return AndroidListenerIntents.createAckIntent(context, ackHandle); } /** See specs for {@link InvalidationClient#acknowledge}. */ public void acknowledge(byte[] ackHandle) { Preconditions.checkNotNull(ackHandle); Context context = getApplicationContext(); context.startService(createAcknowledgeIntent(context, ackHandle)); } /** * See specs for {@link InvalidationListener#ready}. * * @param clientId the client identifier that must be passed to {@link #createRegisterIntent} * and {@link #createUnregisterIntent} */ public abstract void ready(byte[] clientId); /** * See specs for {@link InvalidationListener#reissueRegistrations}. * * @param clientId the client identifier that must be passed to {@link #createRegisterIntent} * and {@link #createUnregisterIntent} */ public abstract void reissueRegistrations(byte[] clientId); /** * See specs for {@link InvalidationListener#informError}. */ public abstract void informError(ErrorInfo errorInfo); /** * See specs for {@link InvalidationListener#invalidate}. * * @param invalidation the invalidation * @param ackHandle event acknowledgment handle */ public abstract void invalidate(Invalidation invalidation, byte[] ackHandle); /** * See specs for {@link InvalidationListener#invalidateUnknownVersion}. * * @param objectId identifier for the object with unknown version * @param ackHandle event acknowledgment handle */ public abstract void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle); /** * See specs for {@link InvalidationListener#invalidateAll}. * * @param ackHandle event acknowledgment handle */ public abstract void invalidateAll(byte[] ackHandle); /** * Read listener state. * * @return serialized state or {@code null} if it is not available */ public abstract byte[] readState(); /** Write listener state to some location. */ public abstract void writeState(byte[] data); /** * See specs for {@link InvalidationListener#informRegistrationFailure}. */ public abstract void informRegistrationFailure(byte[] clientId, ObjectId objectId, boolean isTransient, String errorMessage); /** * See specs for (@link InvalidationListener#informRegistrationStatus}. */ public abstract void informRegistrationStatus(byte[] clientId, ObjectId objectId, RegistrationState regState); /** * Called when an authorization token is needed. Respond by calling {@link #setAuthToken}. * * @param pendingIntent pending intent that must be used in {@link #setAuthToken} response. * @param invalidAuthToken the existing invalid token or null if none exists. Implementation * should invalidate the token. */ public abstract void requestAuthToken(PendingIntent pendingIntent, String invalidAuthToken); @Override public void onCreate() { super.onCreate(); // Initialize the intent mapper (now that context is available). intentMapper = new AndroidInvalidationListenerIntentMapper(invalidationListener, this); } @Override protected void onHandleIntent(Intent intent) { if (intent == null) { return; } // We lazily initialize state in calls to onHandleIntent rather than initializing in onCreate // because onCreate runs on the UI thread and initializeState performs I/O. if (state == null) { initializeState(); } // Handle any intents specific to the AndroidListener. For other intents, defer to the // intentMapper, which handles listener upcalls corresponding to the InvalidationListener // methods. if (!tryHandleAuthTokenRequestIntent(intent) && !tryHandleRegistrationIntent(intent) && !tryHandleStartIntent(intent) && !tryHandleStopIntent(intent) && !tryHandleAckIntent(intent)) { intentMapper.handleIntent(intent); } // Always check to see if we need to persist state changes after handling an intent. if (state.getIsDirty()) { writeState(state.marshal().toByteArray()); state.resetIsDirty(); } } /** Returns invalidation client that can be used to trigger intents against the TICL service. */ private InvalidationClient getClient() { return intentMapper.client; } /** * Initializes listener state either from persistent proto (if available) or from scratch. */ private void initializeState() { AndroidListenerProtocol.AndroidListenerState proto = getPersistentState(); if (proto != null) { state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor, proto); } else { state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor); } } /** * Reads and parses persistent state for the listener. Returns {@code null} if the state does not * exist or is invalid. */ private AndroidListenerProtocol.AndroidListenerState getPersistentState() { // Defer to application code to read the blob containing the state proto. byte[] stateData = readState(); try { if (null != stateData) { AndroidListenerProtocol.AndroidListenerState state = AndroidListenerProtocol.AndroidListenerState.parseFrom(stateData); if (!AndroidListenerProtos.isValidAndroidListenerState(state)) { logger.warning("Invalid listener state."); return null; } return state; } } catch (InvalidProtocolBufferException exception) { logger.warning("Failed to parse listener state: %s", exception); } return null; } /** * Tries to handle a request for an authorization token. Returns {@code true} iff the intent is * an auth token request. */ private boolean tryHandleAuthTokenRequestIntent(Intent intent) { if (!AndroidListenerIntents.isAuthTokenRequest(intent)) { return false; } Context context = getApplicationContext(); // Check for invalid auth token. Subclass may have to invalidate it if it exists in the call // to getNewAuthToken. String invalidAuthToken = intent.getStringExtra( AuthTokenConstants.EXTRA_INVALIDATE_AUTH_TOKEN); // Intent also includes a pending intent that we can use to pass back our response. PendingIntent pendingIntent = intent.getParcelableExtra( AuthTokenConstants.EXTRA_PENDING_INTENT); if (pendingIntent == null) { logger.warning("Authorization request without pending intent extra."); } else { // Delegate to client application to figure out what the new token should be and the auth // type. requestAuthToken(pendingIntent, invalidAuthToken); } return true; } /** Tries to handle a stop intent. Returns {@code true} iff the intent is a stop intent. */ private boolean tryHandleStopIntent(Intent intent) { if (!AndroidListenerIntents.isStopIntent(intent)) { return false; } getClient().stop(); return true; } /** * Tries to handle a registration intent. Returns {@code true} iff the intent is a registration * intent. */ private boolean tryHandleRegistrationIntent(Intent intent) { RegistrationCommand command = AndroidListenerIntents.findRegistrationCommand(intent); if ((command == null) || !AndroidListenerProtos.isValidRegistrationCommand(command)) { return false; } // Make sure the registration is intended for this client. If not, we ignore it (suggests // there is a new client now). if (!command.getClientId().equals(state.getClientId())) { logger.warning("Ignoring registration request for old client. Old ID = {0}, New ID = {1}", command.getClientId(), state.getClientId()); return true; } boolean isRegister = command.getIsRegister(); for (ObjectIdP objectIdP : command.getObjectIdList()) { ObjectId objectId = ProtoConverter.convertFromObjectIdProto(objectIdP); // We may need to delay the registration command (if it is not already delayed). int delayMs = 0; if (!command.getIsDelayed()) { delayMs = state.getNextDelay(objectId); } if (delayMs == 0) { issueRegistration(objectId, isRegister); } else { AndroidListenerIntents.issueDelayedRegistrationIntent(getApplicationContext(), clock, state.getClientId(), objectId, isRegister, delayMs, state.getNextRequestCode()); } } return true; } /** * Called when the client application requests a new registration. If a redundant register request * is made -- i.e. when the application attempts to register an object that is already in the * {@code AndroidListenerState#desiredRegistrations} collection -- the method returns immediately. * Unregister requests are never ignored since we can't reliably determine whether an unregister * request is redundant: our policy on failures of any kind is to remove the registration from * the {@code AndroidListenerState#desiredRegistrations} collection. */ private void issueRegistration(ObjectId objectId, boolean isRegister) { if (isRegister) { if (state.addDesiredRegistration(objectId)) { // Don't bother if we think it's already registered. Note that we remove the object from the // collection when there is a failure. getClient().register(objectId); } } else { // Remove the object ID from the desired registration collection so that subsequent attempts // to re-register are not ignored. state.removeDesiredRegistration(objectId); getClient().unregister(objectId); } } /** Tries to handle a start intent. Returns {@code true} iff the intent is a start intent. */ private boolean tryHandleStartIntent(Intent intent) { StartCommand command = AndroidListenerIntents.findStartCommand(intent); if ((command == null) || !AndroidListenerProtos.isValidStartCommand(command)) { return false; } // Reset the state so that we make no assumptions about desired registrations and can ignore // messages directed at the wrong instance. state = new AndroidListenerState(initialMaxDelayMs, maxDelayFactor); boolean skipStartForTest = false; Intent startIntent = ProtocolIntents.InternalDowncalls.newCreateClientIntent( command.getClientType(), command.getClientName().toByteArray(), ClientConfigP.getDefaultInstance(), skipStartForTest); AndroidListenerIntents.issueTiclIntent(getApplicationContext(), startIntent); return true; } /** Tries to handle an ack intent. Returns {@code true} iff the intent is an ack intent. */ private boolean tryHandleAckIntent(Intent intent) { byte[] data = AndroidListenerIntents.findAckHandle(intent); if (data == null) { return false; } getClient().acknowledge(AckHandle.newInstance(data)); return true; } /** Returns the current state of the listener, for tests. */ AndroidListenerState getStateForTest() { return state; } }