]>
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 android.app.PendingIntent; | |
20 | import android.content.Context; | |
21 | import android.content.Intent; | |
22 | import android.content.IntentFilter; | |
23 | import android.content.SharedPreferences; | |
24 | import android.content.SharedPreferences.Editor; | |
25 | import android.content.pm.PackageInfo; | |
26 | import android.content.pm.PackageManager; | |
27 | import android.content.pm.PackageManager.NameNotFoundException; | |
28 | import android.os.Build; | |
29 | import android.util.Log; | |
30 | ||
31 | /** | |
32 | * Utilities for device registration. | |
33 | * <p> | |
34 | * <strong>Note:</strong> this class uses a private {@link SharedPreferences} | |
35 | * object to keep track of the registration token. | |
36 | */ | |
37 | public final class GCMRegistrar { | |
38 | ||
39 | /** | |
40 | * Default lifespan (7 days) of the {@link #isRegisteredOnServer(Context)} | |
41 | * flag until it is considered expired. | |
42 | */ | |
43 | // NOTE: cannot use TimeUnit.DAYS because it's not available on API Level 8 | |
44 | public static final long DEFAULT_ON_SERVER_LIFESPAN_MS = | |
45 | 1000 * 3600 * 24 * 7; | |
46 | ||
47 | private static final String TAG = "GCMRegistrar"; | |
48 | private static final String BACKOFF_MS = "backoff_ms"; | |
49 | private static final String GSF_PACKAGE = "com.google.android.gsf"; | |
50 | private static final String PREFERENCES = "com.google.android.gcm"; | |
51 | private static final int DEFAULT_BACKOFF_MS = 3000; | |
52 | private static final String PROPERTY_REG_ID = "regId"; | |
53 | private static final String PROPERTY_APP_VERSION = "appVersion"; | |
54 | /** | |
55 | * {@link GCMBroadcastReceiver} instance used to handle the retry intent. | |
56 | * | |
57 | * <p> | |
58 | * This instance cannot be the same as the one defined in the manifest | |
59 | * because it needs a different permission. | |
60 | */ | |
61 | private static GCMBroadcastReceiver sRetryReceiver; | |
62 | ||
63 | private static String sRetryReceiverClassName; | |
64 | ||
65 | /** | |
66 | * Checks if the device has the proper dependencies installed. | |
67 | * <p> | |
68 | * This method should be called when the application starts to verify that | |
69 | * the device supports GCM. | |
70 | * | |
71 | * @param context application context. | |
72 | * @throws UnsupportedOperationException if the device does not support GCM. | |
73 | */ | |
74 | public static void checkDevice(final Context context) { | |
75 | final int version = Build.VERSION.SDK_INT; | |
76 | if (version < 8) { | |
77 | throw new UnsupportedOperationException("Device must be at least " + | |
78 | "API Level 8 (instead of " + version + ")"); | |
79 | } | |
80 | final PackageManager packageManager = context.getPackageManager(); | |
81 | try { | |
82 | packageManager.getPackageInfo(GSF_PACKAGE, 0); | |
83 | } catch (NameNotFoundException e) { | |
84 | throw new UnsupportedOperationException( | |
85 | "Device does not have package " + GSF_PACKAGE); | |
86 | } | |
87 | } | |
88 | ||
89 | /** | |
90 | * Initiate messaging registration for the current application. | |
91 | * <p> | |
92 | * The result will be returned as an | |
93 | * {@link GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK} intent with | |
94 | * either a {@link GCMConstants#EXTRA_REGISTRATION_ID} or | |
95 | * {@link GCMConstants#EXTRA_ERROR}. | |
96 | * | |
97 | * @param context application context. | |
98 | * @param senderIds Google Project ID of the accounts authorized to send | |
99 | * messages to this application. | |
100 | * @throws IllegalStateException if device does not have all GCM | |
101 | * dependencies installed. | |
102 | */ | |
103 | public static void register(Context context, String... senderIds) { | |
104 | GCMRegistrar.resetBackoff(context); | |
105 | internalRegister(context, senderIds); | |
106 | } | |
107 | ||
108 | static void internalRegister(Context context, String... senderIds) { | |
109 | String flatSenderIds = getFlatSenderIds(senderIds); | |
110 | Log.v(TAG, "Registering app " + context.getPackageName() + | |
111 | " of senders " + flatSenderIds); | |
112 | Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_REGISTRATION); | |
113 | intent.setPackage(GSF_PACKAGE); | |
114 | intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT, | |
115 | PendingIntent.getBroadcast(context, 0, new Intent(), 0)); | |
116 | intent.putExtra(GCMConstants.EXTRA_SENDER, flatSenderIds); | |
117 | context.startService(intent); | |
118 | } | |
119 | ||
120 | static String getFlatSenderIds(String... senderIds) { | |
121 | if (senderIds == null || senderIds.length == 0) { | |
122 | throw new IllegalArgumentException("No senderIds"); | |
123 | } | |
124 | StringBuilder builder = new StringBuilder(senderIds[0]); | |
125 | for (int i = 1; i < senderIds.length; i++) { | |
126 | builder.append(',').append(senderIds[i]); | |
127 | } | |
128 | return builder.toString(); | |
129 | } | |
130 | ||
131 | /** | |
132 | * Clear internal resources. | |
133 | * | |
134 | * <p> | |
135 | * This method should be called by the main activity's {@code onDestroy()} | |
136 | * method. | |
137 | */ | |
138 | public static synchronized void onDestroy(Context context) { | |
139 | if (sRetryReceiver != null) { | |
140 | Log.v(TAG, "Unregistering receiver"); | |
141 | context.unregisterReceiver(sRetryReceiver); | |
142 | sRetryReceiver = null; | |
143 | } | |
144 | } | |
145 | ||
146 | static void internalUnregister(Context context) { | |
147 | Log.v(TAG, "Unregistering app " + context.getPackageName()); | |
148 | Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_UNREGISTRATION); | |
149 | intent.setPackage(GSF_PACKAGE); | |
150 | intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT, | |
151 | PendingIntent.getBroadcast(context, 0, new Intent(), 0)); | |
152 | context.startService(intent); | |
153 | } | |
154 | ||
155 | /** | |
156 | * Lazy initializes the {@link GCMBroadcastReceiver} instance. | |
157 | */ | |
158 | static synchronized void setRetryBroadcastReceiver(Context context) { | |
159 | if (sRetryReceiver == null) { | |
160 | if (sRetryReceiverClassName == null) { | |
161 | // should never happen | |
162 | Log.e(TAG, "internal error: retry receiver class not set yet"); | |
163 | sRetryReceiver = new GCMBroadcastReceiver(); | |
164 | } else { | |
165 | Class<?> clazz; | |
166 | try { | |
167 | clazz = Class.forName(sRetryReceiverClassName); | |
168 | sRetryReceiver = (GCMBroadcastReceiver) clazz.newInstance(); | |
169 | } catch (Exception e) { | |
170 | Log.e(TAG, "Could not create instance of " + | |
171 | sRetryReceiverClassName + ". Using " + | |
172 | GCMBroadcastReceiver.class.getName() + | |
173 | " directly."); | |
174 | sRetryReceiver = new GCMBroadcastReceiver(); | |
175 | } | |
176 | } | |
177 | String category = context.getPackageName(); | |
178 | IntentFilter filter = new IntentFilter( | |
179 | GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY); | |
180 | filter.addCategory(category); | |
181 | // must use a permission that is defined on manifest for sure | |
182 | String permission = category + ".permission.C2D_MESSAGE"; | |
183 | Log.v(TAG, "Registering receiver"); | |
184 | context.registerReceiver(sRetryReceiver, filter, permission, null); | |
185 | } | |
186 | } | |
187 | ||
188 | /** | |
189 | * Sets the name of the retry receiver class. | |
190 | */ | |
191 | static void setRetryReceiverClassName(String className) { | |
192 | Log.v(TAG, "Setting the name of retry receiver class to " + className); | |
193 | sRetryReceiverClassName = className; | |
194 | } | |
195 | ||
196 | /** | |
197 | * Gets the current registration id for application on GCM service. | |
198 | * <p> | |
199 | * If result is empty, the registration has failed. | |
200 | * | |
201 | * @return registration id, or empty string if the registration is not | |
202 | * complete. | |
203 | */ | |
204 | public static String getRegistrationId(Context context) { | |
205 | final SharedPreferences prefs = getGCMPreferences(context); | |
206 | String registrationId = prefs.getString(PROPERTY_REG_ID, ""); | |
207 | // check if app was updated; if so, it must clear registration id to | |
208 | // avoid a race condition if GCM sends a message | |
209 | int oldVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE); | |
210 | int newVersion = getAppVersion(context); | |
211 | if (oldVersion != Integer.MIN_VALUE && oldVersion != newVersion) { | |
212 | Log.v(TAG, "App version changed from " + oldVersion + " to " + | |
213 | newVersion + "; resetting registration id"); | |
214 | clearRegistrationId(context); | |
215 | registrationId = ""; | |
216 | } | |
217 | return registrationId; | |
218 | } | |
219 | ||
220 | /** | |
221 | * Checks whether the application was successfully registered on GCM | |
222 | * service. | |
223 | */ | |
224 | static boolean isRegistered(Context context) { | |
225 | return getRegistrationId(context).length() > 0; | |
226 | } | |
227 | ||
228 | /** | |
229 | * Clears the registration id in the persistence store. | |
230 | * | |
231 | * @param context application's context. | |
232 | * @return old registration id. | |
233 | */ | |
234 | static String clearRegistrationId(Context context) { | |
235 | return setRegistrationId(context, ""); | |
236 | } | |
237 | ||
238 | /** | |
239 | * Sets the registration id in the persistence store. | |
240 | * | |
241 | * @param context application's context. | |
242 | * @param regId registration id | |
243 | */ | |
244 | static String setRegistrationId(Context context, String regId) { | |
245 | final SharedPreferences prefs = getGCMPreferences(context); | |
246 | String oldRegistrationId = prefs.getString(PROPERTY_REG_ID, ""); | |
247 | int appVersion = getAppVersion(context); | |
248 | Log.v(TAG, "Saving regId on app version " + appVersion); | |
249 | Editor editor = prefs.edit(); | |
250 | editor.putString(PROPERTY_REG_ID, regId); | |
251 | editor.putInt(PROPERTY_APP_VERSION, appVersion); | |
252 | editor.commit(); | |
253 | return oldRegistrationId; | |
254 | } | |
255 | ||
256 | /** | |
257 | * Gets the application version. | |
258 | */ | |
259 | private static int getAppVersion(Context context) { | |
260 | try { | |
261 | PackageInfo packageInfo = context.getPackageManager() | |
262 | .getPackageInfo(context.getPackageName(), 0); | |
263 | return packageInfo.versionCode; | |
264 | } catch (NameNotFoundException e) { | |
265 | // should never happen | |
266 | throw new RuntimeException("Coult not get package name: " + e); | |
267 | } | |
268 | } | |
269 | ||
270 | /** | |
271 | * Resets the backoff counter. | |
272 | * <p> | |
273 | * This method should be called after a GCM call succeeds. | |
274 | * | |
275 | * @param context application's context. | |
276 | */ | |
277 | static void resetBackoff(Context context) { | |
278 | Log.d(TAG, "resetting backoff for " + context.getPackageName()); | |
279 | setBackoff(context, DEFAULT_BACKOFF_MS); | |
280 | } | |
281 | ||
282 | /** | |
283 | * Gets the current backoff counter. | |
284 | * | |
285 | * @param context application's context. | |
286 | * @return current backoff counter, in milliseconds. | |
287 | */ | |
288 | static int getBackoff(Context context) { | |
289 | final SharedPreferences prefs = getGCMPreferences(context); | |
290 | return prefs.getInt(BACKOFF_MS, DEFAULT_BACKOFF_MS); | |
291 | } | |
292 | ||
293 | /** | |
294 | * Sets the backoff counter. | |
295 | * <p> | |
296 | * This method should be called after a GCM call fails, passing an | |
297 | * exponential value. | |
298 | * | |
299 | * @param context application's context. | |
300 | * @param backoff new backoff counter, in milliseconds. | |
301 | */ | |
302 | static void setBackoff(Context context, int backoff) { | |
303 | final SharedPreferences prefs = getGCMPreferences(context); | |
304 | Editor editor = prefs.edit(); | |
305 | editor.putInt(BACKOFF_MS, backoff); | |
306 | editor.commit(); | |
307 | } | |
308 | ||
309 | private static SharedPreferences getGCMPreferences(Context context) { | |
310 | return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); | |
311 | } | |
312 | ||
313 | private GCMRegistrar() { | |
314 | throw new UnsupportedOperationException(); | |
315 | } | |
316 | } |