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.examples.android2;
18 import com.google.ipc.invalidation.examples.android2.ExampleListenerProto.ExampleListenerStateProto.ObjectIdProto;
19 import com.google.ipc.invalidation.external.client.InvalidationClientConfig;
20 import com.google.ipc.invalidation.external.client.InvalidationListener.RegistrationState;
21 import com.google.ipc.invalidation.external.client.contrib.AndroidListener;
22 import com.google.ipc.invalidation.external.client.types.ErrorInfo;
23 import com.google.ipc.invalidation.external.client.types.Invalidation;
24 import com.google.ipc.invalidation.external.client.types.ObjectId;
25 import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
26 import com.google.protobuf.nano.MessageNano;
28 import android.accounts.Account;
29 import android.accounts.AccountManager;
30 import android.accounts.AccountManagerFuture;
31 import android.accounts.AuthenticatorException;
32 import android.accounts.OperationCanceledException;
33 import android.app.PendingIntent;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.SharedPreferences;
37 import android.content.SharedPreferences.Editor;
38 import android.os.Bundle;
39 import android.util.Base64;
40 import android.util.Log;
42 import java.io.IOException;
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.Locale;
49 * Implements the service that handles invalidation client events for this application. It maintains
50 * state for all objects tracked by the listener (see {@link ExampleListenerState}). By default, the
51 * listener registers an interest in a small number of objects when started, but it responds to
52 * registration intents from the main activity (see {@link #createRegisterIntent} and
53 * {@link #createUnregisterIntent}) so that registrations can be dynamically managed.
55 * Many errors cases in this example implementation are handled by logging errors, which is not the
56 * appropriate response in a real application where retries or user notification may be needed.
59 public final class ExampleListener extends AndroidListener {
61 /** The account type value for Google accounts */
62 private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
65 * This is the authentication token type that's used for invalidation client communication to the
66 * server. For real applications, it would generally match the authorization type used by the
69 private static final String AUTH_TYPE = "android";
71 /** Name used for shared preferences. */
72 private static final String PREFERENCES_NAME = "example_listener";
74 /** Key used for {@link AndroidListener} state in shared preferences. */
75 private static final String ANDROID_LISTENER_STATE_KEY = "android_listener_state";
77 /** Key used for {@link ExampleListener} state in shared preferences. */
78 private static final String EXAMPLE_LISTENER_STATE_KEY = "example_listener_state";
80 /** The tag used for logging in the listener. */
81 private static final String TAG = "TEA2:EL";
83 /** Ticl client configuration. */
84 private static final int CLIENT_TYPE = 4; // Demo client ID.
85 private static final byte[] CLIENT_NAME = "TEA2:eetrofoot".getBytes();
88 private static final String START_INTENT_ACTION = TAG + ":START";
89 private static final String STOP_INTENT_ACTION = TAG + ":STOP";
90 private static final String REGISTER_INTENT_ACTION = TAG + ":REGISTER";
91 private static final String UNREGISTER_INTENT_ACTION = TAG + ":UNREGISTER";
92 private static final String OBJECT_ID_EXTRA = "oid";
94 /** Persistent state for the example listener. */
95 private ExampleListenerState exampleListenerState;
97 public ExampleListener() {
102 public void onCreate() {
105 // Deserialize persistent state.
106 String data = getSharedPreferences().getString(EXAMPLE_LISTENER_STATE_KEY, null);
107 exampleListenerState = ExampleListenerState.deserialize(data);
111 public void onHandleIntent(Intent intent) {
112 if (intent == null) {
116 boolean handled = tryHandleRegistrationIntent(intent);
117 handled = handled || tryHandleStartIntent(intent);
118 handled = handled || tryHandleStopIntent(intent);
120 super.onHandleIntent(intent);
125 public void informError(ErrorInfo errorInfo) {
126 Log.e(TAG, "informError: " + errorInfo);
128 /***********************************************************************************************
131 * Handling of permanent failures is application-specific.
132 **********************************************************************************************/
136 public void ready(byte[] clientId) {
137 Log.i(TAG, "ready()");
138 exampleListenerState.setClientId(clientId);
139 writeExampleListenerState();
143 public void reissueRegistrations(byte[] clientId) {
144 Log.i(TAG, "reissueRegistrations()");
145 register(clientId, exampleListenerState.getInterestingObjects());
149 public void invalidate(Invalidation invalidation, byte[] ackHandle) {
150 Log.i(TAG, "invalidate: " + invalidation);
152 exampleListenerState.informInvalidation(invalidation.getObjectId(), invalidation.getVersion(),
153 invalidation.getPayload(), /* isBackground */ false);
154 writeExampleListenerState();
156 // Do real work here based upon the invalidation
158 acknowledge(ackHandle);
162 public void invalidateUnknownVersion(ObjectId objectId, byte[] ackHandle) {
163 Log.i(TAG, "invalidateUnknownVersion: " + objectId);
165 exampleListenerState.informUnknownVersionInvalidation(objectId);
166 writeExampleListenerState();
168 // In a real app, the application backend would need to be consulted for object state.
170 acknowledge(ackHandle);
174 public void invalidateAll(byte[] ackHandle) {
175 Log.i(TAG, "invalidateAll");
177 // Do real work here based upon the invalidation.
178 exampleListenerState.informInvalidateAll();
179 writeExampleListenerState();
181 acknowledge(ackHandle);
186 public byte[] readState() {
187 Log.i(TAG, "readState");
188 SharedPreferences sharedPreferences = getSharedPreferences();
189 String data = sharedPreferences.getString(ANDROID_LISTENER_STATE_KEY, null);
190 return (data != null) ? Base64.decode(data, Base64.DEFAULT) : null;
194 public void writeState(byte[] data) {
195 Log.i(TAG, "writeState");
196 Editor editor = getSharedPreferences().edit();
197 editor.putString(ANDROID_LISTENER_STATE_KEY, Base64.encodeToString(data, Base64.DEFAULT));
198 if (!editor.commit()) {
199 Log.e(TAG, "failed to write state"); // In a real app, this case would need to handled.
204 public void requestAuthToken(PendingIntent pendingIntent,
205 String invalidAuthToken) {
206 Log.i(TAG, "requestAuthToken");
208 // In response to requestAuthToken, we need to get an auth token and inform the invalidation
209 // client of the result through a call to setAuthToken. In this example, we block until a
210 // result is available. It is also possible to invoke setAuthToken in a callback or when
211 // handling an intent.
212 AccountManager accountManager = AccountManager.get(getApplicationContext());
214 // Invalidate the old token if necessary.
215 if (invalidAuthToken != null) {
216 accountManager.invalidateAuthToken(GOOGLE_ACCOUNT_TYPE, invalidAuthToken);
219 // Choose an (arbitrary in this example) account for which to retrieve an authentication token.
220 Account account = getAccount(accountManager);
223 // There are three possible outcomes of the call to getAuthToken:
225 // 1. Authentication failure (null result).
226 // 2. The user needs to sign in or give permission for the account. In such cases, the result
227 // includes an intent that can be started to retrieve credentials from the user.
228 // 3. The response includes the auth token, in which case we can inform the invalidation
231 // In the first case, we simply log and return. The response to such errors is application-
232 // specific. For instance, the application may prompt the user to choose another account.
234 // In the second case, we start an intent to ask for user credentials so that they are
235 // available to the application if there is a future request. An application should listen for
236 // the LOGIN_ACCOUNTS_CHANGED_ACTION broadcast intent to trigger a response to the
237 // invalidation client after the user has responded. Otherwise, it may take several minutes
238 // for the invalidation client to start.
240 // In the third case, success!, we pass the authorization token and type to the invalidation
241 // client using the setAuthToken method.
242 AccountManagerFuture<Bundle> future = accountManager.getAuthToken(account, AUTH_TYPE,
243 new Bundle(), false, null, null);
244 Bundle result = future.getResult();
245 if (result == null) {
246 // If the result is null, it means that authentication was not possible.
247 Log.w(TAG, "Auth token - getAuthToken returned null");
250 if (result.containsKey(AccountManager.KEY_INTENT)) {
251 Log.i(TAG, "Starting intent to get auth credentials");
253 // Need to start intent to get credentials.
254 Intent intent = result.getParcelable(AccountManager.KEY_INTENT);
255 int flags = intent.getFlags();
256 flags |= Intent.FLAG_ACTIVITY_NEW_TASK;
257 intent.setFlags(flags);
258 getApplicationContext().startActivity(intent);
262 Log.i(TAG, "Passing auth token to invalidation client");
263 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
264 setAuthToken(getApplicationContext(), pendingIntent, authToken, AUTH_TYPE);
265 } catch (OperationCanceledException e) {
266 Log.w(TAG, "Auth token - operation cancelled", e);
267 } catch (AuthenticatorException e) {
268 Log.w(TAG, "Auth token - authenticator exception", e);
269 } catch (IOException e) {
270 Log.w(TAG, "Auth token - IO exception", e);
274 /** Returns any Google account enabled on the device. */
275 private static Account getAccount(AccountManager accountManager) {
276 if (accountManager == null) {
277 throw new NullPointerException();
279 for (Account acct : accountManager.getAccounts()) {
280 if (GOOGLE_ACCOUNT_TYPE.equals(acct.type)) {
284 throw new RuntimeException("No google account enabled.");
288 public void informRegistrationFailure(byte[] clientId, ObjectId objectId, boolean isTransient,
289 String errorMessage) {
290 Log.e(TAG, "Registration failure!");
292 // Retry immediately on transient failures. The base AndroidListener will handle exponential
293 // backoff if there are repeated failures.
294 List<ObjectId> objectIds = new ArrayList<ObjectId>();
295 objectIds.add(objectId);
296 if (exampleListenerState.isInterestedInObject(objectId)) {
297 Log.i(TAG, "Retrying registration of " + objectId);
298 register(clientId, objectIds);
300 Log.i(TAG, "Retrying unregistration of " + objectId);
301 unregister(clientId, objectIds);
307 public void informRegistrationStatus(byte[] clientId, ObjectId objectId,
308 RegistrationState regState) {
309 Log.i(TAG, "informRegistrationStatus");
311 List<ObjectId> objectIds = new ArrayList<ObjectId>();
312 objectIds.add(objectId);
313 if (regState == RegistrationState.REGISTERED) {
314 if (!exampleListenerState.isInterestedInObject(objectId)) {
315 Log.i(TAG, "Unregistering for object we're no longer interested in");
316 unregister(clientId, objectIds);
317 writeExampleListenerState();
320 if (exampleListenerState.isInterestedInObject(objectId)) {
321 Log.i(TAG, "Registering for an object");
322 register(clientId, objectIds);
323 writeExampleListenerState();
329 protected void backgroundInvalidateForInternalUse(Iterable<Invalidation> invalidations) {
330 for (Invalidation invalidation : invalidations) {
331 Log.i(TAG, "background invalidate: " + invalidation);
332 exampleListenerState.informInvalidation(invalidation.getObjectId(), invalidation.getVersion(),
333 invalidation.getPayload(), /* isBackground */ true);
334 writeExampleListenerState();
338 /** Creates an intent that registers an interest in object invalidations for {@code objectId}. */
339 public static Intent createRegisterIntent(Context context, ObjectId objectId) {
340 return createRegistrationIntent(context, objectId, /* isRegister */ true);
343 /** Creates an intent that unregisters for invalidations for {@code objectId}. */
344 public static Intent createUnregisterIntent(Context context, ObjectId objectId) {
345 return createRegistrationIntent(context, objectId, /* isRegister */ false);
348 private static Intent createRegistrationIntent(Context context, ObjectId objectId,
349 boolean isRegister) {
350 Intent intent = new Intent();
351 intent.setAction(isRegister ? REGISTER_INTENT_ACTION : UNREGISTER_INTENT_ACTION);
352 intent.putExtra(OBJECT_ID_EXTRA, serializeObjectId(objectId));
353 intent.setClass(context, ExampleListener.class);
357 /** Creates an intent that starts the invalidation client. */
358 public static Intent createStartIntent(Context context) {
359 Intent intent = new Intent();
360 intent.setAction(START_INTENT_ACTION);
361 intent.setClass(context, ExampleListener.class);
365 /** Creates an intent that stops the invalidation client. */
366 public static Intent createStopIntent(Context context) {
367 Intent intent = new Intent();
368 intent.setAction(STOP_INTENT_ACTION);
369 intent.setClass(context, ExampleListener.class);
373 private boolean tryHandleRegistrationIntent(Intent intent) {
374 final boolean isRegister;
375 if (REGISTER_INTENT_ACTION.equals(intent.getAction())) {
377 } else if (UNREGISTER_INTENT_ACTION.equals(intent.getAction())) {
380 // Not a registration intent.
384 // Try to parse object id extra.
385 ObjectId objectId = parseObjectIdExtra(intent);
386 if (objectId == null) {
387 Log.e(TAG, "Registration intent without valid object id extra");
391 // Update example listener state.
393 exampleListenerState.addObjectOfInterest(objectId);
395 exampleListenerState.removeObjectOfInterest(objectId);
397 writeExampleListenerState();
399 // If the client is ready, perform registration now.
400 byte[] clientId = exampleListenerState.getClientId();
401 if (clientId == null) {
402 Log.i(TAG, "Deferring registration until client is ready");
404 // Perform registration immediately if we have been assigned a client id.
405 List<ObjectId> objectIds = new ArrayList<ObjectId>(1);
406 objectIds.add(objectId);
408 register(clientId, objectIds);
410 unregister(clientId, objectIds);
416 private boolean tryHandleStartIntent(Intent intent) {
417 if (START_INTENT_ACTION.equals(intent.getAction())) {
418 // Clear the client id since a new one will be provided after the client has started.
419 exampleListenerState.setClientId(null);
420 writeExampleListenerState();
422 // Setting this to true allows us to see invalidations that may suppress older invalidations.
423 // When this flag is 'false', AndroidListener#invalidateUnknownVersion is called instead of
424 // AndroidListener#invalidate when suppression has potentially occurred.
425 final boolean allowSuppression = true;
426 InvalidationClientConfig config = new InvalidationClientConfig(CLIENT_TYPE, CLIENT_NAME,
427 "ExampleListener", allowSuppression);
428 startService(AndroidListener.createStartIntent(this, config));
434 private boolean tryHandleStopIntent(Intent intent) {
435 if (STOP_INTENT_ACTION.equals(intent.getAction())) {
436 // Clear the client id since the client is no longer available.
437 exampleListenerState.setClientId(null);
438 writeExampleListenerState();
439 startService(AndroidListener.createStopIntent(this));
445 private void writeExampleListenerState() {
446 Editor editor = getSharedPreferences().edit();
447 editor.putString(EXAMPLE_LISTENER_STATE_KEY, exampleListenerState.serialize());
448 if (!editor.commit()) {
449 // In a real app, this case would need to handled.
450 Log.e(TAG, "failed to write example listener state");
452 MainActivity.State.setInfo(exampleListenerState.toString());
455 private static byte[] serializeObjectId(ObjectId objectId) {
456 return MessageNano.toByteArray(ExampleListenerState.serializeObjectId(objectId));
459 private static ObjectId parseObjectIdExtra(Intent intent) {
460 byte[] bytes = intent.getByteArrayExtra(OBJECT_ID_EXTRA);
465 ObjectIdProto proto = MessageNano.mergeFrom(new ObjectIdProto(), bytes);
466 return ExampleListenerState.deserializeObjectId(proto);
467 } catch (InvalidProtocolBufferNanoException exception) {
468 Log.e(TAG, String.format(Locale.ROOT, "Error parsing object id. error='%s'",
469 exception.getMessage()));
474 private SharedPreferences getSharedPreferences() {
475 return getApplicationContext().getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);