Set target to android 18
[fonbot.git] / lib / com / google / android / gcm / GCMBaseIntentService.java
1 /*
2 * Copyright 2012 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.android.gcm;
18
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;
29
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;
38
39 import java.util.Random;
40 import java.util.concurrent.TimeUnit;
41
42 /**
43 * Skeleton for application-specific {@link IntentService}s responsible for
44 * handling communication from Google Cloud Messaging service.
45 * <p>
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
49 * be blocked.
50 * <p>
51 * Subclasses must provide a public no-arg constructor.
52 */
53 public abstract class GCMBaseIntentService extends IntentService {
54
55 public static final String TAG = "GCMBaseIntentService";
56
57 // wakelock
58 private static final String WAKELOCK_KEY = "GCM_LIB";
59 private static PowerManager.WakeLock sWakeLock;
60
61 // Java lock used to synchronize access to sWakelock
62 private static final Object LOCK = GCMBaseIntentService.class;
63
64 private final String[] mSenderIds;
65
66 // instance counter
67 private static int sCounter = 0;
68
69 private static final Random sRandom = new Random();
70
71 private static final int MAX_BACKOFF_MS =
72 (int) TimeUnit.SECONDS.toMillis(3600); // 1 hour
73
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";
78
79 /**
80 * Constructor that does not set a sender id, useful when the sender id
81 * is context-specific.
82 * <p>
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.
87 */
88 protected GCMBaseIntentService() {
89 this(getName("DynamicSenderIds"), null);
90 }
91
92 /**
93 * Constructor used when the sender id(s) is fixed.
94 */
95 protected GCMBaseIntentService(String... senderIds) {
96 this(getName(senderIds), senderIds);
97 }
98
99 private GCMBaseIntentService(String name, String[] senderIds) {
100 super(name); // name is used as base name for threads, etc.
101 mSenderIds = senderIds;
102 }
103
104 private static String getName(String senderId) {
105 String name = "GCMIntentService-" + senderId + "-" + (++sCounter);
106 Log.v(TAG, "Intent service name: " + name);
107 return name;
108 }
109
110 private static String getName(String[] senderIds) {
111 String flatSenderIds = GCMRegistrar.getFlatSenderIds(senderIds);
112 return getName(flatSenderIds);
113 }
114
115 /**
116 * Gets the sender ids.
117 *
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.
120 *
121 * @throws IllegalStateException if sender id was not set on constructor.
122 */
123 protected String[] getSenderIds() {
124 if (mSenderIds == null) {
125 throw new IllegalStateException("sender id not set on constructor");
126 }
127 return mSenderIds;
128 }
129
130 /**
131 * Called when a cloud message has been received.
132 *
133 * @param context application's context.
134 * @param intent intent containing the message payload as extras.
135 */
136 protected abstract void onMessage(Context context, Intent intent);
137
138 /**
139 * Called when the GCM server tells pending messages have been deleted
140 * because the device was idle.
141 *
142 * @param context application's context.
143 * @param total total number of collapsed messages
144 */
145 protected void onDeletedMessages(Context context, int total) {
146 //do nothing
147 }
148
149 /**
150 * Called on a registration error that could be retried.
151 *
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.
154 *
155 * @param context application's context.
156 * @param errorId error id returned by the GCM service.
157 *
158 * @return if {@literal true}, failed operation will be retried (using
159 * exponential backoff).
160 */
161 protected static boolean onRecoverableError(Context context, String errorId) {
162 return true;
163 }
164
165 /**
166 * Called on registration or unregistration error.
167 *
168 * @param context application's context.
169 * @param errorId error id returned by the GCM service.
170 */
171 protected abstract void onError(Context context, String errorId);
172
173 /**
174 * Called after a device has been registered.
175 *
176 * @param context application's context.
177 * @param registrationId the registration id returned by the GCM service.
178 */
179 protected abstract void onRegistered(Context context,
180 String registrationId);
181
182 /**
183 * Called after a device has been unregistered.
184 *
185 * @param registrationId the registration id that was previously registered.
186 * @param context application's context.
187 */
188 protected abstract void onUnregistered(Context context,
189 String registrationId);
190
191 @Override
192 public final void onHandleIntent(Intent intent) {
193 try {
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
201 String messageType =
202 intent.getStringExtra(EXTRA_SPECIAL_MESSAGE);
203 if (messageType != null) {
204 if (messageType.equals(VALUE_DELETED_MESSAGES)) {
205 String sTotal =
206 intent.getStringExtra(EXTRA_TOTAL_DELETED);
207 if (sTotal != null) {
208 try {
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);
216 }
217 }
218 } else {
219 // application is not using the latest GCM library
220 Log.e(TAG, "Received unknown special message: " +
221 messageType);
222 }
223 } else {
224 onMessage(context, intent);
225 }
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
230 // malicious app.
231 Log.e(TAG, "Received invalid token: " + token);
232 return;
233 }
234 // retry last call
235 if (GCMRegistrar.isRegistered(context)) {
236 GCMRegistrar.internalUnregister(context);
237 } else {
238 String[] senderIds = getSenderIds();
239 GCMRegistrar.internalRegister(context, senderIds);
240 }
241 }
242 } finally {
243 // Release the power lock, so phone can get back to sleep.
244 // The lock is reference-counted by default, so multiple
245 // messages are ok.
246
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");
253 sWakeLock.release();
254 } else {
255 // should never happen during normal workflow
256 Log.e(TAG, "Wakelock reference is null");
257 }
258 }
259 }
260 }
261
262 /**
263 * Called from the broadcast receiver.
264 * <p>
265 * Will process the received intent, call handleMessage(), registered(),
266 * etc. in background threads, with a wake lock, while keeping the service
267 * alive.
268 */
269 static void runIntentInService(Context context, Intent intent,
270 String className) {
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,
277 WAKELOCK_KEY);
278 }
279 }
280 Log.v(TAG, "Acquiring wakelock");
281 sWakeLock.acquire();
282 intent.setClassName(context, className);
283 context.startService(intent);
284 }
285
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);
292
293 // registration succeeded
294 if (registrationId != null) {
295 GCMRegistrar.resetBackoff(context);
296 GCMRegistrar.setRegistrationId(context, registrationId);
297 onRegistered(context, registrationId);
298 return;
299 }
300
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);
308 return;
309 }
310
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);
316 if (retry) {
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 + ")");
322 Intent retryIntent =
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,
331 retryPendingIntent);
332 // Next retry should wait longer.
333 if (backoffTimeMs < MAX_BACKOFF_MS) {
334 GCMRegistrar.setBackoff(context, backoffTimeMs * 2);
335 }
336 } else {
337 Log.d(TAG, "Not retrying failed operation");
338 }
339 } else {
340 // Unrecoverable error, notify app
341 onError(context, error);
342 }
343 }
344
345 }
This page took 0.032895 seconds and 4 git commands to generate.