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 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
;
32 * Utilities for device registration.
34 * <strong>Note:</strong> this class uses a private {@link SharedPreferences}
35 * object to keep track of the registration token.
37 public final class GCMRegistrar
{
40 * Default lifespan (7 days) of the {@link #isRegisteredOnServer(Context)}
41 * flag until it is considered expired.
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
=
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";
55 * {@link GCMBroadcastReceiver} instance used to handle the retry intent.
58 * This instance cannot be the same as the one defined in the manifest
59 * because it needs a different permission.
61 private static GCMBroadcastReceiver sRetryReceiver
;
63 private static String sRetryReceiverClassName
;
66 * Checks if the device has the proper dependencies installed.
68 * This method should be called when the application starts to verify that
69 * the device supports GCM.
71 * @param context application context.
72 * @throws UnsupportedOperationException if the device does not support GCM.
74 public static void checkDevice(final Context context
) {
75 final int version
= Build
.VERSION
.SDK_INT
;
77 throw new UnsupportedOperationException("Device must be at least " +
78 "API Level 8 (instead of " + version
+ ")");
80 final PackageManager packageManager
= context
.getPackageManager();
82 packageManager
.getPackageInfo(GSF_PACKAGE
, 0);
83 } catch (NameNotFoundException e
) {
84 throw new UnsupportedOperationException(
85 "Device does not have package " + GSF_PACKAGE
);
90 * Initiate messaging registration for the current application.
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}.
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.
103 public static void register(Context context
, String
... senderIds
) {
104 GCMRegistrar
.resetBackoff(context
);
105 internalRegister(context
, senderIds
);
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
);
120 static String
getFlatSenderIds(String
... senderIds
) {
121 if (senderIds
== null || senderIds
.length
== 0) {
122 throw new IllegalArgumentException("No senderIds");
124 StringBuilder builder
= new StringBuilder(senderIds
[0]);
125 for (int i
= 1; i
< senderIds
.length
; i
++) {
126 builder
.append(',').append(senderIds
[i
]);
128 return builder
.toString();
132 * Clear internal resources.
135 * This method should be called by the main activity's {@code onDestroy()}
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;
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
);
156 * Lazy initializes the {@link GCMBroadcastReceiver} instance.
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();
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() +
174 sRetryReceiver
= new GCMBroadcastReceiver();
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);
189 * Sets the name of the retry receiver class.
191 static void setRetryReceiverClassName(String className
) {
192 Log
.v(TAG
, "Setting the name of retry receiver class to " + className
);
193 sRetryReceiverClassName
= className
;
197 * Gets the current registration id for application on GCM service.
199 * If result is empty, the registration has failed.
201 * @return registration id, or empty string if the registration is not
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
);
217 return registrationId
;
221 * Checks whether the application was successfully registered on GCM
224 static boolean isRegistered(Context context
) {
225 return getRegistrationId(context
).length() > 0;
229 * Clears the registration id in the persistence store.
231 * @param context application's context.
232 * @return old registration id.
234 static String
clearRegistrationId(Context context
) {
235 return setRegistrationId(context
, "");
239 * Sets the registration id in the persistence store.
241 * @param context application's context.
242 * @param regId registration id
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
);
253 return oldRegistrationId
;
257 * Gets the application version.
259 private static int getAppVersion(Context context
) {
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
);
271 * Resets the backoff counter.
273 * This method should be called after a GCM call succeeds.
275 * @param context application's context.
277 static void resetBackoff(Context context
) {
278 Log
.d(TAG
, "resetting backoff for " + context
.getPackageName());
279 setBackoff(context
, DEFAULT_BACKOFF_MS
);
283 * Gets the current backoff counter.
285 * @param context application's context.
286 * @return current backoff counter, in milliseconds.
288 static int getBackoff(Context context
) {
289 final SharedPreferences prefs
= getGCMPreferences(context
);
290 return prefs
.getInt(BACKOFF_MS
, DEFAULT_BACKOFF_MS
);
294 * Sets the backoff counter.
296 * This method should be called after a GCM call fails, passing an
299 * @param context application's context.
300 * @param backoff new backoff counter, in milliseconds.
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
);
309 private static SharedPreferences
getGCMPreferences(Context context
) {
310 return context
.getSharedPreferences(PREFERENCES
, Context
.MODE_PRIVATE
);
313 private GCMRegistrar() {
314 throw new UnsupportedOperationException();