Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / cacheinvalidation / src / java / com / google / ipc / invalidation / external / client / contrib / MultiplexingGcmListener.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.external.client.contrib;
18
19 import com.google.android.gcm.GCMBaseIntentService;
20 import com.google.android.gcm.GCMBroadcastReceiver;
21 import com.google.android.gcm.GCMRegistrar;
22 import com.google.ipc.invalidation.external.client.SystemResources.Logger;
23 import com.google.ipc.invalidation.external.client.android.service.AndroidLogger;
24 import com.google.ipc.invalidation.ticl.android2.WakeLockManager;
25
26 import android.app.IntentService;
27 import android.content.BroadcastReceiver;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.pm.PackageManager;
32 import android.content.pm.PackageManager.NameNotFoundException;
33 import android.content.pm.ServiceInfo;
34
35 /**
36  * A Google Cloud Messaging listener class that rebroadcasts events as package-scoped
37  * broadcasts. This allows multiple components to share a single GCM connection.
38  * <p>
39  * This listener uses an API of broadcasted Intents that is modeled after that provided by
40  * {@link GCMBaseIntentService}. For each upcall (e.g., onMessage, on Registered, etc) specified
41  * by {@code GCMBaseIntentService}, there is an {@code EXTRA_OP_...} constant defined in
42  * {@link Intents}.
43  * <p>
44  * Note that this class does <b>NOT</b> handle registering with GCM; applications are still required
45  * to do that in the usual way (e.g., using the GCMRegistrar class from the GCM library).
46  * <p>
47  * In order to raise a {@code GCMBaseIntentService} event to listeners, this service will broadcast
48  * an Intent with the following properties:
49  * 1. The action of the Intent is {@link Intents#ACTION}
50  * 2. There is a boolean-valued extra in the Intent whose key is the {@code EXTRA_OP_...} key
51  *    for that call and whose value is {@code true}. For any intent, exactly one {@code EXTRA_OP}
52  *    extra will be set.
53  * 3. The Intent contains additional call-specific extras required to interpret it. (See note for
54  *    onMessage, below).
55  * <p>
56  * Clients of this service <b>MUST NOT</b> assume that there is a one-to-one mapping between
57  * issued broadcasts and actual GCM intents. I.e., this service may issue broadcast intents
58  * spontaneously, and it may not issue an intent for every GCM event.
59  * <p>
60  * For the onMessage() call, the broadcast intent will contain key/value extras containing the
61  * message payload. These extras are guaranteed to be identical to those that would have been in
62  * the Intent provided to the onMessage call. However, clients <b>MUST NOT</b> assume that the
63  * Intent broadcast to communicate a GCM message is literally the same Intent generated by the GCM
64  * client library.
65  * <p>
66  * This class does not expose the {@code onError} call, since according to the GCM documentation
67  * there is nothing to do except log an error (which this class does).
68  *
69  */
70 public class MultiplexingGcmListener extends GCMBaseIntentService {
71   /* This class is public so that it can be instantiated by the Android runtime. */
72
73   /** Constants used in broadcast Intents. */
74   public static final class Intents {
75     /** Prefix of the action and extras. */
76     private static final String PREFIX = "com.google.ipc.invalidation.gcmmplex.";
77
78     /** Action of all broadcast intents issued. */
79     public static final String ACTION = PREFIX + "EVENT";
80
81     /** Extra corresponding to an {@code onMessage} upcall. */
82     public static final String EXTRA_OP_MESSAGE = PREFIX + "MESSAGE";
83
84     /** Extra corresponding to an {@code onRegistered} upcall. */
85     public static final String EXTRA_OP_REGISTERED = PREFIX + "REGISTERED";
86
87     /** Extra corresponding to an {@code onUnregistered} upcall. */
88     public static final String EXTRA_OP_UNREGISTERED = PREFIX + "UNREGISTERED";
89
90     /** Extra corresponding to an {@code onDeletedMessages} upcall. */
91     public static final String EXTRA_OP_DELETED_MESSAGES = PREFIX + "DELETED_MSGS";
92
93     /**
94      * Extra set iff the operation is {@link #EXTRA_OP_REGISTERED} or
95      * {@link #EXTRA_OP_UNREGISTERED}; it is string-valued and holds the registration id.
96      */
97     public static final String EXTRA_DATA_REG_ID = PREFIX + "REGID";
98
99     /**
100      * Extra set iff the operation is {@link #EXTRA_OP_DELETED_MESSAGES}; it is integer-valued
101      * and holds the number of deleted messages.
102      */
103     public static final String EXTRA_DATA_NUM_DELETED_MSGS = PREFIX + "NUM_DELETED_MSGS";
104   }
105
106   /**
107    * {@link GCMBroadcastReceiver} that forwards GCM intents to the {@code MultiplexingGcmListener}
108    * class.
109    */
110   public static class GCMReceiver extends GCMBroadcastReceiver {
111     /* This class is public so that it can be instantiated by the Android runtime. */
112     @Override
113     protected String getGCMIntentServiceClassName(Context context) {
114       return MultiplexingGcmListener.class.getName();
115     }
116   }
117
118   /**
119    * Convenience base class for client implementations. It provides base classes for a broadcast
120    * receiver and an intent service that work together to handle events from the
121    * {@code MultiplexingGcmListener} while holding a wake lock.
122    * <p>
123    * This class guarantees that the {@code onYYY} methods will be called holding a wakelock, and
124    * that the wakelock will be automatically released when the method returns.
125    * <p>
126    * The wakelock will also be automatically released
127    * {@link Receiver#WAKELOCK_TIMEOUT_MS} ms after the original Intent was received by the
128    * {@link Receiver} class, to guard against leaks. Applications requiring a longer-duration
129    * wakelock should acquire one on their own in the appropriate {@code onYYY} method.
130    */
131   public static abstract class AbstractListener extends IntentService {
132     /** Prefix of all wakelocks acquired by the receiver and the intent service. */
133     private static final String WAKELOCK_PREFIX = "multiplexing-gcm-listener:";
134
135     /** Intent extra key used to hold wakelock names, for runtime checks. */
136     private static final String EXTRA_WAKELOCK_NAME =
137         "com.google.ipc.invalidation.gcmmplex.listener.WAKELOCK_NAME";
138
139     /**
140      * A {@code BroadcastReceiver} to receive intents from the {@code MultiplexingGcmListener}
141      * service. It acquires a wakelock and forwards the intent to the service named by
142      * {@link #getServiceClass}, which must be a subclass of {@code AbstractListener}.
143      */
144     public static abstract class Receiver extends BroadcastReceiver {
145       /** Timeout after which wakelocks will be automatically released. */
146       private static final int WAKELOCK_TIMEOUT_MS = 30 * 1000;
147
148       @Override
149       public final void onReceive(Context context, Intent intent) {
150         // This method is final to prevent subclasses from overriding it and introducing errors in
151         // the wakelock protocol.
152         Class<?> serviceClass = getServiceClass();
153
154         // If the service isn't an AbstractListener subclass, then it will not release the wakelock
155         // properly, causing bugs.
156         if (!AbstractListener.class.isAssignableFrom(serviceClass)) {
157           throw new RuntimeException(
158               "Service class is not a subclass of AbstractListener: " + serviceClass);
159         }
160         String wakelockKey = getWakelockKey(serviceClass);
161         intent.setClass(context, serviceClass);
162
163         // To avoid insidious bugs, tell the service which wakelock we acquired. The service will
164         // log a warning if the lock it releases is not this lock.
165         intent.putExtra(EXTRA_WAKELOCK_NAME, wakelockKey);
166
167         // Acquire the lock and start the service. The service is responsible for releasing the
168         // lock.
169         WakeLockManager.getInstance(context).acquire(wakelockKey, WAKELOCK_TIMEOUT_MS);
170         context.startService(intent);
171       }
172
173       /** Returns the class of the service that will handle intents. */
174       protected abstract Class<?> getServiceClass();
175     }
176
177     protected AbstractListener(String name) {
178       super(name);
179
180       // If the process dies during a call to onHandleIntent, redeliver the intent when the service
181       // restarts.
182       setIntentRedelivery(true);
183     }
184
185     @Override
186     public final void onHandleIntent(Intent intent) {
187       if (intent == null) {
188         return;
189       }
190
191       // This method is final to prevent subclasses from overriding it and introducing errors in
192       // the wakelock protocol.
193       try {
194         doHandleIntent(intent);
195       } finally {
196         // Release the wakelock acquired by the receiver. The receiver provides the name of the
197         // lock it acquired in the Intent so that we can sanity-check that we are releasing the
198         // right lock.
199         String receiverAcquiredWakelock = intent.getStringExtra(EXTRA_WAKELOCK_NAME);
200         String wakelockToRelease = getWakelockKey(getClass());
201         if (!wakelockToRelease.equals(receiverAcquiredWakelock)) {
202           logger.warning("Receiver acquired wakelock '%s' but releasing '%s'",
203               receiverAcquiredWakelock, wakelockToRelease);
204         }
205         WakeLockManager wakelockManager = WakeLockManager.getInstance(this);
206         wakelockManager.release(wakelockToRelease);
207       }
208     }
209
210     /** Handles {@code intent} while holding a wake lock. */
211     private void doHandleIntent(Intent intent) {
212       // Ensure this is an Intent we want to handle.
213       if (!MultiplexingGcmListener.Intents.ACTION.equals(intent.getAction())) {
214         logger.warning("Ignoring intent with unknown action: %s", intent);
215         return;
216       }
217       // Dispatch based on the extras.
218       if (intent.hasExtra(MultiplexingGcmListener.Intents.EXTRA_OP_MESSAGE)) {
219         onMessage(intent);
220       } else if (intent.hasExtra(MultiplexingGcmListener.Intents.EXTRA_OP_REGISTERED)) {
221         onRegistered(intent.getStringExtra(MultiplexingGcmListener.Intents.EXTRA_DATA_REG_ID));
222       } else if (intent.hasExtra(MultiplexingGcmListener.Intents.EXTRA_OP_UNREGISTERED)) {
223         onUnregistered(intent.getStringExtra(MultiplexingGcmListener.Intents.EXTRA_DATA_REG_ID));
224       } else if (intent.hasExtra(MultiplexingGcmListener.Intents.EXTRA_OP_DELETED_MESSAGES)) {
225         int numDeleted =
226             intent.getIntExtra(MultiplexingGcmListener.Intents.EXTRA_DATA_NUM_DELETED_MSGS, -1);
227         if (numDeleted == -1) {
228           logger.warning("Could not parse num-deleted field of GCM broadcast: %s", intent);
229           return;
230         }
231         onDeletedMessages(numDeleted);
232       } else {
233         logger.warning("Broadcast GCM intent with no known operation: %s", intent);
234       }
235     }
236
237     // These methods have the same specs as in {@code GCMBaseIntentService}.
238     protected abstract void onMessage(Intent intent);
239     protected abstract void onRegistered(String registrationId);
240     protected abstract void onUnregistered(String registrationId);
241     protected abstract void onDeletedMessages(int total);
242
243     /**
244      * Returns the name of the wakelock to acquire for the intent service implemented by
245      * {@code clazz}.
246      */
247     private static String getWakelockKey(Class<?> clazz) {
248       return WAKELOCK_PREFIX + clazz.getName();
249     }
250   }
251
252   /**
253    * Name of the metadata element within the {@code service} element whose value is a
254    * comma-delimited list of GCM sender ids.
255    */
256   private static final String GCM_SENDER_IDS_METADATA_KEY = "sender_ids";
257
258   /** Logger. */
259   private static final Logger logger = AndroidLogger.forTag("MplexGcmListener");
260
261   // All onYYY methods work by constructing an appropriate Intent and broadcasting it.
262
263   @Override
264   protected void onMessage(Context context, Intent intent) {
265     Intent newIntent = new Intent();
266     newIntent.putExtra(Intents.EXTRA_OP_MESSAGE, true);
267
268     // Copy the extra keys containing the message payload into the new Intent.
269     for (String extraKey : intent.getExtras().keySet()) {
270       newIntent.putExtra(extraKey, intent.getStringExtra(extraKey));
271     }
272     rebroadcast(newIntent);
273   }
274
275   @Override
276   protected void onRegistered(Context context, String registrationId) {
277     Intent intent = new Intent();
278     intent.putExtra(Intents.EXTRA_OP_REGISTERED, true);
279     intent.putExtra(Intents.EXTRA_DATA_REG_ID, registrationId);
280     rebroadcast(intent);
281   }
282
283   @Override
284   protected void onUnregistered(Context context, String registrationId) {
285     Intent intent = new Intent();
286     intent.putExtra(Intents.EXTRA_OP_UNREGISTERED, true);
287     intent.putExtra(Intents.EXTRA_DATA_REG_ID, registrationId);
288     rebroadcast(intent);
289   }
290
291   @Override
292   protected void onDeletedMessages(Context context, int total) {
293     Intent intent = new Intent();
294     intent.putExtra(Intents.EXTRA_OP_DELETED_MESSAGES, true);
295     intent.putExtra(Intents.EXTRA_DATA_NUM_DELETED_MSGS, total);
296     rebroadcast(intent);
297   }
298
299   @Override
300   protected void onError(Context context, String errorId) {
301     // This is called for unrecoverable errors, so just log a warning.
302     logger.warning("GCM error: %s", errorId);
303   }
304
305   @Override
306   protected String[] getSenderIds(Context context) {
307     return readSenderIdsFromManifestOrDie(this);
308   }
309
310   /**
311    * Broadcasts {@code intent} with the action set to {@link Intents#ACTION} and the package name
312    * set to the package name of this service.
313    */
314   private void rebroadcast(Intent intent) {
315     intent.setAction(Intents.ACTION);
316     intent.setPackage(getPackageName());
317     sendBroadcast(intent);
318   }
319
320   /**
321    * Registers with GCM if not already registered. Also verifies that the device supports GCM
322    * and that the manifest is correctly configured. Returns the existing registration id, if one
323    * exists, or the empty string if one does not.
324    *
325    * @throws UnsupportedOperationException if the device does not have all GCM dependencies
326    * @throws IllegalStateException if the manifest is not correctly configured
327    */
328   public static String initializeGcm(Context context) {
329     GCMRegistrar.checkDevice(context);
330     GCMRegistrar.checkManifest(context);
331     final String regId = GCMRegistrar.getRegistrationId(context);
332     if (regId.isEmpty()) {
333       GCMRegistrar.register(context, readSenderIdsFromManifestOrDie(context));
334     }
335     return regId;
336   }
337
338   /**
339    * Returns the GCM sender ids from {@link #GCM_SENDER_IDS_METADATA_KEY} or throws a
340    * {@code RuntimeException} if they are not defined.
341    */
342   
343   static String[] readSenderIdsFromManifestOrDie(Context context) {
344     try {
345       ServiceInfo serviceInfo = context.getPackageManager().getServiceInfo(
346           new ComponentName(context, MultiplexingGcmListener.class), PackageManager.GET_META_DATA);
347       if (serviceInfo.metaData == null) {
348         throw new RuntimeException("Service has no metadata");
349       }
350       String senderIds = serviceInfo.metaData.getString(GCM_SENDER_IDS_METADATA_KEY);
351       if (senderIds == null) {
352         throw new RuntimeException("Service does not have the sender-ids metadata");
353       }
354       return senderIds.split(",");
355     } catch (NameNotFoundException exception) {
356       throw new RuntimeException("Could not read service info from manifest", exception);
357     }
358   }
359 }