2 * Copyright 2012 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
.android
.gcm
;
19 import static com
.google
.android
.gcm
.GCMConstants
.ERROR_SERVICE_NOT_AVAILABLE
;
20 import static com
.google
.android
.gcm
.GCMConstants
.EXTRA_ERROR
;
21 import static com
.google
.android
.gcm
.GCMConstants
.EXTRA_REGISTRATION_ID
;
22 import static com
.google
.android
.gcm
.GCMConstants
.EXTRA_SPECIAL_MESSAGE
;
23 import static com
.google
.android
.gcm
.GCMConstants
.EXTRA_TOTAL_DELETED
;
24 import static com
.google
.android
.gcm
.GCMConstants
.EXTRA_UNREGISTERED
;
25 import static com
.google
.android
.gcm
.GCMConstants
.INTENT_FROM_GCM_LIBRARY_RETRY
;
26 import static com
.google
.android
.gcm
.GCMConstants
.INTENT_FROM_GCM_MESSAGE
;
27 import static com
.google
.android
.gcm
.GCMConstants
.INTENT_FROM_GCM_REGISTRATION_CALLBACK
;
28 import static com
.google
.android
.gcm
.GCMConstants
.VALUE_DELETED_MESSAGES
;
30 import android
.app
.AlarmManager
;
31 import android
.app
.IntentService
;
32 import android
.app
.PendingIntent
;
33 import android
.content
.Context
;
34 import android
.content
.Intent
;
35 import android
.os
.PowerManager
;
36 import android
.os
.SystemClock
;
37 import android
.util
.Log
;
39 import java
.util
.Random
;
40 import java
.util
.concurrent
.TimeUnit
;
43 * Skeleton for application-specific {@link IntentService}s responsible for
44 * handling communication from Google Cloud Messaging service.
46 * The abstract methods in this class are called from its worker thread, and
47 * hence should run in a limited amount of time. If they execute long
48 * operations, they should spawn new threads, otherwise the worker thread will
51 * Subclasses must provide a public no-arg constructor.
53 public abstract class GCMBaseIntentService
extends IntentService
{
55 public static final String TAG
= "GCMBaseIntentService";
58 private static final String WAKELOCK_KEY
= "GCM_LIB";
59 private static PowerManager
.WakeLock sWakeLock
;
61 // Java lock used to synchronize access to sWakelock
62 private static final Object LOCK
= GCMBaseIntentService
.class;
64 private final String
[] mSenderIds
;
67 private static int sCounter
= 0;
69 private static final Random sRandom
= new Random();
71 private static final int MAX_BACKOFF_MS
=
72 (int) TimeUnit
.SECONDS
.toMillis(3600); // 1 hour
74 // token used to check intent origin
75 private static final String TOKEN
=
76 Long
.toBinaryString(sRandom
.nextLong());
77 private static final String EXTRA_TOKEN
= "token";
80 * Constructor that does not set a sender id, useful when the sender id
81 * is context-specific.
83 * When using this constructor, the subclass <strong>must</strong>
84 * override {@link #getSenderIds(Context)}, otherwise methods such as
85 * {@link #onHandleIntent(Intent)} will throw an
86 * {@link IllegalStateException} on runtime.
88 protected GCMBaseIntentService() {
89 this(getName("DynamicSenderIds"), null);
93 * Constructor used when the sender id(s) is fixed.
95 protected GCMBaseIntentService(String
... senderIds
) {
96 this(getName(senderIds
), senderIds
);
99 private GCMBaseIntentService(String name
, String
[] senderIds
) {
100 super(name
); // name is used as base name for threads, etc.
101 mSenderIds
= senderIds
;
104 private static String
getName(String senderId
) {
105 String name
= "GCMIntentService-" + senderId
+ "-" + (++sCounter
);
106 Log
.v(TAG
, "Intent service name: " + name
);
110 private static String
getName(String
[] senderIds
) {
111 String flatSenderIds
= GCMRegistrar
.getFlatSenderIds(senderIds
);
112 return getName(flatSenderIds
);
116 * Gets the sender ids.
118 * <p>By default, it returns the sender ids passed in the constructor, but
119 * it could be overridden to provide a dynamic sender id.
121 * @throws IllegalStateException if sender id was not set on constructor.
123 protected String
[] getSenderIds() {
124 if (mSenderIds
== null) {
125 throw new IllegalStateException("sender id not set on constructor");
131 * Called when a cloud message has been received.
133 * @param context application's context.
134 * @param intent intent containing the message payload as extras.
136 protected abstract void onMessage(Context context
, Intent intent
);
139 * Called when the GCM server tells pending messages have been deleted
140 * because the device was idle.
142 * @param context application's context.
143 * @param total total number of collapsed messages
145 protected void onDeletedMessages(Context context
, int total
) {
150 * Called on a registration error that could be retried.
152 * <p>By default, it does nothing and returns {@literal true}, but could be
153 * overridden to change that behavior and/or display the error.
155 * @param context application's context.
156 * @param errorId error id returned by the GCM service.
158 * @return if {@literal true}, failed operation will be retried (using
159 * exponential backoff).
161 protected static boolean onRecoverableError(Context context
, String errorId
) {
166 * Called on registration or unregistration error.
168 * @param context application's context.
169 * @param errorId error id returned by the GCM service.
171 protected abstract void onError(Context context
, String errorId
);
174 * Called after a device has been registered.
176 * @param context application's context.
177 * @param registrationId the registration id returned by the GCM service.
179 protected abstract void onRegistered(Context context
,
180 String registrationId
);
183 * Called after a device has been unregistered.
185 * @param registrationId the registration id that was previously registered.
186 * @param context application's context.
188 protected abstract void onUnregistered(Context context
,
189 String registrationId
);
192 public final void onHandleIntent(Intent intent
) {
194 Context context
= getApplicationContext();
195 String action
= intent
.getAction();
196 if (action
.equals(INTENT_FROM_GCM_REGISTRATION_CALLBACK
)) {
197 GCMRegistrar
.setRetryBroadcastReceiver(context
);
198 handleRegistration(context
, intent
);
199 } else if (action
.equals(INTENT_FROM_GCM_MESSAGE
)) {
200 // checks for special messages
202 intent
.getStringExtra(EXTRA_SPECIAL_MESSAGE
);
203 if (messageType
!= null) {
204 if (messageType
.equals(VALUE_DELETED_MESSAGES
)) {
206 intent
.getStringExtra(EXTRA_TOTAL_DELETED
);
207 if (sTotal
!= null) {
209 int total
= Integer
.parseInt(sTotal
);
210 Log
.v(TAG
, "Received deleted messages " +
211 "notification: " + total
);
212 onDeletedMessages(context
, total
);
213 } catch (NumberFormatException e
) {
214 Log
.e(TAG
, "GCM returned invalid number of " +
215 "deleted messages: " + sTotal
);
219 // application is not using the latest GCM library
220 Log
.e(TAG
, "Received unknown special message: " +
224 onMessage(context
, intent
);
226 } else if (action
.equals(INTENT_FROM_GCM_LIBRARY_RETRY
)) {
227 String token
= intent
.getStringExtra(EXTRA_TOKEN
);
228 if (!TOKEN
.equals(token
)) {
229 // make sure intent was generated by this class, not by a
231 Log
.e(TAG
, "Received invalid token: " + token
);
235 if (GCMRegistrar
.isRegistered(context
)) {
236 GCMRegistrar
.internalUnregister(context
);
238 String
[] senderIds
= getSenderIds();
239 GCMRegistrar
.internalRegister(context
, senderIds
);
243 // Release the power lock, so phone can get back to sleep.
244 // The lock is reference-counted by default, so multiple
247 // If onMessage() needs to spawn a thread or do something else,
248 // it should use its own lock.
249 synchronized (LOCK
) {
250 // sanity check for null as this is a public method
251 if (sWakeLock
!= null) {
252 Log
.v(TAG
, "Releasing wakelock");
255 // should never happen during normal workflow
256 Log
.e(TAG
, "Wakelock reference is null");
263 * Called from the broadcast receiver.
265 * Will process the received intent, call handleMessage(), registered(),
266 * etc. in background threads, with a wake lock, while keeping the service
269 static void runIntentInService(Context context
, Intent intent
,
271 synchronized (LOCK
) {
272 if (sWakeLock
== null) {
273 // This is called from BroadcastReceiver, there is no init.
274 PowerManager pm
= (PowerManager
)
275 context
.getSystemService(Context
.POWER_SERVICE
);
276 sWakeLock
= pm
.newWakeLock(PowerManager
.PARTIAL_WAKE_LOCK
,
280 Log
.v(TAG
, "Acquiring wakelock");
282 intent
.setClassName(context
, className
);
283 context
.startService(intent
);
286 private void handleRegistration(final Context context
, Intent intent
) {
287 String registrationId
= intent
.getStringExtra(EXTRA_REGISTRATION_ID
);
288 String error
= intent
.getStringExtra(EXTRA_ERROR
);
289 String unregistered
= intent
.getStringExtra(EXTRA_UNREGISTERED
);
290 Log
.d(TAG
, "handleRegistration: registrationId = " + registrationId
+
291 ", error = " + error
+ ", unregistered = " + unregistered
);
293 // registration succeeded
294 if (registrationId
!= null) {
295 GCMRegistrar
.resetBackoff(context
);
296 GCMRegistrar
.setRegistrationId(context
, registrationId
);
297 onRegistered(context
, registrationId
);
301 // unregistration succeeded
302 if (unregistered
!= null) {
303 // Remember we are unregistered
304 GCMRegistrar
.resetBackoff(context
);
305 String oldRegistrationId
=
306 GCMRegistrar
.clearRegistrationId(context
);
307 onUnregistered(context
, oldRegistrationId
);
311 // last operation (registration or unregistration) returned an error;
312 Log
.d(TAG
, "Registration error: " + error
);
313 // Registration failed
314 if (ERROR_SERVICE_NOT_AVAILABLE
.equals(error
)) {
315 boolean retry
= onRecoverableError(context
, error
);
317 int backoffTimeMs
= GCMRegistrar
.getBackoff(context
);
318 int nextAttempt
= backoffTimeMs
/ 2 +
319 sRandom
.nextInt(backoffTimeMs
);
320 Log
.d(TAG
, "Scheduling registration retry, backoff = " +
321 nextAttempt
+ " (" + backoffTimeMs
+ ")");
323 new Intent(INTENT_FROM_GCM_LIBRARY_RETRY
);
324 retryIntent
.putExtra(EXTRA_TOKEN
, TOKEN
);
325 PendingIntent retryPendingIntent
= PendingIntent
326 .getBroadcast(context
, 0, retryIntent
, 0);
327 AlarmManager am
= (AlarmManager
)
328 context
.getSystemService(Context
.ALARM_SERVICE
);
329 am
.set(AlarmManager
.ELAPSED_REALTIME
,
330 SystemClock
.elapsedRealtime() + nextAttempt
,
332 // Next retry should wait longer.
333 if (backoffTimeMs
< MAX_BACKOFF_MS
) {
334 GCMRegistrar
.setBackoff(context
, backoffTimeMs
* 2);
337 Log
.d(TAG
, "Not retrying failed operation");
340 // Unrecoverable error, notify app
341 onError(context
, error
);