| Index: third_party/cacheinvalidation/src/javaexample/com/google/ipc/invalidation/examples/android2/ExampleListener.java
|
| diff --git a/third_party/cacheinvalidation/src/javaexample/com/google/ipc/invalidation/examples/android2/ExampleListener.java b/third_party/cacheinvalidation/src/javaexample/com/google/ipc/invalidation/examples/android2/ExampleListener.java
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..3510c13df91f1921fc8a14b4914c1013813ba3a2
|
| --- /dev/null
|
| +++ b/third_party/cacheinvalidation/src/javaexample/com/google/ipc/invalidation/examples/android2/ExampleListener.java
|
| @@ -0,0 +1,477 @@
|
| +/*
|
| + * 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.examples.android2;
|
| +
|
| +import com.google.ipc.invalidation.examples.android2.ExampleListenerProto.ExampleListenerStateProto.ObjectIdProto;
|
| +import com.google.ipc.invalidation.external.client.InvalidationClientConfig;
|
| +import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
|
| +import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
|
| +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.protobuf.nano.InvalidProtocolBufferNanoException;
|
| +import com.google.protobuf.nano.MessageNano;
|
| +
|
| +import android.accounts.Account;
|
| +import android.accounts.AccountManager;
|
| +import android.accounts.AccountManagerFuture;
|
| +import android.accounts.AuthenticatorException;
|
| +import android.accounts.OperationCanceledException;
|
| +import android.app.PendingIntent;
|
| +import android.content.Context;
|
| +import android.content.Intent;
|
| +import android.content.SharedPreferences;
|
| +import android.content.SharedPreferences.Editor;
|
| +import android.os.Bundle;
|
| +import android.util.Base64;
|
| +import android.util.Log;
|
| +
|
| +import java.io.IOException;
|
| +import java.util.ArrayList;
|
| +import java.util.List;
|
| +import java.util.Locale;
|
| +
|
| +
|
| +/**
|
| + * Implements the service that handles invalidation client events for this application. It maintains
|
| + * state for all objects tracked by the listener (see {@link ExampleListenerState}). By default, the
|
| + * listener registers an interest in a small number of objects when started, but it responds to
|
| + * registration intents from the main activity (see {@link #createRegisterIntent} and
|
| + * {@link #createUnregisterIntent}) so that registrations can be dynamically managed.
|
| + * <p>
|
| + * Many errors cases in this example implementation are handled by logging errors, which is not the
|
| + * appropriate response in a real application where retries or user notification may be needed.
|
| + *
|
| + */
|
| +public final class ExampleListener extends AndroidListener {
|
| +
|
| + /** The account type value for Google accounts */
|
| + private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
|
| +
|
| + /**
|
| + * This is the authentication token type that's used for invalidation client communication to the
|
| + * server. For real applications, it would generally match the authorization type used by the
|
| + * application.
|
| + */
|
| + private static final String AUTH_TYPE = "android";
|
| +
|
| + /** Name used for shared preferences. */
|
| + private static final String PREFERENCES_NAME = "example_listener";
|
| +
|
| + /** Key used for {@link AndroidListener} state in shared preferences. */
|
| + private static final String ANDROID_LISTENER_STATE_KEY = "android_listener_state";
|
| +
|
| + /** Key used for {@link ExampleListener} state in shared preferences. */
|
| + private static final String EXAMPLE_LISTENER_STATE_KEY = "example_listener_state";
|
| +
|
| + /** The tag used for logging in the listener. */
|
| + private static final String TAG = "TEA2:EL";
|
| +
|
| + /** Ticl client configuration. */
|
| + private static final int CLIENT_TYPE = 4; // Demo client ID.
|
| + private static final byte[] CLIENT_NAME = "TEA2:eetrofoot".getBytes();
|
| +
|
| + // Intent constants.
|
| + private static final String START_INTENT_ACTION = TAG + ":START";
|
| + private static final String STOP_INTENT_ACTION = TAG + ":STOP";
|
| + private static final String REGISTER_INTENT_ACTION = TAG + ":REGISTER";
|
| + private static final String UNREGISTER_INTENT_ACTION = TAG + ":UNREGISTER";
|
| + private static final String OBJECT_ID_EXTRA = "oid";
|
| +
|
| + /** Persistent state for the example listener. */
|
| + private ExampleListenerState exampleListenerState;
|
| +
|
| + public ExampleListener() {
|
| + super();
|
| + }
|
| +
|
| + @Override
|
| + public void onCreate() {
|
| + super.onCreate();
|
| +
|
| + // Deserialize persistent state.
|
| + String data = getSharedPreferences().getString(EXAMPLE_LISTENER_STATE_KEY, null);
|
| + exampleListenerState = ExampleListenerState.deserialize(data);
|
| + }
|
| +
|
| + @Override
|
| + public void onHandleIntent(Intent intent) {
|
| + if (intent == null) {
|
| + return;
|
| + }
|
| +
|
| + boolean handled = tryHandleRegistrationIntent(intent);
|
| + handled = handled || tryHandleStartIntent(intent);
|
| + handled = handled || tryHandleStopIntent(intent);
|
| + if (!handled) {
|
| + super.onHandleIntent(intent);
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void informError(ErrorInfo errorInfo) {
|
| + Log.e(TAG, "informError: " + errorInfo);
|
| +
|
| + /***********************************************************************************************
|
| + * YOUR CODE HERE
|
| + *
|
| + * Handling of permanent failures is application-specific.
|
| + **********************************************************************************************/
|
| + }
|
| +
|
| + @Override
|
| + public void ready(byte[] clientId) {
|
| + Log.i(TAG, "ready()");
|
| + exampleListenerState.setClientId(clientId);
|
| + writeExampleListenerState();
|
| + }
|
| +
|
| + @Override
|
| + public void reissueRegistrations(byte[] clientId) {
|
| + Log.i(TAG, "reissueRegistrations()");
|
| + register(clientId, exampleListenerState.getInterestingObjects());
|
| + }
|
| +
|
| + @Override
|
| + public void invalidate(Invalidation invalidation, byte[] ackHandle) {
|
| + Log.i(TAG, "invalidate: " + invalidation);
|
| +
|
| + exampleListenerState.informInvalidation(invalidation.getObjectId(), invalidation.getVersion(),
|
| + invalidation.getPayload(), /* isBackground */ false);
|
| + writeExampleListenerState();
|
| +
|
| + // Do real work here based upon the invalidation
|
| +
|
| + acknowledge(ackHandle);
|
| + }
|
| +
|
| + @Override
|
| + public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {
|
| + Log.i(TAG, "invalidateUnknownVersion: " + objectId);
|
| +
|
| + exampleListenerState.informUnknownVersionInvalidation(objectId);
|
| + writeExampleListenerState();
|
| +
|
| + // In a real app, the application backend would need to be consulted for object state.
|
| +
|
| + acknowledge(ackHandle);
|
| + }
|
| +
|
| + @Override
|
| + public void invalidateAll(byte[] ackHandle) {
|
| + Log.i(TAG, "invalidateAll");
|
| +
|
| + // Do real work here based upon the invalidation.
|
| + exampleListenerState.informInvalidateAll();
|
| + writeExampleListenerState();
|
| +
|
| + acknowledge(ackHandle);
|
| + }
|
| +
|
| +
|
| + @Override
|
| + public byte[] readState() {
|
| + Log.i(TAG, "readState");
|
| + SharedPreferences sharedPreferences = getSharedPreferences();
|
| + String data = sharedPreferences.getString(ANDROID_LISTENER_STATE_KEY, null);
|
| + return (data != null) ? Base64.decode(data, Base64.DEFAULT) : null;
|
| + }
|
| +
|
| + @Override
|
| + public void writeState(byte[] data) {
|
| + Log.i(TAG, "writeState");
|
| + Editor editor = getSharedPreferences().edit();
|
| + editor.putString(ANDROID_LISTENER_STATE_KEY, Base64.encodeToString(data, Base64.DEFAULT));
|
| + if (!editor.commit()) {
|
| + Log.e(TAG, "failed to write state"); // In a real app, this case would need to handled.
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void requestAuthToken(PendingIntent pendingIntent,
|
| + String invalidAuthToken) {
|
| + Log.i(TAG, "requestAuthToken");
|
| +
|
| + // In response to requestAuthToken, we need to get an auth token and inform the invalidation
|
| + // client of the result through a call to setAuthToken. In this example, we block until a
|
| + // result is available. It is also possible to invoke setAuthToken in a callback or when
|
| + // handling an intent.
|
| + AccountManager accountManager = AccountManager.get(getApplicationContext());
|
| +
|
| + // Invalidate the old token if necessary.
|
| + if (invalidAuthToken != null) {
|
| + accountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, invalidAuthToken);
|
| + }
|
| +
|
| + // Choose an (arbitrary in this example) account for which to retrieve an authentication token.
|
| + Account account = getAccount(accountManager);
|
| +
|
| + try {
|
| + // There are three possible outcomes of the call to getAuthToken:
|
| + //
|
| + // 1. Authentication failure (null result).
|
| + // 2. The user needs to sign in or give permission for the account. In such cases, the result
|
| + // includes an intent that can be started to retrieve credentials from the user.
|
| + // 3. The response includes the auth token, in which case we can inform the invalidation
|
| + // client.
|
| + //
|
| + // In the first case, we simply log and return. The response to such errors is application-
|
| + // specific. For instance, the application may prompt the user to choose another account.
|
| + //
|
| + // In the second case, we start an intent to ask for user credentials so that they are
|
| + // available to the application if there is a future request. An application should listen for
|
| + // the LOGIN_ACCOUNTS_CHANGED_ACTION broadcast intent to trigger a response to the
|
| + // invalidation client after the user has responded. Otherwise, it may take several minutes
|
| + // for the invalidation client to start.
|
| + //
|
| + // In the third case, success!, we pass the authorization token and type to the invalidation
|
| + // client using the setAuthToken method.
|
| + AccountManagerFuture<Bundle> future = accountManager.getAuthToken(account, AUTH_TYPE,
|
| + new Bundle(), false, null, null);
|
| + Bundle result = future.getResult();
|
| + if (result == null) {
|
| + // If the result is null, it means that authentication was not possible.
|
| + Log.w(TAG, "Auth token - getAuthToken returned null");
|
| + return;
|
| + }
|
| + if (result.containsKey(AccountManager.KEY_INTENT)) {
|
| + Log.i(TAG, "Starting intent to get auth credentials");
|
| +
|
| + // Need to start intent to get credentials.
|
| + Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
|
| + int flags = intent.getFlags();
|
| + flags |= Intent.FLAG_ACTIVITY_NEW_TASK;
|
| + intent.setFlags(flags);
|
| + getApplicationContext().startActivity(intent);
|
| + return;
|
| + }
|
| +
|
| + Log.i(TAG, "Passing auth token to invalidation client");
|
| + String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
|
| + setAuthToken(getApplicationContext(), pendingIntent, authToken, AUTH_TYPE);
|
| + } catch (OperationCanceledException e) {
|
| + Log.w(TAG, "Auth token - operation cancelled", e);
|
| + } catch (AuthenticatorException e) {
|
| + Log.w(TAG, "Auth token - authenticator exception", e);
|
| + } catch (IOException e) {
|
| + Log.w(TAG, "Auth token - IO exception", e);
|
| + }
|
| + }
|
| +
|
| + /** Returns any Google account enabled on the device. */
|
| + private static Account getAccount(AccountManager accountManager) {
|
| + if (accountManager == null) {
|
| + throw new NullPointerException();
|
| + }
|
| + for (Account acct : accountManager.getAccounts()) {
|
| + if (GOOGLE_ACCOUNT_TYPE.equals(acct.type)) {
|
| + return acct;
|
| + }
|
| + }
|
| + throw new RuntimeException("No google account enabled.");
|
| + }
|
| +
|
| + @Override
|
| + public void informRegistrationFailure(byte[] clientId, ObjectId objectId, boolean isTransient,
|
| + String errorMessage) {
|
| + Log.e(TAG, "Registration failure!");
|
| + if (isTransient) {
|
| + // Retry immediately on transient failures. The base AndroidListener will handle exponential
|
| + // backoff if there are repeated failures.
|
| + List<ObjectId> objectIds = new ArrayList<ObjectId>();
|
| + objectIds.add(objectId);
|
| + if (exampleListenerState.isInterestedInObject(objectId)) {
|
| + Log.i(TAG, "Retrying registration of " + objectId);
|
| + register(clientId, objectIds);
|
| + } else {
|
| + Log.i(TAG, "Retrying unregistration of " + objectId);
|
| + unregister(clientId, objectIds);
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + public void informRegistrationStatus(byte[] clientId, ObjectId objectId,
|
| + RegistrationState regState) {
|
| + Log.i(TAG, "informRegistrationStatus");
|
| +
|
| + List<ObjectId> objectIds = new ArrayList<ObjectId>();
|
| + objectIds.add(objectId);
|
| + if (regState == RegistrationState.REGISTERED) {
|
| + if (!exampleListenerState.isInterestedInObject(objectId)) {
|
| + Log.i(TAG, "Unregistering for object we're no longer interested in");
|
| + unregister(clientId, objectIds);
|
| + writeExampleListenerState();
|
| + }
|
| + } else {
|
| + if (exampleListenerState.isInterestedInObject(objectId)) {
|
| + Log.i(TAG, "Registering for an object");
|
| + register(clientId, objectIds);
|
| + writeExampleListenerState();
|
| + }
|
| + }
|
| + }
|
| +
|
| + @Override
|
| + protected void backgroundInvalidateForInternalUse(Iterable<Invalidation> invalidations) {
|
| + for (Invalidation invalidation : invalidations) {
|
| + Log.i(TAG, "background invalidate: " + invalidation);
|
| + exampleListenerState.informInvalidation(invalidation.getObjectId(), invalidation.getVersion(),
|
| + invalidation.getPayload(), /* isBackground */ true);
|
| + writeExampleListenerState();
|
| + }
|
| + }
|
| +
|
| + /** Creates an intent that registers an interest in object invalidations for {@code objectId}. */
|
| + public static Intent createRegisterIntent(Context context, ObjectId objectId) {
|
| + return createRegistrationIntent(context, objectId, /* isRegister */ true);
|
| + }
|
| +
|
| + /** Creates an intent that unregisters for invalidations for {@code objectId}. */
|
| + public static Intent createUnregisterIntent(Context context, ObjectId objectId) {
|
| + return createRegistrationIntent(context, objectId, /* isRegister */ false);
|
| + }
|
| +
|
| + private static Intent createRegistrationIntent(Context context, ObjectId objectId,
|
| + boolean isRegister) {
|
| + Intent intent = new Intent();
|
| + intent.setAction(isRegister ? REGISTER_INTENT_ACTION : UNREGISTER_INTENT_ACTION);
|
| + intent.putExtra(OBJECT_ID_EXTRA, serializeObjectId(objectId));
|
| + intent.setClass(context, ExampleListener.class);
|
| + return intent;
|
| + }
|
| +
|
| + /** Creates an intent that starts the invalidation client. */
|
| + public static Intent createStartIntent(Context context) {
|
| + Intent intent = new Intent();
|
| + intent.setAction(START_INTENT_ACTION);
|
| + intent.setClass(context, ExampleListener.class);
|
| + return intent;
|
| + }
|
| +
|
| + /** Creates an intent that stops the invalidation client. */
|
| + public static Intent createStopIntent(Context context) {
|
| + Intent intent = new Intent();
|
| + intent.setAction(STOP_INTENT_ACTION);
|
| + intent.setClass(context, ExampleListener.class);
|
| + return intent;
|
| + }
|
| +
|
| + private boolean tryHandleRegistrationIntent(Intent intent) {
|
| + final boolean isRegister;
|
| + if (REGISTER_INTENT_ACTION.equals(intent.getAction())) {
|
| + isRegister = true;
|
| + } else if (UNREGISTER_INTENT_ACTION.equals(intent.getAction())) {
|
| + isRegister = false;
|
| + } else {
|
| + // Not a registration intent.
|
| + return false;
|
| + }
|
| +
|
| + // Try to parse object id extra.
|
| + ObjectId objectId = parseObjectIdExtra(intent);
|
| + if (objectId == null) {
|
| + Log.e(TAG, "Registration intent without valid object id extra");
|
| + return false;
|
| + }
|
| +
|
| + // Update example listener state.
|
| + if (isRegister) {
|
| + exampleListenerState.addObjectOfInterest(objectId);
|
| + } else {
|
| + exampleListenerState.removeObjectOfInterest(objectId);
|
| + }
|
| + writeExampleListenerState();
|
| +
|
| + // If the client is ready, perform registration now.
|
| + byte[] clientId = exampleListenerState.getClientId();
|
| + if (clientId == null) {
|
| + Log.i(TAG, "Deferring registration until client is ready");
|
| + } else {
|
| + // Perform registration immediately if we have been assigned a client id.
|
| + List<ObjectId> objectIds = new ArrayList<ObjectId>(1);
|
| + objectIds.add(objectId);
|
| + if (isRegister) {
|
| + register(clientId, objectIds);
|
| + } else {
|
| + unregister(clientId, objectIds);
|
| + }
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + private boolean tryHandleStartIntent(Intent intent) {
|
| + if (START_INTENT_ACTION.equals(intent.getAction())) {
|
| + // Clear the client id since a new one will be provided after the client has started.
|
| + exampleListenerState.setClientId(null);
|
| + writeExampleListenerState();
|
| +
|
| + // Setting this to true allows us to see invalidations that may suppress older invalidations.
|
| + // When this flag is 'false', AndroidListener#invalidateUnknownVersion is called instead of
|
| + // AndroidListener#invalidate when suppression has potentially occurred.
|
| + final boolean allowSuppression = true;
|
| + InvalidationClientConfig config = new InvalidationClientConfig(CLIENT_TYPE, CLIENT_NAME,
|
| + "ExampleListener", allowSuppression);
|
| + startService(AndroidListener.createStartIntent(this, config));
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + private boolean tryHandleStopIntent(Intent intent) {
|
| + if (STOP_INTENT_ACTION.equals(intent.getAction())) {
|
| + // Clear the client id since the client is no longer available.
|
| + exampleListenerState.setClientId(null);
|
| + writeExampleListenerState();
|
| + startService(AndroidListener.createStopIntent(this));
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + private void writeExampleListenerState() {
|
| + Editor editor = getSharedPreferences().edit();
|
| + editor.putString(EXAMPLE_LISTENER_STATE_KEY, exampleListenerState.serialize());
|
| + if (!editor.commit()) {
|
| + // In a real app, this case would need to handled.
|
| + Log.e(TAG, "failed to write example listener state");
|
| + }
|
| + MainActivity.State.setInfo(exampleListenerState.toString());
|
| + }
|
| +
|
| + private static byte[] serializeObjectId(ObjectId objectId) {
|
| + return MessageNano.toByteArray(ExampleListenerState.serializeObjectId(objectId));
|
| + }
|
| +
|
| + private static ObjectId parseObjectIdExtra(Intent intent) {
|
| + byte[] bytes = intent.getByteArrayExtra(OBJECT_ID_EXTRA);
|
| + if (bytes == null) {
|
| + return null;
|
| + }
|
| + try {
|
| + ObjectIdProto proto = MessageNano.mergeFrom(new ObjectIdProto(), bytes);
|
| + return ExampleListenerState.deserializeObjectId(proto);
|
| + } catch (InvalidProtocolBufferNanoException exception) {
|
| + Log.e(TAG, String.format(Locale.ROOT, "Error parsing object id. error='%s'",
|
| + exception.getMessage()));
|
| + return null;
|
| + }
|
| + }
|
| +
|
| + private SharedPreferences getSharedPreferences() {
|
| + return getApplicationContext().getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
|
| + }
|
| +}
|
|
|