/* * 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.ticl.android.c2dm; import com.google.ipc.invalidation.external.client.SystemResources.Logger; import com.google.ipc.invalidation.external.client.android.service.AndroidLogger; import com.google.ipc.invalidation.ticl.android.AndroidC2DMConstants; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; import android.os.AsyncTask; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * Class for managing C2DM registration and dispatching of messages to observers. * * Requires setting the {@link #SENDER_ID_METADATA_FIELD} metadata field with the correct e-mail to * be used for the C2DM registration. * * This is based on the open source chrometophone project. */ public class C2DMManager extends IntentService { private static final Logger logger = AndroidLogger.forTag("C2DM"); /** Maximum amount of time to wait for manager initialization to complete */ private static final long MAX_INIT_SECONDS = 30; /** Timeout after which wakelocks will be automatically released. */ private static final int WAKELOCK_TIMEOUT_MS = 30 * 1000; /** * The action of intents sent from the android c2dm framework regarding registration */ public static final String REGISTRATION_CALLBACK_INTENT = "com.google.android.c2dm.intent.REGISTRATION"; /** * The action of intents sent from the Android C2DM framework when we are supposed to retry * registration. */ private static final String C2DM_RETRY = "com.google.android.c2dm.intent.RETRY"; /** * The key in the bundle to use for the sender ID when registering for C2DM. * * The value of the field itself must be the account that the server-side pushing messages * towards the client is using when talking to C2DM. */ private static final String EXTRA_SENDER = "sender"; /** * The key in the bundle to use for boilerplate code identifying the client application towards * the Android C2DM framework */ private static final String EXTRA_APPLICATION_PENDING_INTENT = "app"; /** * The action of intents sent to the Android C2DM framework when we want to register */ private static final String REQUEST_UNREGISTRATION_INTENT = "com.google.android.c2dm.intent.UNREGISTER"; /** * The action of intents sent to the Android C2DM framework when we want to unregister */ private static final String REQUEST_REGISTRATION_INTENT = "com.google.android.c2dm.intent.REGISTER"; /** * The package for the Google Services Framework */ private static final String GSF_PACKAGE = "com.google.android.gsf"; /** * The action of intents sent from the Android C2DM framework when a message is received. */ public static final String C2DM_INTENT = "com.google.android.c2dm.intent.RECEIVE"; /** * The key in the bundle to use when we want to read the C2DM registration ID after a successful * registration */ public static final String EXTRA_REGISTRATION_ID = "registration_id"; /** * The key in the bundle to use when we want to see if we were unregistered from C2DM */ static final String EXTRA_UNREGISTERED = "unregistered"; /** * The key in the bundle to use when we want to see if there was any errors when we tried to * register. */ static final String EXTRA_ERROR = "error"; /** * The android:name we read from the meta-data for the C2DMManager service in the * AndroidManifest.xml file when we want to know which sender id we should use when registering * towards C2DM */ static final String SENDER_ID_METADATA_FIELD = "sender_id"; /** * If {@code true}, newly-registered observers will be informed of the current registration id * if one is already held. Used in service lifecycle testing to suppress inconvenient * events. */ public static final AtomicBoolean disableRegistrationCallbackOnRegisterForTest = new AtomicBoolean(false); /** * C2DMMManager is initialized asynchronously because it requires I/O that should not be done on * the main thread. This latch will only be changed to zero once this initialization has been * completed successfully. No intents should be handled or other work done until the latch * reaches the initialized state. */ private final CountDownLatch initLatch = new CountDownLatch(1); /** * The sender ID we have read from the meta-data in AndroidManifest.xml for this service. */ private String senderId; /** * Observers to dispatch messages from C2DM to */ private Set<C2DMObserver> observers; /** * A field which is set to true whenever a C2DM registration is in progress. It is set to false * otherwise. */ private boolean registrationInProcess; /** * The context read during onCreate() which is used throughout the lifetime of this service. */ private Context context; /** * A field which is set to true whenever a C2DM unregistration is in progress. It is set to false * otherwise. */ private boolean unregistrationInProcess; /** * A reference to our helper service for handling WakeLocks. */ private WakeLockManager wakeLockManager; /** * Called from the broadcast receiver and from any observer wanting to register (observers usually * go through calling C2DMessaging.register(...). Will process the received intent, call * handleMessage(), onRegistered(), etc. in background threads, with a wake lock, while keeping * the service alive. * * @param context application to run service in * @param intent the intent received */ static void runIntentInService(Context context, Intent intent) { // This is called from C2DMBroadcastReceiver and C2DMessaging, there is no init. WakeLockManager.getInstance(context).acquire(C2DMManager.class, WAKELOCK_TIMEOUT_MS); intent.setClassName(context, C2DMManager.class.getCanonicalName()); context.startService(intent); } public C2DMManager() { super("C2DMManager"); // Always redeliver intents if evicted while processing setIntentRedelivery(true); } @Override public void onCreate() { super.onCreate(); // Use the mock context when testing, otherwise the service application context. context = getApplicationContext(); wakeLockManager = WakeLockManager.getInstance(context); // Spawn an AsyncTask performing the blocking IO operations. new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... unused) { // C2DMSettings relies on SharedPreferencesImpl which performs disk access. C2DMManager manager = C2DMManager.this; manager.observers = C2DMSettings.getObservers(context); manager.registrationInProcess = C2DMSettings.isRegistering(context); manager.unregistrationInProcess = C2DMSettings.isUnregistering(context); return null; } @Override protected void onPostExecute(Void unused) { logger.fine("Initialized"); initLatch.countDown(); } }.execute(); senderId = readSenderIdFromMetaData(this); if (senderId == null) { stopSelf(); } } @Override public final void onHandleIntent(Intent intent) { try { // OK to block here (if needed) because IntentService guarantees that onHandleIntent will // only be called on a background thread. logger.fine("Handle intent = %s", intent); waitForInitialization(); if (intent.getAction().equals(REGISTRATION_CALLBACK_INTENT)) { handleRegistration(intent); } else if (intent.getAction().equals(C2DM_INTENT)) { onMessage(intent); } else if (intent.getAction().equals(C2DM_RETRY)) { register(); } else if (intent.getAction().equals(C2DMessaging.ACTION_REGISTER)) { registerObserver(intent); } else if (intent.getAction().equals(C2DMessaging.ACTION_UNREGISTER)) { unregisterObserver(intent); } else { logger.warning("Receieved unknown action: %s", intent.getAction()); } } finally { // Release the power lock, so device can get back to sleep. // The lock is reference counted by default, so multiple // messages are ok, but because sometimes Android reschedules // services we need to handle the case that the wakelock should // never be underlocked. if (wakeLockManager.isHeld(C2DMManager.class)) { wakeLockManager.release(C2DMManager.class); } } } /** Returns true of the C2DMManager is fully initially */ boolean isInitialized() { return initLatch.getCount() == 0; } /** * Blocks until asynchronous initialization work has been completed. */ private void waitForInitialization() { boolean interrupted = false; try { if (initLatch.await(MAX_INIT_SECONDS, TimeUnit.SECONDS)) { return; } logger.warning("Initialization timeout"); } catch (InterruptedException e) { // Unexpected, so to ensure a consistent state wait for initialization to complete and // then interrupt so higher level code can handle the interrupt. logger.fine("Latch wait interrupted"); interrupted = true; } finally { if (interrupted) { logger.warning("Initialization interrupted"); Thread.currentThread().interrupt(); } } // Either an unexpected interrupt or a timeout occurred during initialization. Set to a default // clean state (no registration work in progress, no observers) and proceed. observers = new HashSet<C2DMObserver>(); } /** * Called when a cloud message has been received. * * @param intent the received intent */ private void onMessage(Intent intent) { boolean matched = false; for (C2DMObserver observer : observers) { if (observer.matches(intent)) { Intent outgoingIntent = createOnMessageIntent( observer.getObserverClass(), context, intent); deliverObserverIntent(observer, outgoingIntent); matched = true; } } if (!matched) { logger.info("No receivers matched intent: %s", intent); } } /** * Returns an intent to deliver a C2DM message to {@code observerClass}. * @param context Android context to use to create the intent * @param intent the C2DM message intent to deliver */ public static Intent createOnMessageIntent(Class<?> observerClass, Context context, Intent intent) { Intent outgoingIntent = new Intent(intent); outgoingIntent.setAction(C2DMessaging.ACTION_MESSAGE); outgoingIntent.setClass(context, observerClass); return outgoingIntent; } /** * Called on registration error. Override to provide better error messages. * * This is called in the context of a Service - no dialog or UI. * * @param errorId the errorId String */ private void onRegistrationError(String errorId) { setRegistrationInProcess(false); for (C2DMObserver observer : observers) { deliverObserverIntent(observer, createOnRegistrationErrorIntent(observer.getObserverClass(), context, errorId)); } } /** * Returns an intent to deliver the C2DM error {@code errorId} to {@code observerClass}. * @param context Android context to use to create the intent */ public static Intent createOnRegistrationErrorIntent(Class<?> observerClass, Context context, String errorId) { Intent errorIntent = new Intent(context, observerClass); errorIntent.setAction(C2DMessaging.ACTION_REGISTRATION_ERROR); errorIntent.putExtra(C2DMessaging.EXTRA_REGISTRATION_ERROR, errorId); return errorIntent; } /** * Called when a registration token has been received. * * @param registrationId the registration ID received from C2DM */ private void onRegistered(String registrationId) { setRegistrationInProcess(false); C2DMSettings.setC2DMRegistrationId(context, registrationId); try { C2DMSettings.setApplicationVersion(context, getCurrentApplicationVersion(this)); } catch (NameNotFoundException e) { logger.severe("Unable to find our own package name when storing application version: %s", e.getMessage()); } for (C2DMObserver observer : observers) { onRegisteredSingleObserver(registrationId, observer); } } /** * Informs the given observer about the registration ID */ private void onRegisteredSingleObserver(String registrationId, C2DMObserver observer) { if (!disableRegistrationCallbackOnRegisterForTest.get()) { deliverObserverIntent(observer, createOnRegisteredIntent(observer.getObserverClass(), context, registrationId)); } } /** * Returns an intent to deliver a new C2DM {@code registrationId} to {@code observerClass}. * @param context Android context to use to create the intent */ public static Intent createOnRegisteredIntent(Class<?> observerClass, Context context, String registrationId) { Intent outgoingIntent = new Intent(context, observerClass); outgoingIntent.setAction(C2DMessaging.ACTION_REGISTERED); outgoingIntent.putExtra(C2DMessaging.EXTRA_REGISTRATION_ID, registrationId); return outgoingIntent; } /** * Called when the device has been unregistered. */ private void onUnregistered() { setUnregisteringInProcess(false); C2DMSettings.clearC2DMRegistrationId(context); for (C2DMObserver observer : observers) { onUnregisteredSingleObserver(observer); } } /** * Informs the given observer that the application is no longer registered to C2DM */ private void onUnregisteredSingleObserver(C2DMObserver observer) { Intent outgoingIntent = new Intent(context, observer.getObserverClass()); outgoingIntent.setAction(C2DMessaging.ACTION_UNREGISTERED); deliverObserverIntent(observer, outgoingIntent); } /** * Starts the observer service by delivering it the provided intent. If the observer has asked us * to get a WakeLock for it, we do that and inform the observer that the WakeLock has been * acquired through the flag C2DMessaging.EXTRA_RELEASE_WAKELOCK. */ private void deliverObserverIntent(C2DMObserver observer, Intent intent) { if (observer.isHandleWakeLock()) { // Set the extra so the observer knows that it needs to release the wake lock intent.putExtra(C2DMessaging.EXTRA_RELEASE_WAKELOCK, true); wakeLockManager.acquire(observer.getObserverClass(), WAKELOCK_TIMEOUT_MS); } context.startService(intent); } /** * Registers an observer. * * If this was the first observer we also start registering towards C2DM. If we were already * registered, we do a callback to inform about the current C2DM registration ID. * * <p>We also start a registration if the application version stored does not match the * current version number. This leads to any observer registering after an upgrade will trigger * a new C2DM registration. */ private void registerObserver(Intent intent) { C2DMObserver observer = C2DMObserver.createFromIntent(intent); observers.add(observer); C2DMSettings.setObservers(context, observers); if (C2DMSettings.hasC2DMRegistrationId(context)) { onRegisteredSingleObserver(C2DMSettings.getC2DMRegistrationId(context), observer); if (!isApplicationVersionCurrent() && !isRegistrationInProcess()) { logger.fine("Registering to C2DM since application version is not current."); register(); } } else { if (!isRegistrationInProcess()) { logger.fine("Registering to C2DM since we have no C2DM registration."); register(); } } } /** * Unregisters an observer. * * The observer is moved to unregisteringObservers which only gets messages from C2DMManager if * we unregister from C2DM completely. If this was the last observer, we also start the process of * unregistering from C2DM. */ private void unregisterObserver(Intent intent) { C2DMObserver observer = C2DMObserver.createFromIntent(intent); if (observers.remove(observer)) { C2DMSettings.setObservers(context, observers); onUnregisteredSingleObserver(observer); } if (observers.isEmpty()) { // No more observers, need to unregister if (!isUnregisteringInProcess()) { unregister(); } } } /** * Called when the Android C2DM framework sends us a message regarding registration. * * This method parses the intent from the Android C2DM framework and calls the appropriate * methods for when we are registered, unregistered or if there was an error when trying to * register. */ private void handleRegistration(Intent intent) { String registrationId = intent.getStringExtra(EXTRA_REGISTRATION_ID); String error = intent.getStringExtra(EXTRA_ERROR); String removed = intent.getStringExtra(EXTRA_UNREGISTERED); logger.fine("Got registration message: registrationId = %s, error = %s, removed = %s", registrationId, error, removed); if (removed != null) { onUnregistered(); } else if (error != null) { handleRegistrationBackoffOnError(error); } else { handleRegistration(registrationId); } } /** * Informs observers about a registration error, and schedules a registration retry if the error * was transient. */ private void handleRegistrationBackoffOnError(String error) { logger.severe("Registration error %s", error); onRegistrationError(error); if (C2DMessaging.ERR_SERVICE_NOT_AVAILABLE.equals(error)) { long backoffTimeMs = C2DMSettings.getBackoff(context); createAlarm(backoffTimeMs); increaseBackoff(backoffTimeMs); } } /** * When C2DM registration fails, we call this method to schedule a retry in the future. */ private void createAlarm(long backoffTimeMs) { logger.fine("Scheduling registration retry, backoff = %d", backoffTimeMs); Intent retryIntent = new Intent(C2DM_RETRY); PendingIntent retryPIntent = PendingIntent.getBroadcast(context, 0, retryIntent, 0); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.set(AlarmManager.ELAPSED_REALTIME, backoffTimeMs, retryPIntent); } /** * Increases the backoff time for retrying C2DM registration */ private void increaseBackoff(long backoffTimeMs) { backoffTimeMs *= 2; C2DMSettings.setBackoff(context, backoffTimeMs); } /** * When C2DM registration is complete, this method resets the backoff and makes sure all observers * are informed */ private void handleRegistration(String registrationId) { C2DMSettings.resetBackoff(context); onRegistered(registrationId); } private void setRegistrationInProcess(boolean registrationInProcess) { C2DMSettings.setRegistering(context, registrationInProcess); this.registrationInProcess = registrationInProcess; } private boolean isRegistrationInProcess() { return registrationInProcess; } private void setUnregisteringInProcess(boolean unregisteringInProcess) { C2DMSettings.setUnregistering(context, unregisteringInProcess); this.unregistrationInProcess = unregisteringInProcess; } private boolean isUnregisteringInProcess() { return unregistrationInProcess; } /** * Initiate c2d messaging registration for the current application */ private void register() { Intent registrationIntent = new Intent(REQUEST_REGISTRATION_INTENT); registrationIntent.setPackage(GSF_PACKAGE); registrationIntent.putExtra( EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0)); registrationIntent.putExtra(EXTRA_SENDER, senderId); setRegistrationInProcess(true); context.startService(registrationIntent); } /** * Unregister the application. New messages will be blocked by server. */ private void unregister() { Intent regIntent = new Intent(REQUEST_UNREGISTRATION_INTENT); regIntent.setPackage(GSF_PACKAGE); regIntent.putExtra( EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0)); setUnregisteringInProcess(true); context.startService(regIntent); } /** * Checks if the stored application version is the same as the current application version. */ private boolean isApplicationVersionCurrent() { try { String currentApplicationVersion = getCurrentApplicationVersion(this); if (currentApplicationVersion == null) { return false; } return currentApplicationVersion.equals(C2DMSettings.getApplicationVersion(context)); } catch (NameNotFoundException e) { logger.fine("Unable to find our own package name when reading application version: %s", e.getMessage()); return false; } } /** * Retrieves the current application version. */ public static String getCurrentApplicationVersion(Context context) throws NameNotFoundException { PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return packageInfo.versionName; } /** * Reads the meta-data to find the field specified in SENDER_ID_METADATA_FIELD. The value of that * field is used when registering towards C2DM. If no value is found, * {@link AndroidC2DMConstants#SENDER_ID} is returned. */ static String readSenderIdFromMetaData(Context context) { String senderId = AndroidC2DMConstants.SENDER_ID; try { ServiceInfo serviceInfo = context.getPackageManager().getServiceInfo( new ComponentName(context, C2DMManager.class), PackageManager.GET_META_DATA); if (serviceInfo.metaData != null) { String manifestSenderId = serviceInfo.metaData.getString(SENDER_ID_METADATA_FIELD); if (manifestSenderId != null) { logger.fine("Using manifest-specified sender-id: %s", manifestSenderId); senderId = manifestSenderId; } else { logger.severe("No meta-data element with the name %s found on the service declaration", SENDER_ID_METADATA_FIELD); } } } catch (NameNotFoundException exception) { logger.info("Could not find C2DMManager service info in manifest"); } return senderId; } }