Upstream version 10.38.222.0
[platform/framework/web/crosswalk.git] / src / third_party / cacheinvalidation / src / java / com / google / ipc / invalidation / ticl / android / c2dm / C2DMManager.java
1 /*
2  * Copyright 2011 Google Inc.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.google.ipc.invalidation.ticl.android.c2dm;
18
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;
22
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;
34
35 import java.util.HashSet;
36 import java.util.Set;
37 import java.util.concurrent.CountDownLatch;
38 import java.util.concurrent.TimeUnit;
39 import java.util.concurrent.atomic.AtomicBoolean;
40
41 /**
42  * Class for managing C2DM registration and dispatching of messages to observers.
43  *
44  * Requires setting the {@link #SENDER_ID_METADATA_FIELD} metadata field with the correct e-mail to
45  * be used for the C2DM registration.
46  *
47  * This is based on the open source chrometophone project.
48  */
49 public class C2DMManager extends IntentService {
50
51   private static final Logger logger = AndroidLogger.forTag("C2DM");
52
53   /** Maximum amount of time to wait for manager initialization to complete */
54   private static final long MAX_INIT_SECONDS = 30;
55
56   /** Timeout after which wakelocks will be automatically released. */
57   private static final int WAKELOCK_TIMEOUT_MS = 30 * 1000;
58
59   /**
60    * The action of intents sent from the android c2dm framework regarding registration
61    */
62   
63   public static final String REGISTRATION_CALLBACK_INTENT =
64       "com.google.android.c2dm.intent.REGISTRATION";
65
66   /**
67    * The action of intents sent from the Android C2DM framework when we are supposed to retry
68    * registration.
69    */
70   private static final String C2DM_RETRY = "com.google.android.c2dm.intent.RETRY";
71
72   /**
73    * The key in the bundle to use for the sender ID when registering for C2DM.
74    *
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.
77    */
78   private static final String EXTRA_SENDER = "sender";
79
80   /**
81    * The key in the bundle to use for boilerplate code identifying the client application towards
82    * the Android C2DM framework
83    */
84   private static final String EXTRA_APPLICATION_PENDING_INTENT = "app";
85
86   /**
87    * The action of intents sent to the Android C2DM framework when we want to register
88    */
89   private static final String REQUEST_UNREGISTRATION_INTENT =
90       "com.google.android.c2dm.intent.UNREGISTER";
91
92   /**
93    * The action of intents sent to the Android C2DM framework when we want to unregister
94    */
95   private static final String REQUEST_REGISTRATION_INTENT =
96       "com.google.android.c2dm.intent.REGISTER";
97
98   /**
99    * The package for the Google Services Framework
100    */
101   private static final String GSF_PACKAGE = "com.google.android.gsf";
102
103   /**
104    * The action of intents sent from the Android C2DM framework when a message is received.
105    */
106   
107   public static final String C2DM_INTENT = "com.google.android.c2dm.intent.RECEIVE";
108
109   /**
110    * The key in the bundle to use when we want to read the C2DM registration ID after a successful
111    * registration
112    */
113   
114   public static final String EXTRA_REGISTRATION_ID = "registration_id";
115
116   /**
117    * The key in the bundle to use when we want to see if we were unregistered from C2DM
118    */
119   
120   static final String EXTRA_UNREGISTERED = "unregistered";
121
122   /**
123    * The key in the bundle to use when we want to see if there was any errors when we tried to
124    * register.
125    */
126   
127   static final String EXTRA_ERROR = "error";
128
129   /**
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
132    * towards C2DM
133    */
134   
135   static final String SENDER_ID_METADATA_FIELD = "sender_id";
136
137   /**
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
140    * events.
141    */
142   public static final AtomicBoolean disableRegistrationCallbackOnRegisterForTest =
143       new AtomicBoolean(false);
144
145   /**
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.
150    */
151   private final CountDownLatch initLatch = new CountDownLatch(1);
152
153   /**
154    * The sender ID we have read from the meta-data in AndroidManifest.xml for this service.
155    */
156   private String senderId;
157
158   /**
159    * Observers to dispatch messages from C2DM to
160    */
161   private Set<C2DMObserver> observers;
162
163   /**
164    * A field which is set to true whenever a C2DM registration is in progress. It is set to false
165    * otherwise.
166    */
167   private boolean registrationInProcess;
168
169   /**
170    * The context read during onCreate() which is used throughout the lifetime of this service.
171    */
172   private Context context;
173
174   /**
175    * A field which is set to true whenever a C2DM unregistration is in progress. It is set to false
176    * otherwise.
177    */
178   private boolean unregistrationInProcess;
179
180   /**
181    * A reference to our helper service for handling WakeLocks.
182    */
183   private WakeLockManager wakeLockManager;
184
185   /**
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
189    * the service alive.
190    *
191    * @param context application to run service in
192    * @param intent the intent received
193    */
194   
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);
200   }
201
202   public C2DMManager() {
203     super("C2DMManager");
204     // Always redeliver intents if evicted while processing
205     setIntentRedelivery(true);
206   }
207
208   @Override
209   public void onCreate() {
210     super.onCreate();
211     // Use the mock context when testing, otherwise the service application context.
212     context = getApplicationContext();
213     wakeLockManager = WakeLockManager.getInstance(context);
214
215     // Spawn an AsyncTask performing the blocking IO operations.
216     new AsyncTask<Void, Void, Void>() {
217       @Override
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);
224         return null;
225       }
226
227       @Override
228       protected void onPostExecute(Void unused) {
229         logger.fine("Initialized");
230         initLatch.countDown();
231       }
232     }.execute();
233
234     senderId = readSenderIdFromMetaData(this);
235     if (senderId == null) {
236       stopSelf();
237     }
238   }
239
240   @Override
241   public final void onHandleIntent(Intent intent) {
242     if (intent == null) {
243       return;
244     }
245     try {
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)) {
253         onMessage(intent);
254       } else if (intent.getAction().equals(C2DM_RETRY)) {
255         register();
256       } else if (intent.getAction().equals(C2DMessaging.ACTION_REGISTER)) {
257         registerObserver(intent);
258       } else if (intent.getAction().equals(C2DMessaging.ACTION_UNREGISTER)) {
259         unregisterObserver(intent);
260       } else {
261         logger.warning("Received unknown action: %s", intent.getAction());
262       }
263     } finally {
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);
271       }
272     }
273   }
274
275   /** Returns true of the C2DMManager is fully initially */
276   
277   boolean isInitialized() {
278     return initLatch.getCount() == 0;
279   }
280
281   /**
282    * Blocks until asynchronous initialization work has been completed.
283    */
284   private void waitForInitialization() {
285     boolean interrupted = false;
286     try {
287       if (initLatch.await(MAX_INIT_SECONDS, TimeUnit.SECONDS)) {
288         return;
289       }
290       logger.warning("Initialization timeout");
291
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");
296       interrupted = true;
297     } finally {
298       if (interrupted) {
299         logger.warning("Initialization interrupted");
300         Thread.currentThread().interrupt();
301       }
302     }
303
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>();
307   }
308
309   /**
310    * Called when a cloud message has been received.
311    *
312    * @param intent the received intent
313    */
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);
321         matched = true;
322       }
323     }
324     if (!matched) {
325       logger.info("No receivers matched intent: %s", intent);
326     }
327   }
328
329   /**
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
333    */
334   
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;
341   }
342
343   /**
344    * Called on registration error. Override to provide better error messages.
345    *
346    * This is called in the context of a Service - no dialog or UI.
347    *
348    * @param errorId the errorId String
349    */
350   private void onRegistrationError(String errorId) {
351     setRegistrationInProcess(false);
352     for (C2DMObserver observer : observers) {
353       deliverObserverIntent(observer,
354           createOnRegistrationErrorIntent(observer.getObserverClass(),
355               context, errorId));
356     }
357   }
358
359   /**
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
362    */
363   
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);
369     return errorIntent;
370   }
371
372   /**
373    * Called when a registration token has been received.
374    *
375    * @param registrationId the registration ID received from C2DM
376    */
377   private void onRegistered(String registrationId) {
378     setRegistrationInProcess(false);
379     C2DMSettings.setC2DMRegistrationId(context, registrationId);
380     try {
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",
384           e.getMessage());
385     }
386     for (C2DMObserver observer : observers) {
387       onRegisteredSingleObserver(registrationId, observer);
388     }
389   }
390
391   /**
392    * Informs the given observer about the registration ID
393    */
394   private void onRegisteredSingleObserver(String registrationId, C2DMObserver observer) {
395     if (!disableRegistrationCallbackOnRegisterForTest.get()) {
396       deliverObserverIntent(observer,
397           createOnRegisteredIntent(observer.getObserverClass(), context, registrationId));
398     }
399   }
400
401   /**
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
404    */
405   
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;
412   }
413
414   /**
415    * Called when the device has been unregistered.
416    */
417   private void onUnregistered() {
418     setUnregisteringInProcess(false);
419     C2DMSettings.clearC2DMRegistrationId(context);
420     for (C2DMObserver observer : observers) {
421       onUnregisteredSingleObserver(observer);
422     }
423   }
424
425   /**
426    * Informs the given observer that the application is no longer registered to C2DM
427    */
428   private void onUnregisteredSingleObserver(C2DMObserver observer) {
429     Intent outgoingIntent = new Intent(context, observer.getObserverClass());
430     outgoingIntent.setAction(C2DMessaging.ACTION_UNREGISTERED);
431     deliverObserverIntent(observer, outgoingIntent);
432   }
433
434   /**
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.
438    */
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);
444     }
445     context.startService(intent);
446   }
447
448   /**
449    * Registers an observer.
450    *
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.
453    *
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.
457    */
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.");
466         register();
467       }
468     } else {
469       if (!isRegistrationInProcess()) {
470         logger.fine("Registering to C2DM since we have no C2DM registration.");
471         register();
472       }
473     }
474   }
475
476   /**
477    * Unregisters an observer.
478    *
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.
482    */
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);
488     }
489     if (observers.isEmpty()) {
490       // No more observers, need to unregister
491       if (!isUnregisteringInProcess()) {
492         unregister();
493       }
494     }
495   }
496
497   /**
498    * Called when the Android C2DM framework sends us a message regarding registration.
499    *
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
502    * register.
503    */
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) {
511       onUnregistered();
512     } else if (error != null) {
513       handleRegistrationBackoffOnError(error);
514     } else {
515       handleRegistration(registrationId);
516     }
517   }
518
519   /**
520    * Informs observers about a registration error, and schedules a registration retry if the error
521    * was transient.
522    */
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);
530     }
531   }
532
533   /**
534    * When C2DM registration fails, we call this method to schedule a retry in the future.
535    */
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);
542   }
543
544   /**
545    * Increases the backoff time for retrying C2DM registration
546    */
547   private void increaseBackoff(long backoffTimeMs) {
548     backoffTimeMs *= 2;
549     C2DMSettings.setBackoff(context, backoffTimeMs);
550   }
551
552   /**
553    * When C2DM registration is complete, this method resets the backoff and makes sure all observers
554    * are informed
555    */
556   private void handleRegistration(String registrationId) {
557     C2DMSettings.resetBackoff(context);
558     onRegistered(registrationId);
559   }
560
561   private void setRegistrationInProcess(boolean registrationInProcess) {
562     C2DMSettings.setRegistering(context, registrationInProcess);
563     this.registrationInProcess = registrationInProcess;
564   }
565
566   private boolean isRegistrationInProcess() {
567     return registrationInProcess;
568   }
569
570   private void setUnregisteringInProcess(boolean unregisteringInProcess) {
571     C2DMSettings.setUnregistering(context, unregisteringInProcess);
572     this.unregistrationInProcess = unregisteringInProcess;
573   }
574
575   private boolean isUnregisteringInProcess() {
576     return unregistrationInProcess;
577   }
578
579   /**
580    * Initiate c2d messaging registration for the current application
581    */
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);
590   }
591
592   /**
593    * Unregister the application. New messages will be blocked by server.
594    */
595   private void unregister() {
596     Intent regIntent = new Intent(REQUEST_UNREGISTRATION_INTENT);
597     regIntent.setPackage(GSF_PACKAGE);
598     regIntent.putExtra(
599         EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0));
600     setUnregisteringInProcess(true);
601     context.startService(regIntent);
602   }
603
604   /**
605    * Checks if the stored application version is the same as the current application version.
606    */
607   private boolean isApplicationVersionCurrent() {
608     try {
609       String currentApplicationVersion = getCurrentApplicationVersion(this);
610       if (currentApplicationVersion == null) {
611         return false;
612       }
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",
616                      e.getMessage());
617       return false;
618     }
619   }
620
621   /**
622    * Retrieves the current application version.
623    */
624   
625   public static String getCurrentApplicationVersion(Context context) throws NameNotFoundException {
626     PackageInfo packageInfo =
627         context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
628     return packageInfo.versionName;
629   }
630
631   /**
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.
635    */
636   static String readSenderIdFromMetaData(Context context) {
637     String senderId = AndroidC2DMConstants.SENDER_ID;
638     try {
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;
646         } else {
647           logger.severe("No meta-data element with the name %s found on the service declaration",
648                         SENDER_ID_METADATA_FIELD);
649         }
650       }
651     } catch (NameNotFoundException exception) {
652       logger.info("Could not find C2DMManager service info in manifest");
653     }
654     return senderId;
655   }
656 }