]>
Commit | Line | Data |
---|---|---|
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 | } |