/* * Copyright 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.gcm; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.os.Build; import android.util.Log; import java.sql.Timestamp; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Utilities for device registration. * <p> * <strong>Note:</strong> this class uses a private {@link SharedPreferences} * object to keep track of the registration token. */ public final class GCMRegistrar { /** * Default lifespan (7 days) of the {@link #isRegisteredOnServer(Context)} * flag until it is considered expired. */ // NOTE: cannot use TimeUnit.DAYS because it's not available on API Level 8 public static final long DEFAULT_ON_SERVER_LIFESPAN_MS = 1000 * 3600 * 24 * 7; private static final String TAG = "GCMRegistrar"; private static final String BACKOFF_MS = "backoff_ms"; private static final String GSF_PACKAGE = "com.google.android.gsf"; private static final String PREFERENCES = "com.google.android.gcm"; private static final int DEFAULT_BACKOFF_MS = 3000; private static final String PROPERTY_REG_ID = "regId"; private static final String PROPERTY_APP_VERSION = "appVersion"; private static final String PROPERTY_ON_SERVER = "onServer"; private static final String PROPERTY_ON_SERVER_EXPIRATION_TIME = "onServerExpirationTime"; private static final String PROPERTY_ON_SERVER_LIFESPAN = "onServerLifeSpan"; /** * {@link GCMBroadcastReceiver} instance used to handle the retry intent. * * <p> * This instance cannot be the same as the one defined in the manifest * because it needs a different permission. */ private static GCMBroadcastReceiver sRetryReceiver; private static String sRetryReceiverClassName; /** * Checks if the device has the proper dependencies installed. * <p> * This method should be called when the application starts to verify that * the device supports GCM. * * @param context application context. * @throws UnsupportedOperationException if the device does not support GCM. */ public static void checkDevice(Context context) { int version = Build.VERSION.SDK_INT; if (version < 8) { throw new UnsupportedOperationException("Device must be at least " + "API Level 8 (instead of " + version + ")"); } PackageManager packageManager = context.getPackageManager(); try { packageManager.getPackageInfo(GSF_PACKAGE, 0); } catch (NameNotFoundException e) { throw new UnsupportedOperationException( "Device does not have package " + GSF_PACKAGE); } } /** * Checks that the application manifest is properly configured. * <p> * A proper configuration means: * <ol> * <li>It creates a custom permission called * {@code PACKAGE_NAME.permission.C2D_MESSAGE}. * <li>It defines at least one {@link BroadcastReceiver} with category * {@code PACKAGE_NAME}. * <li>The {@link BroadcastReceiver}(s) uses the * {@value GCMConstants#PERMISSION_GCM_INTENTS} permission. * <li>The {@link BroadcastReceiver}(s) handles the 3 GCM intents * ({@value GCMConstants#INTENT_FROM_GCM_MESSAGE}, * {@value GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK}, * and {@value GCMConstants#INTENT_FROM_GCM_LIBRARY_RETRY}). * </ol> * ...where {@code PACKAGE_NAME} is the application package. * <p> * This method should be used during development time to verify that the * manifest is properly set up, but it doesn't need to be called once the * application is deployed to the users' devices. * * @param context application context. * @throws IllegalStateException if any of the conditions above is not met. */ public static void checkManifest(Context context) { PackageManager packageManager = context.getPackageManager(); String packageName = context.getPackageName(); String permissionName = packageName + ".permission.C2D_MESSAGE"; // check permission try { packageManager.getPermissionInfo(permissionName, PackageManager.GET_PERMISSIONS); } catch (NameNotFoundException e) { throw new IllegalStateException( "Application does not define permission " + permissionName); } // check receivers PackageInfo receiversInfo; try { receiversInfo = packageManager.getPackageInfo( packageName, PackageManager.GET_RECEIVERS); } catch (NameNotFoundException e) { throw new IllegalStateException( "Could not get receivers for package " + packageName); } ActivityInfo[] receivers = receiversInfo.receivers; if (receivers == null || receivers.length == 0) { throw new IllegalStateException("No receiver for package " + packageName); } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "number of receivers for " + packageName + ": " + receivers.length); } Set<String> allowedReceivers = new HashSet<String>(); for (ActivityInfo receiver : receivers) { if (GCMConstants.PERMISSION_GCM_INTENTS.equals( receiver.permission)) { allowedReceivers.add(receiver.name); } } if (allowedReceivers.isEmpty()) { throw new IllegalStateException("No receiver allowed to receive " + GCMConstants.PERMISSION_GCM_INTENTS); } checkReceiver(context, allowedReceivers, GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK); checkReceiver(context, allowedReceivers, GCMConstants.INTENT_FROM_GCM_MESSAGE); } private static void checkReceiver(Context context, Set<String> allowedReceivers, String action) { PackageManager pm = context.getPackageManager(); String packageName = context.getPackageName(); Intent intent = new Intent(action); intent.setPackage(packageName); List<ResolveInfo> receivers = pm.queryBroadcastReceivers(intent, PackageManager.GET_INTENT_FILTERS); if (receivers.isEmpty()) { throw new IllegalStateException("No receivers for action " + action); } if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Found " + receivers.size() + " receivers for action " + action); } // make sure receivers match for (ResolveInfo receiver : receivers) { String name = receiver.activityInfo.name; if (!allowedReceivers.contains(name)) { throw new IllegalStateException("Receiver " + name + " is not set with permission " + GCMConstants.PERMISSION_GCM_INTENTS); } } } /** * Initiate messaging registration for the current application. * <p> * The result will be returned as an * {@link GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK} intent with * either a {@link GCMConstants#EXTRA_REGISTRATION_ID} or * {@link GCMConstants#EXTRA_ERROR}. * * @param context application context. * @param senderIds Google Project ID of the accounts authorized to send * messages to this application. * @throws IllegalStateException if device does not have all GCM * dependencies installed. */ public static void register(Context context, String... senderIds) { GCMRegistrar.resetBackoff(context); internalRegister(context, senderIds); } static void internalRegister(Context context, String... senderIds) { String flatSenderIds = getFlatSenderIds(senderIds); Log.v(TAG, "Registering app " + context.getPackageName() + " of senders " + flatSenderIds); Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_REGISTRATION); intent.setPackage(GSF_PACKAGE); intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0)); intent.putExtra(GCMConstants.EXTRA_SENDER, flatSenderIds); context.startService(intent); } static String getFlatSenderIds(String... senderIds) { if (senderIds == null || senderIds.length == 0) { throw new IllegalArgumentException("No senderIds"); } StringBuilder builder = new StringBuilder(senderIds[0]); for (int i = 1; i < senderIds.length; i++) { builder.append(',').append(senderIds[i]); } return builder.toString(); } /** * Unregister the application. * <p> * The result will be returned as an * {@link GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK} intent with an * {@link GCMConstants#EXTRA_UNREGISTERED} extra. */ public static void unregister(Context context) { GCMRegistrar.resetBackoff(context); internalUnregister(context); } /** * Clear internal resources. * * <p> * This method should be called by the main activity's {@code onDestroy()} * method. */ public static synchronized void onDestroy(Context context) { if (sRetryReceiver != null) { Log.v(TAG, "Unregistering receiver"); context.unregisterReceiver(sRetryReceiver); sRetryReceiver = null; } } static void internalUnregister(Context context) { Log.v(TAG, "Unregistering app " + context.getPackageName()); Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_UNREGISTRATION); intent.setPackage(GSF_PACKAGE); intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context, 0, new Intent(), 0)); context.startService(intent); } /** * Lazy initializes the {@link GCMBroadcastReceiver} instance. */ static synchronized void setRetryBroadcastReceiver(Context context) { if (sRetryReceiver == null) { if (sRetryReceiverClassName == null) { // should never happen Log.e(TAG, "internal error: retry receiver class not set yet"); sRetryReceiver = new GCMBroadcastReceiver(); } else { Class<?> clazz; try { clazz = Class.forName(sRetryReceiverClassName); sRetryReceiver = (GCMBroadcastReceiver) clazz.newInstance(); } catch (Exception e) { Log.e(TAG, "Could not create instance of " + sRetryReceiverClassName + ". Using " + GCMBroadcastReceiver.class.getName() + " directly."); sRetryReceiver = new GCMBroadcastReceiver(); } } String category = context.getPackageName(); IntentFilter filter = new IntentFilter( GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY); filter.addCategory(category); // must use a permission that is defined on manifest for sure String permission = category + ".permission.C2D_MESSAGE"; Log.v(TAG, "Registering receiver"); context.registerReceiver(sRetryReceiver, filter, permission, null); } } /** * Sets the name of the retry receiver class. */ static void setRetryReceiverClassName(String className) { Log.v(TAG, "Setting the name of retry receiver class to " + className); sRetryReceiverClassName = className; } /** * Gets the current registration id for application on GCM service. * <p> * If result is empty, the registration has failed. * * @return registration id, or empty string if the registration is not * complete. */ public static String getRegistrationId(Context context) { final SharedPreferences prefs = getGCMPreferences(context); String registrationId = prefs.getString(PROPERTY_REG_ID, ""); // check if app was updated; if so, it must clear registration id to // avoid a race condition if GCM sends a message int oldVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE); int newVersion = getAppVersion(context); if (oldVersion != Integer.MIN_VALUE && oldVersion != newVersion) { Log.v(TAG, "App version changed from " + oldVersion + " to " + newVersion + "; resetting registration id"); clearRegistrationId(context); registrationId = ""; } return registrationId; } /** * Checks whether the application was successfully registered on GCM * service. */ public static boolean isRegistered(Context context) { return getRegistrationId(context).length() > 0; } /** * Clears the registration id in the persistence store. * * @param context application's context. * @return old registration id. */ static String clearRegistrationId(Context context) { return setRegistrationId(context, ""); } /** * Sets the registration id in the persistence store. * * @param context application's context. * @param regId registration id */ static String setRegistrationId(Context context, String regId) { final SharedPreferences prefs = getGCMPreferences(context); String oldRegistrationId = prefs.getString(PROPERTY_REG_ID, ""); int appVersion = getAppVersion(context); Log.v(TAG, "Saving regId on app version " + appVersion); Editor editor = prefs.edit(); editor.putString(PROPERTY_REG_ID, regId); editor.putInt(PROPERTY_APP_VERSION, appVersion); editor.commit(); return oldRegistrationId; } /** * Sets whether the device was successfully registered in the server side. */ public static void setRegisteredOnServer(Context context, boolean flag) { final SharedPreferences prefs = getGCMPreferences(context); Editor editor = prefs.edit(); editor.putBoolean(PROPERTY_ON_SERVER, flag); // set the flag's expiration date long lifespan = getRegisterOnServerLifespan(context); long expirationTime = System.currentTimeMillis() + lifespan; Log.v(TAG, "Setting registeredOnServer status as " + flag + " until " + new Timestamp(expirationTime)); editor.putLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, expirationTime); editor.commit(); } /** * Checks whether the device was successfully registered in the server side, * as set by {@link #setRegisteredOnServer(Context, boolean)}. * * <p>To avoid the scenario where the device sends the registration to the * server but the server loses it, this flag has an expiration date, which * is {@link #DEFAULT_ON_SERVER_LIFESPAN_MS} by default (but can be changed * by {@link #setRegisterOnServerLifespan(Context, long)}). */ public static boolean isRegisteredOnServer(Context context) { final SharedPreferences prefs = getGCMPreferences(context); boolean isRegistered = prefs.getBoolean(PROPERTY_ON_SERVER, false); Log.v(TAG, "Is registered on server: " + isRegistered); if (isRegistered) { // checks if the information is not stale long expirationTime = prefs.getLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, -1); if (System.currentTimeMillis() > expirationTime) { Log.v(TAG, "flag expired on: " + new Timestamp(expirationTime)); return false; } } return isRegistered; } /** * Gets how long (in milliseconds) the {@link #isRegistered(Context)} * property is valid. * * @return value set by {@link #setRegisteredOnServer(Context, boolean)} or * {@link #DEFAULT_ON_SERVER_LIFESPAN_MS} if not set. */ public static long getRegisterOnServerLifespan(Context context) { final SharedPreferences prefs = getGCMPreferences(context); long lifespan = prefs.getLong(PROPERTY_ON_SERVER_LIFESPAN, DEFAULT_ON_SERVER_LIFESPAN_MS); return lifespan; } /** * Sets how long (in milliseconds) the {@link #isRegistered(Context)} * flag is valid. */ public static void setRegisterOnServerLifespan(Context context, long lifespan) { final SharedPreferences prefs = getGCMPreferences(context); Editor editor = prefs.edit(); editor.putLong(PROPERTY_ON_SERVER_LIFESPAN, lifespan); editor.commit(); } /** * Gets the application version. */ private static int getAppVersion(Context context) { try { PackageInfo packageInfo = context.getPackageManager() .getPackageInfo(context.getPackageName(), 0); return packageInfo.versionCode; } catch (NameNotFoundException e) { // should never happen throw new RuntimeException("Coult not get package name: " + e); } } /** * Resets the backoff counter. * <p> * This method should be called after a GCM call succeeds. * * @param context application's context. */ static void resetBackoff(Context context) { Log.d(TAG, "resetting backoff for " + context.getPackageName()); setBackoff(context, DEFAULT_BACKOFF_MS); } /** * Gets the current backoff counter. * * @param context application's context. * @return current backoff counter, in milliseconds. */ static int getBackoff(Context context) { final SharedPreferences prefs = getGCMPreferences(context); return prefs.getInt(BACKOFF_MS, DEFAULT_BACKOFF_MS); } /** * Sets the backoff counter. * <p> * This method should be called after a GCM call fails, passing an * exponential value. * * @param context application's context. * @param backoff new backoff counter, in milliseconds. */ static void setBackoff(Context context, int backoff) { final SharedPreferences prefs = getGCMPreferences(context); Editor editor = prefs.edit(); editor.putInt(BACKOFF_MS, backoff); editor.commit(); } private static SharedPreferences getGCMPreferences(Context context) { return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); } private GCMRegistrar() { throw new UnsupportedOperationException(); } }