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.
17 package com.google.ipc.invalidation.ticl.android.c2dm;
19 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
20 import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
21 import com.google.ipc.invalidation.ticl.android.AndroidC2DMConstants;
23 import android.app.AlarmManager;
24 import android.app.IntentService;
25 import android.app.PendingIntent;
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.PackageManager.NameNotFoundException;
32 import android.content.pm.ServiceInfo;
33 import android.os.AsyncTask;
35 import java.util.HashSet;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicBoolean;
42 * Class for managing C2DM registration and dispatching of messages to observers.
44 * Requires setting the {@link #SENDER_ID_METADATA_FIELD} metadata field with the correct e-mail to
45 * be used for the C2DM registration.
47 * This is based on the open source chrometophone project.
49 public class C2DMManager extends IntentService {
51 private static final Logger logger = AndroidLogger.forTag("C2DM");
53 /** Maximum amount of time to wait for manager initialization to complete */
54 private static final long MAX_INIT_SECONDS = 30;
56 /** Timeout after which wakelocks will be automatically released. */
57 private static final int WAKELOCK_TIMEOUT_MS = 30 * 1000;
60 * The action of intents sent from the android c2dm framework regarding registration
63 public static final String REGISTRATION_CALLBACK_INTENT =
64 "com.google.android.c2dm.intent.REGISTRATION";
67 * The action of intents sent from the Android C2DM framework when we are supposed to retry
70 private static final String C2DM_RETRY = "com.google.android.c2dm.intent.RETRY";
73 * The key in the bundle to use for the sender ID when registering for C2DM.
75 * The value of the field itself must be the account that the server-side pushing messages
76 * towards the client is using when talking to C2DM.
78 private static final String EXTRA_SENDER = "sender";
81 * The key in the bundle to use for boilerplate code identifying the client application towards
82 * the Android C2DM framework
84 private static final String EXTRA_APPLICATION_PENDING_INTENT = "app";
87 * The action of intents sent to the Android C2DM framework when we want to register
89 private static final String REQUEST_UNREGISTRATION_INTENT =
90 "com.google.android.c2dm.intent.UNREGISTER";
93 * The action of intents sent to the Android C2DM framework when we want to unregister
95 private static final String REQUEST_REGISTRATION_INTENT =
96 "com.google.android.c2dm.intent.REGISTER";
99 * The package for the Google Services Framework
101 private static final String GSF_PACKAGE = "com.google.android.gsf";
104 * The action of intents sent from the Android C2DM framework when a message is received.
107 public static final String C2DM_INTENT = "com.google.android.c2dm.intent.RECEIVE";
110 * The key in the bundle to use when we want to read the C2DM registration ID after a successful
114 public static final String EXTRA_REGISTRATION_ID = "registration_id";
117 * The key in the bundle to use when we want to see if we were unregistered from C2DM
120 static final String EXTRA_UNREGISTERED = "unregistered";
123 * The key in the bundle to use when we want to see if there was any errors when we tried to
127 static final String EXTRA_ERROR = "error";
130 * The android:name we read from the meta-data for the C2DMManager service in the
131 * AndroidManifest.xml file when we want to know which sender id we should use when registering
135 static final String SENDER_ID_METADATA_FIELD = "sender_id";
138 * If {@code true}, newly-registered observers will be informed of the current registration id
139 * if one is already held. Used in service lifecycle testing to suppress inconvenient
142 public static final AtomicBoolean disableRegistrationCallbackOnRegisterForTest =
143 new AtomicBoolean(false);
146 * C2DMMManager is initialized asynchronously because it requires I/O that should not be done on
147 * the main thread. This latch will only be changed to zero once this initialization has been
148 * completed successfully. No intents should be handled or other work done until the latch
149 * reaches the initialized state.
151 private final CountDownLatch initLatch = new CountDownLatch(1);
154 * The sender ID we have read from the meta-data in AndroidManifest.xml for this service.
156 private String senderId;
159 * Observers to dispatch messages from C2DM to
161 private Set<C2DMObserver> observers;
164 * A field which is set to true whenever a C2DM registration is in progress. It is set to false
167 private boolean registrationInProcess;
170 * The context read during onCreate() which is used throughout the lifetime of this service.
172 private Context context;
175 * A field which is set to true whenever a C2DM unregistration is in progress. It is set to false
178 private boolean unregistrationInProcess;
181 * A reference to our helper service for handling WakeLocks.
183 private WakeLockManager wakeLockManager;
186 * Called from the broadcast receiver and from any observer wanting to register (observers usually
187 * go through calling C2DMessaging.register(...). Will process the received intent, call
188 * handleMessage(), onRegistered(), etc. in background threads, with a wake lock, while keeping
191 * @param context application to run service in
192 * @param intent the intent received
195 static void runIntentInService(Context context, Intent intent) {
196 // This is called from C2DMBroadcastReceiver and C2DMessaging, there is no init.
197 WakeLockManager.getInstance(context).acquire(C2DMManager.class, WAKELOCK_TIMEOUT_MS);
198 intent.setClassName(context, C2DMManager.class.getCanonicalName());
199 context.startService(intent);
202 public C2DMManager() {
203 super("C2DMManager");
204 // Always redeliver intents if evicted while processing
205 setIntentRedelivery(true);
209 public void onCreate() {
211 // Use the mock context when testing, otherwise the service application context.
212 context = getApplicationContext();
213 wakeLockManager = WakeLockManager.getInstance(context);
215 // Spawn an AsyncTask performing the blocking IO operations.
216 new AsyncTask<Void, Void, Void>() {
218 protected Void doInBackground(Void... unused) {
219 // C2DMSettings relies on SharedPreferencesImpl which performs disk access.
220 C2DMManager manager = C2DMManager.this;
221 manager.observers = C2DMSettings.getObservers(context);
222 manager.registrationInProcess = C2DMSettings.isRegistering(context);
223 manager.unregistrationInProcess = C2DMSettings.isUnregistering(context);
228 protected void onPostExecute(Void unused) {
229 logger.fine("Initialized");
230 initLatch.countDown();
234 senderId = readSenderIdFromMetaData(this);
235 if (senderId == null) {
241 public final void onHandleIntent(Intent intent) {
242 if (intent == null) {
246 // OK to block here (if needed) because IntentService guarantees that onHandleIntent will
247 // only be called on a background thread.
248 logger.fine("Handle intent = %s", intent);
249 waitForInitialization();
250 if (intent.getAction().equals(REGISTRATION_CALLBACK_INTENT)) {
251 handleRegistration(intent);
252 } else if (intent.getAction().equals(C2DM_INTENT)) {
254 } else if (intent.getAction().equals(C2DM_RETRY)) {
256 } else if (intent.getAction().equals(C2DMessaging.ACTION_REGISTER)) {
257 registerObserver(intent);
258 } else if (intent.getAction().equals(C2DMessaging.ACTION_UNREGISTER)) {
259 unregisterObserver(intent);
261 logger.warning("Received unknown action: %s", intent.getAction());
264 // Release the power lock, so device can get back to sleep.
265 // The lock is reference counted by default, so multiple
266 // messages are ok, but because sometimes Android reschedules
267 // services we need to handle the case that the wakelock should
268 // never be underlocked.
269 if (wakeLockManager.isHeld(C2DMManager.class)) {
270 wakeLockManager.release(C2DMManager.class);
275 /** Returns true of the C2DMManager is fully initially */
277 boolean isInitialized() {
278 return initLatch.getCount() == 0;
282 * Blocks until asynchronous initialization work has been completed.
284 private void waitForInitialization() {
285 boolean interrupted = false;
287 if (initLatch.await(MAX_INIT_SECONDS, TimeUnit.SECONDS)) {
290 logger.warning("Initialization timeout");
292 } catch (InterruptedException e) {
293 // Unexpected, so to ensure a consistent state wait for initialization to complete and
294 // then interrupt so higher level code can handle the interrupt.
295 logger.fine("Latch wait interrupted");
299 logger.warning("Initialization interrupted");
300 Thread.currentThread().interrupt();
304 // Either an unexpected interrupt or a timeout occurred during initialization. Set to a default
305 // clean state (no registration work in progress, no observers) and proceed.
306 observers = new HashSet<C2DMObserver>();
310 * Called when a cloud message has been received.
312 * @param intent the received intent
314 private void onMessage(Intent intent) {
315 boolean matched = false;
316 for (C2DMObserver observer : observers) {
317 if (observer.matches(intent)) {
318 Intent outgoingIntent = createOnMessageIntent(
319 observer.getObserverClass(), context, intent);
320 deliverObserverIntent(observer, outgoingIntent);
325 logger.info("No receivers matched intent: %s", intent);
330 * Returns an intent to deliver a C2DM message to {@code observerClass}.
331 * @param context Android context to use to create the intent
332 * @param intent the C2DM message intent to deliver
335 public static Intent createOnMessageIntent(Class<?> observerClass,
336 Context context, Intent intent) {
337 Intent outgoingIntent = new Intent(intent);
338 outgoingIntent.setAction(C2DMessaging.ACTION_MESSAGE);
339 outgoingIntent.setClass(context, observerClass);
340 return outgoingIntent;
344 * Called on registration error. Override to provide better error messages.
346 * This is called in the context of a Service - no dialog or UI.
348 * @param errorId the errorId String
350 private void onRegistrationError(String errorId) {
351 setRegistrationInProcess(false);
352 for (C2DMObserver observer : observers) {
353 deliverObserverIntent(observer,
354 createOnRegistrationErrorIntent(observer.getObserverClass(),
360 * Returns an intent to deliver the C2DM error {@code errorId} to {@code observerClass}.
361 * @param context Android context to use to create the intent
364 public static Intent createOnRegistrationErrorIntent(Class<?> observerClass,
365 Context context, String errorId) {
366 Intent errorIntent = new Intent(context, observerClass);
367 errorIntent.setAction(C2DMessaging.ACTION_REGISTRATION_ERROR);
368 errorIntent.putExtra(C2DMessaging.EXTRA_REGISTRATION_ERROR, errorId);
373 * Called when a registration token has been received.
375 * @param registrationId the registration ID received from C2DM
377 private void onRegistered(String registrationId) {
378 setRegistrationInProcess(false);
379 C2DMSettings.setC2DMRegistrationId(context, registrationId);
381 C2DMSettings.setApplicationVersion(context, getCurrentApplicationVersion(this));
382 } catch (NameNotFoundException e) {
383 logger.severe("Unable to find our own package name when storing application version: %s",
386 for (C2DMObserver observer : observers) {
387 onRegisteredSingleObserver(registrationId, observer);
392 * Informs the given observer about the registration ID
394 private void onRegisteredSingleObserver(String registrationId, C2DMObserver observer) {
395 if (!disableRegistrationCallbackOnRegisterForTest.get()) {
396 deliverObserverIntent(observer,
397 createOnRegisteredIntent(observer.getObserverClass(), context, registrationId));
402 * Returns an intent to deliver a new C2DM {@code registrationId} to {@code observerClass}.
403 * @param context Android context to use to create the intent
406 public static Intent createOnRegisteredIntent(Class<?> observerClass, Context context,
407 String registrationId) {
408 Intent outgoingIntent = new Intent(context, observerClass);
409 outgoingIntent.setAction(C2DMessaging.ACTION_REGISTERED);
410 outgoingIntent.putExtra(C2DMessaging.EXTRA_REGISTRATION_ID, registrationId);
411 return outgoingIntent;
415 * Called when the device has been unregistered.
417 private void onUnregistered() {
418 setUnregisteringInProcess(false);
419 C2DMSettings.clearC2DMRegistrationId(context);
420 for (C2DMObserver observer : observers) {
421 onUnregisteredSingleObserver(observer);
426 * Informs the given observer that the application is no longer registered to C2DM
428 private void onUnregisteredSingleObserver(C2DMObserver observer) {
429 Intent outgoingIntent = new Intent(context, observer.getObserverClass());
430 outgoingIntent.setAction(C2DMessaging.ACTION_UNREGISTERED);
431 deliverObserverIntent(observer, outgoingIntent);
435 * Starts the observer service by delivering it the provided intent. If the observer has asked us
436 * to get a WakeLock for it, we do that and inform the observer that the WakeLock has been
437 * acquired through the flag C2DMessaging.EXTRA_RELEASE_WAKELOCK.
439 private void deliverObserverIntent(C2DMObserver observer, Intent intent) {
440 if (observer.isHandleWakeLock()) {
441 // Set the extra so the observer knows that it needs to release the wake lock
442 intent.putExtra(C2DMessaging.EXTRA_RELEASE_WAKELOCK, true);
443 wakeLockManager.acquire(observer.getObserverClass(), WAKELOCK_TIMEOUT_MS);
445 context.startService(intent);
449 * Registers an observer.
451 * If this was the first observer we also start registering towards C2DM. If we were already
452 * registered, we do a callback to inform about the current C2DM registration ID.
454 * <p>We also start a registration if the application version stored does not match the
455 * current version number. This leads to any observer registering after an upgrade will trigger
456 * a new C2DM registration.
458 private void registerObserver(Intent intent) {
459 C2DMObserver observer = C2DMObserver.createFromIntent(intent);
460 observers.add(observer);
461 C2DMSettings.setObservers(context, observers);
462 if (C2DMSettings.hasC2DMRegistrationId(context)) {
463 onRegisteredSingleObserver(C2DMSettings.getC2DMRegistrationId(context), observer);
464 if (!isApplicationVersionCurrent() && !isRegistrationInProcess()) {
465 logger.fine("Registering to C2DM since application version is not current.");
469 if (!isRegistrationInProcess()) {
470 logger.fine("Registering to C2DM since we have no C2DM registration.");
477 * Unregisters an observer.
479 * The observer is moved to unregisteringObservers which only gets messages from C2DMManager if
480 * we unregister from C2DM completely. If this was the last observer, we also start the process of
481 * unregistering from C2DM.
483 private void unregisterObserver(Intent intent) {
484 C2DMObserver observer = C2DMObserver.createFromIntent(intent);
485 if (observers.remove(observer)) {
486 C2DMSettings.setObservers(context, observers);
487 onUnregisteredSingleObserver(observer);
489 if (observers.isEmpty()) {
490 // No more observers, need to unregister
491 if (!isUnregisteringInProcess()) {
498 * Called when the Android C2DM framework sends us a message regarding registration.
500 * This method parses the intent from the Android C2DM framework and calls the appropriate
501 * methods for when we are registered, unregistered or if there was an error when trying to
504 private void handleRegistration(Intent intent) {
505 String registrationId = intent.getStringExtra(EXTRA_REGISTRATION_ID);
506 String error = intent.getStringExtra(EXTRA_ERROR);
507 String removed = intent.getStringExtra(EXTRA_UNREGISTERED);
508 logger.fine("Got registration message: registrationId = %s, error = %s, removed = %s",
509 registrationId, error, removed);
510 if (removed != null) {
512 } else if (error != null) {
513 handleRegistrationBackoffOnError(error);
515 handleRegistration(registrationId);
520 * Informs observers about a registration error, and schedules a registration retry if the error
523 private void handleRegistrationBackoffOnError(String error) {
524 logger.severe("Registration error %s", error);
525 onRegistrationError(error);
526 if (C2DMessaging.ERR_SERVICE_NOT_AVAILABLE.equals(error)) {
527 long backoffTimeMs = C2DMSettings.getBackoff(context);
528 createAlarm(backoffTimeMs);
529 increaseBackoff(backoffTimeMs);
534 * When C2DM registration fails, we call this method to schedule a retry in the future.
536 private void createAlarm(long backoffTimeMs) {
537 logger.fine("Scheduling registration retry, backoff = %d", backoffTimeMs);
538 Intent retryIntent = new Intent(C2DM_RETRY);
539 PendingIntent retryPIntent = PendingIntent.getBroadcast(context, 0, retryIntent, 0);
540 AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
541 am.set(AlarmManager.ELAPSED_REALTIME, backoffTimeMs, retryPIntent);
545 * Increases the backoff time for retrying C2DM registration
547 private void increaseBackoff(long backoffTimeMs) {
549 C2DMSettings.setBackoff(context, backoffTimeMs);
553 * When C2DM registration is complete, this method resets the backoff and makes sure all observers
556 private void handleRegistration(String registrationId) {
557 C2DMSettings.resetBackoff(context);
558 onRegistered(registrationId);
561 private void setRegistrationInProcess(boolean registrationInProcess) {
562 C2DMSettings.setRegistering(context, registrationInProcess);
563 this.registrationInProcess = registrationInProcess;
566 private boolean isRegistrationInProcess() {
567 return registrationInProcess;
570 private void setUnregisteringInProcess(boolean unregisteringInProcess) {
571 C2DMSettings.setUnregistering(context, unregisteringInProcess);
572 this.unregistrationInProcess = unregisteringInProcess;
575 private boolean isUnregisteringInProcess() {
576 return unregistrationInProcess;
580 * Initiate c2d messaging registration for the current application
582 private void register() {
583 Intent registrationIntent = new Intent(REQUEST_REGISTRATION_INTENT);
584 registrationIntent.setPackage(GSF_PACKAGE);
585 registrationIntent.putExtra(
586 EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0));
587 registrationIntent.putExtra(EXTRA_SENDER, senderId);
588 setRegistrationInProcess(true);
589 context.startService(registrationIntent);
593 * Unregister the application. New messages will be blocked by server.
595 private void unregister() {
596 Intent regIntent = new Intent(REQUEST_UNREGISTRATION_INTENT);
597 regIntent.setPackage(GSF_PACKAGE);
599 EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0));
600 setUnregisteringInProcess(true);
601 context.startService(regIntent);
605 * Checks if the stored application version is the same as the current application version.
607 private boolean isApplicationVersionCurrent() {
609 String currentApplicationVersion = getCurrentApplicationVersion(this);
610 if (currentApplicationVersion == null) {
613 return currentApplicationVersion.equals(C2DMSettings.getApplicationVersion(context));
614 } catch (NameNotFoundException e) {
615 logger.fine("Unable to find our own package name when reading application version: %s",
622 * Retrieves the current application version.
625 public static String getCurrentApplicationVersion(Context context) throws NameNotFoundException {
626 PackageInfo packageInfo =
627 context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
628 return packageInfo.versionName;
632 * Reads the meta-data to find the field specified in SENDER_ID_METADATA_FIELD. The value of that
633 * field is used when registering towards C2DM. If no value is found,
634 * {@link AndroidC2DMConstants#SENDER_ID} is returned.
636 static String readSenderIdFromMetaData(Context context) {
637 String senderId = AndroidC2DMConstants.SENDER_ID;
639 ServiceInfo serviceInfo = context.getPackageManager().getServiceInfo(
640 new ComponentName(context, C2DMManager.class), PackageManager.GET_META_DATA);
641 if (serviceInfo.metaData != null) {
642 String manifestSenderId = serviceInfo.metaData.getString(SENDER_ID_METADATA_FIELD);
643 if (manifestSenderId != null) {
644 logger.fine("Using manifest-specified sender-id: %s", manifestSenderId);
645 senderId = manifestSenderId;
647 logger.severe("No meta-data element with the name %s found on the service declaration",
648 SENDER_ID_METADATA_FIELD);
651 } catch (NameNotFoundException exception) {
652 logger.info("Could not find C2DMManager service info in manifest");