/*
* 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.common.base.Preconditions;
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 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.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.HashSet;
import java.util.List;
import java.util.Set;
/**
* Implements the service that handles invalidation client events for this application. This
* listener registers an interest in a small number of objects and calls {@link MainActivity} with
* any relevant status changes.
*
*/
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 listener state in shared preferences. */
private static final String STATE_KEY = "example_listener_state";
/** Object source for objects the client is tracking. */
private static final int DEMO_SOURCE = 4;
/** Prefix for object names. */
private static final String OBJECT_ID_PREFIX = "Obj";
/** The tag used for logging in the listener. */
private static final String TAG = "TEA2:ExampleListener";
/** Number of objects we're interested in tracking. */
static final int NUM_INTERESTING_OBJECTS = 4;
/** Ids for objects we want to track. */
private final Set<ObjectId> interestingObjects;
public ExampleListener() {
super();
// We're interested in objects with ids Obj1, Obj2, ...
interestingObjects = new HashSet<ObjectId>();
for (int i = 1; i <= NUM_INTERESTING_OBJECTS; i++) {
interestingObjects.add(getObjectId(i));
}
}
@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()");
}
@Override
public void reissueRegistrations(byte[] clientId) {
Log.i(TAG, "reissueRegistrations()");
register(clientId, interestingObjects);
}
@Override
public void invalidate(Invalidation invalidation, byte[] ackHandle) {
Log.i(TAG, "invalidate: " + invalidation);
// Do real work here based upon the invalidation
MainActivity.State.setVersion(
invalidation.getObjectId(), "invalidate", invalidation.toString());
acknowledge(ackHandle);
}
@Override
public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {
Log.i(TAG, "invalidateUnknownVersion: " + objectId);
// Do real work here based upon the invalidation.
MainActivity.State.setVersion(
objectId, "invalidateUnknownVersion", getBackendVersion(objectId));
acknowledge(ackHandle);
}
@Override
public void invalidateAll(byte[] ackHandle) {
Log.i(TAG, "invalidateAll");
// Do real work here based upon the invalidation.
for (ObjectId objectId : interestingObjects) {
MainActivity.State.setVersion(objectId, "invalidateAll", getBackendVersion(objectId));
}
acknowledge(ackHandle);
}
@Override
public byte[] readState() {
Log.i(TAG, "readState");
SharedPreferences sharedPreferences = getSharedPreferences();
String data = sharedPreferences.getString(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(STATE_KEY, Base64.encodeToString(data, Base64.DEFAULT));
editor.commit();
}
@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;
}
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) {
Preconditions.checkNotNull(accountManager);
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 (interestingObjects.contains(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 (!interestingObjects.contains(objectId)) {
Log.i(TAG, "Unregistering for object we're no longer interested in");
unregister(clientId, objectIds);
}
} else {
if (interestingObjects.contains(objectId)) {
Log.i(TAG, "Registering for an object");
register(clientId, objectIds);
}
}
}
private SharedPreferences getSharedPreferences() {
return getApplicationContext().getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
}
private String getBackendVersion(ObjectId objectId) {
/***********************************************************************************************
* YOUR CODE HERE
*
* Invalidation client has no information about the given object. Connect with the application
* backend to determine its current state. The implementation should be non-blocking.
**********************************************************************************************/
// Normally, we would connect to a real application backend. For this example, we return a fixed
// value.
return "some value from custom app backend";
}
/** Gets object ID given index. */
private static ObjectId getObjectId(int i) {
return ObjectId.newInstance(DEMO_SOURCE, (OBJECT_ID_PREFIX + i).getBytes());
}
}