From 0b3719f0bb915fa011975a9b5544531f5af6502b Mon Sep 17 00:00:00 2001 From: vikash Date: Wed, 14 Oct 2015 01:00:42 +0530 Subject: [PATCH] Issue #211: added flag FLAG_INCLUDE_STOPPED_PACKAGES --- .../android/gcm/GCMBaseIntentService.java | 347 ++++++++++++ .../android/gcm/GCMBroadcastReceiver.java | 71 +++ .../com/google/android/gcm/GCMConstants.java | 167 ++++++ .../com/google/android/gcm/GCMRegistrar.java | 534 ++++++++++++++++++ 4 files changed, 1119 insertions(+) create mode 100644 src/android/com/google/android/gcm/GCMBaseIntentService.java create mode 100644 src/android/com/google/android/gcm/GCMBroadcastReceiver.java create mode 100644 src/android/com/google/android/gcm/GCMConstants.java create mode 100644 src/android/com/google/android/gcm/GCMRegistrar.java diff --git a/src/android/com/google/android/gcm/GCMBaseIntentService.java b/src/android/com/google/android/gcm/GCMBaseIntentService.java new file mode 100644 index 000000000..ea76211a4 --- /dev/null +++ b/src/android/com/google/android/gcm/GCMBaseIntentService.java @@ -0,0 +1,347 @@ +/* + * 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 static com.google.android.gcm.GCMConstants.ERROR_SERVICE_NOT_AVAILABLE; +import static com.google.android.gcm.GCMConstants.EXTRA_ERROR; +import static com.google.android.gcm.GCMConstants.EXTRA_REGISTRATION_ID; +import static com.google.android.gcm.GCMConstants.EXTRA_SPECIAL_MESSAGE; +import static com.google.android.gcm.GCMConstants.EXTRA_TOTAL_DELETED; +import static com.google.android.gcm.GCMConstants.EXTRA_UNREGISTERED; +import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_LIBRARY_RETRY; +import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_MESSAGE; +import static com.google.android.gcm.GCMConstants.INTENT_FROM_GCM_REGISTRATION_CALLBACK; +import static com.google.android.gcm.GCMConstants.VALUE_DELETED_MESSAGES; + +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * Skeleton for application-specific {@link IntentService}s responsible for + * handling communication from Google Cloud Messaging service. + *

+ * The abstract methods in this class are called from its worker thread, and + * hence should run in a limited amount of time. If they execute long + * operations, they should spawn new threads, otherwise the worker thread will + * be blocked. + *

+ * Subclasses must provide a public no-arg constructor. + */ +public abstract class GCMBaseIntentService extends IntentService { + + public static final String TAG = "GCMBaseIntentService"; + + // wakelock + private static final String WAKELOCK_KEY = "GCM_LIB"; + private static PowerManager.WakeLock sWakeLock; + + // Java lock used to synchronize access to sWakelock + private static final Object LOCK = GCMBaseIntentService.class; + + private final String[] mSenderIds; + + // instance counter + private static int sCounter = 0; + + private static final Random sRandom = new Random(); + + private static final int MAX_BACKOFF_MS = + (int) TimeUnit.SECONDS.toMillis(3600); // 1 hour + + // token used to check intent origin + private static final String TOKEN = + Long.toBinaryString(sRandom.nextLong()); + private static final String EXTRA_TOKEN = "token"; + + /** + * Constructor that does not set a sender id, useful when the sender id + * is context-specific. + *

+ * When using this constructor, the subclass must + * override {@link #getSenderIds(Context)}, otherwise methods such as + * {@link #onHandleIntent(Intent)} will throw an + * {@link IllegalStateException} on runtime. + */ + protected GCMBaseIntentService() { + this(getName("DynamicSenderIds"), null); + } + + /** + * Constructor used when the sender id(s) is fixed. + */ + protected GCMBaseIntentService(String... senderIds) { + this(getName(senderIds), senderIds); + } + + private GCMBaseIntentService(String name, String[] senderIds) { + super(name); // name is used as base name for threads, etc. + mSenderIds = senderIds; + } + + private static String getName(String senderId) { + String name = "GCMIntentService-" + senderId + "-" + (++sCounter); + Log.v(TAG, "Intent service name: " + name); + return name; + } + + private static String getName(String[] senderIds) { + String flatSenderIds = GCMRegistrar.getFlatSenderIds(senderIds); + return getName(flatSenderIds); + } + + /** + * Gets the sender ids. + * + *

By default, it returns the sender ids passed in the constructor, but + * it could be overridden to provide a dynamic sender id. + * + * @throws IllegalStateException if sender id was not set on constructor. + */ + protected String[] getSenderIds(Context context) { + if (mSenderIds == null) { + throw new IllegalStateException("sender id not set on constructor"); + } + return mSenderIds; + } + + /** + * Called when a cloud message has been received. + * + * @param context application's context. + * @param intent intent containing the message payload as extras. + */ + protected abstract void onMessage(Context context, Intent intent); + + /** + * Called when the GCM server tells pending messages have been deleted + * because the device was idle. + * + * @param context application's context. + * @param total total number of collapsed messages + */ + protected void onDeletedMessages(Context context, int total) { + } + + /** + * Called on a registration error that could be retried. + * + *

By default, it does nothing and returns {@literal true}, but could be + * overridden to change that behavior and/or display the error. + * + * @param context application's context. + * @param errorId error id returned by the GCM service. + * + * @return if {@literal true}, failed operation will be retried (using + * exponential backoff). + */ + protected boolean onRecoverableError(Context context, String errorId) { + return true; + } + + /** + * Called on registration or unregistration error. + * + * @param context application's context. + * @param errorId error id returned by the GCM service. + */ + protected abstract void onError(Context context, String errorId); + + /** + * Called after a device has been registered. + * + * @param context application's context. + * @param registrationId the registration id returned by the GCM service. + */ + protected abstract void onRegistered(Context context, + String registrationId); + + /** + * Called after a device has been unregistered. + * + * @param registrationId the registration id that was previously registered. + * @param context application's context. + */ + protected abstract void onUnregistered(Context context, + String registrationId); + + @Override + public final void onHandleIntent(Intent intent) { + try { + Context context = getApplicationContext(); + String action = intent.getAction(); + switch (action) { + case INTENT_FROM_GCM_REGISTRATION_CALLBACK: + GCMRegistrar.setRetryBroadcastReceiver(context); + handleRegistration(context, intent); + break; + case INTENT_FROM_GCM_MESSAGE: + // checks for special messages + String messageType = + intent.getStringExtra(EXTRA_SPECIAL_MESSAGE); + if (messageType != null) { + if (messageType.equals(VALUE_DELETED_MESSAGES)) { + String sTotal = + intent.getStringExtra(EXTRA_TOTAL_DELETED); + if (sTotal != null) { + try { + int total = Integer.parseInt(sTotal); + Log.v(TAG, "Received deleted messages " + + "notification: " + total); + onDeletedMessages(context, total); + } catch (NumberFormatException e) { + Log.e(TAG, "GCM returned invalid number of " + + "deleted messages: " + sTotal); + } + } + } else { + // application is not using the latest GCM library + Log.e(TAG, "Received unknown special message: " + + messageType); + } + } else { + onMessage(context, intent); + } + break; + case INTENT_FROM_GCM_LIBRARY_RETRY: + String token = intent.getStringExtra(EXTRA_TOKEN); + if (!TOKEN.equals(token)) { + // make sure intent was generated by this class, not by a + // malicious app. + Log.e(TAG, "Received invalid token: " + token); + return; + } + // retry last call + if (GCMRegistrar.isRegistered(context)) { + GCMRegistrar.internalUnregister(context); + } else { + String[] senderIds = getSenderIds(context); + GCMRegistrar.internalRegister(context, senderIds); + } + break; + } + } finally { + // Release the power lock, so phone can get back to sleep. + // The lock is reference-counted by default, so multiple + // messages are ok. + + // If onMessage() needs to spawn a thread or do something else, + // it should use its own lock. + synchronized (LOCK) { + // sanity check for null as this is a public method + if (sWakeLock != null) { + sWakeLock.release(); + } else { + // should never happen during normal workflow + Log.e(TAG, "Wakelock reference is null"); + } + } + } + } + + /** + * Called from the broadcast receiver. + *

+ * Will process the received intent, call handleMessage(), registered(), + * etc. in background threads, with a wake lock, while keeping the service + * alive. + */ + static void runIntentInService(Context context, Intent intent, + String className) { + synchronized (LOCK) { + if (sWakeLock == null) { + // This is called from BroadcastReceiver, there is no init. + PowerManager pm = (PowerManager) + context.getSystemService(Context.POWER_SERVICE); + sWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + WAKELOCK_KEY); + } + } + sWakeLock.acquire(); + intent.setClassName(context, className); + context.startService(intent); + } + + private void handleRegistration(final Context context, Intent intent) { + GCMRegistrar.cancelAppPendingIntent(); + String registrationId = intent.getStringExtra(EXTRA_REGISTRATION_ID); + String error = intent.getStringExtra(EXTRA_ERROR); + String unregistered = intent.getStringExtra(EXTRA_UNREGISTERED); + Log.d(TAG, "handleRegistration: registrationId = " + registrationId + + ", error = " + error + ", unregistered = " + unregistered); + + // registration succeeded + if (registrationId != null) { + GCMRegistrar.resetBackoff(context); + GCMRegistrar.setRegistrationId(context, registrationId); + onRegistered(context, registrationId); + return; + } + + // unregistration succeeded + if (unregistered != null) { + // Remember we are unregistered + GCMRegistrar.resetBackoff(context); + String oldRegistrationId = + GCMRegistrar.clearRegistrationId(context); + onUnregistered(context, oldRegistrationId); + return; + } + + // last operation (registration or unregistration) returned an error; + Log.d(TAG, "Registration error: " + error); + // Registration failed + if (ERROR_SERVICE_NOT_AVAILABLE.equals(error)) { + boolean retry = onRecoverableError(context, error); + if (retry) { + int backoffTimeMs = GCMRegistrar.getBackoff(context); + int nextAttempt = backoffTimeMs / 2 + + sRandom.nextInt(backoffTimeMs); + Log.d(TAG, "Scheduling registration retry, backoff = " + + nextAttempt + " (" + backoffTimeMs + ")"); + Intent retryIntent = + new Intent(INTENT_FROM_GCM_LIBRARY_RETRY); + retryIntent.putExtra(EXTRA_TOKEN, TOKEN); + PendingIntent retryPendingIntent = PendingIntent + .getBroadcast(context, 0, retryIntent, 0); + AlarmManager am = (AlarmManager) + context.getSystemService(Context.ALARM_SERVICE); + am.set(AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + nextAttempt, + retryPendingIntent); + // Next retry should wait longer. + if (backoffTimeMs < MAX_BACKOFF_MS) { + GCMRegistrar.setBackoff(context, backoffTimeMs * 2); + } + } else { + Log.d(TAG, "Not retrying failed operation"); + } + } else { + // Unrecoverable error, notify app + onError(context, error); + } + } + +} diff --git a/src/android/com/google/android/gcm/GCMBroadcastReceiver.java b/src/android/com/google/android/gcm/GCMBroadcastReceiver.java new file mode 100644 index 000000000..e2845aaa5 --- /dev/null +++ b/src/android/com/google/android/gcm/GCMBroadcastReceiver.java @@ -0,0 +1,71 @@ +/* + * 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 static com.google.android.gcm.GCMConstants.DEFAULT_INTENT_SERVICE_CLASS_NAME; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +/** + * {@link BroadcastReceiver} that receives GCM messages and delivers them to + * an application-specific {@link GCMBaseIntentService} subclass. + *

+ * By default, the {@link GCMBaseIntentService} class belongs to the application + * main package and is named + * {@link GCMConstants#DEFAULT_INTENT_SERVICE_CLASS_NAME}. To use a new class, + * the {@link #getGCMIntentServiceClassName(Context)} must be overridden. + */ +public class GCMBroadcastReceiver extends BroadcastReceiver { + + private static final String TAG = "GCMBroadcastReceiver"; + private static boolean mReceiverSet = false; + + @Override + public final void onReceive(Context context, Intent intent) { + Log.v(TAG, "onReceive: " + intent.getAction()); + // do a one-time check if app is using a custom GCMBroadcastReceiver + if (!mReceiverSet) { + mReceiverSet = true; + GCMRegistrar.setRetryReceiverClassName(getClass().getName()); + } + String className = getGCMIntentServiceClassName(context); + Log.v(TAG, "GCM IntentService class: " + className); + // Delegates to the application-specific intent service. + intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); + GCMBaseIntentService.runIntentInService(context, intent, className); + setResult(Activity.RESULT_OK, null /* data */, null /* extra */); + } + + /** + * Gets the class name of the intent service that will handle GCM messages. + */ + protected String getGCMIntentServiceClassName(Context context) { + return getDefaultIntentServiceClassName(context); + } + + /** + * Gets the default class name of the intent service that will handle GCM + * messages. + */ + static String getDefaultIntentServiceClassName(Context context) { + return context.getPackageName() + DEFAULT_INTENT_SERVICE_CLASS_NAME; + } +} diff --git a/src/android/com/google/android/gcm/GCMConstants.java b/src/android/com/google/android/gcm/GCMConstants.java new file mode 100644 index 000000000..f39ff07b3 --- /dev/null +++ b/src/android/com/google/android/gcm/GCMConstants.java @@ -0,0 +1,167 @@ +/* + * 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; + +/** + * Constants used by the GCM library. + */ +public final class GCMConstants { + + /** + * Intent sent to GCM to register the application. + */ + public static final String INTENT_TO_GCM_REGISTRATION = + "com.google.android.c2dm.intent.REGISTER"; + + /** + * Intent sent to GCM to unregister the application. + */ + public static final String INTENT_TO_GCM_UNREGISTRATION = + "com.google.android.c2dm.intent.UNREGISTER"; + + /** + * Intent sent by GCM indicating with the result of a registration request. + */ + public static final String INTENT_FROM_GCM_REGISTRATION_CALLBACK = + "com.google.android.c2dm.intent.REGISTRATION"; + + /** + * Intent used by the GCM library to indicate that the registration call + * should be retried. + */ + public static final String INTENT_FROM_GCM_LIBRARY_RETRY = + "com.google.android.gcm.intent.RETRY"; + + /** + * Intent sent by GCM containing a message. + */ + public static final String INTENT_FROM_GCM_MESSAGE = + "com.google.android.c2dm.intent.RECEIVE"; + + /** + * Extra used on {@value #INTENT_TO_GCM_REGISTRATION} to indicate which + * senders (Google API project ids) can send messages to the application. + */ + public static final String EXTRA_SENDER = "sender"; + + /** + * Extra used on {@value #INTENT_TO_GCM_REGISTRATION} to get the + * application info. + */ + public static final String EXTRA_APPLICATION_PENDING_INTENT = "app"; + + /** + * Extra used on {@value #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate + * that the application has been unregistered. + */ + public static final String EXTRA_UNREGISTERED = "unregistered"; + + /** + * Extra used on {@value #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate + * an error when the registration fails. See constants starting with ERROR_ + * for possible values. + */ + public static final String EXTRA_ERROR = "error"; + + /** + * Extra used on {@value #INTENT_FROM_GCM_REGISTRATION_CALLBACK} to indicate + * the registration id when the registration succeeds. + */ + public static final String EXTRA_REGISTRATION_ID = "registration_id"; + + /** + * Type of message present in the {@value #INTENT_FROM_GCM_MESSAGE} intent. + * This extra is only set for special messages sent from GCM, not for + * messages originated from the application. + */ + public static final String EXTRA_SPECIAL_MESSAGE = "message_type"; + + /** + * Special message indicating the server deleted the pending messages. + */ + public static final String VALUE_DELETED_MESSAGES = "deleted_messages"; + + /** + * Number of messages deleted by the server because the device was idle. + * Present only on messages of special type + * {@value #VALUE_DELETED_MESSAGES} + */ + public static final String EXTRA_TOTAL_DELETED = "total_deleted"; + + /** + * Extra used on {@value #INTENT_FROM_GCM_MESSAGE} to indicate which + * sender (Google API project id) sent the message. + */ + public static final String EXTRA_FROM = "from"; + + /** + * Permission necessary to receive GCM intents. + */ + public static final String PERMISSION_GCM_INTENTS = + "com.google.android.c2dm.permission.SEND"; + + /** + * @see GCMBroadcastReceiver + */ + public static final String DEFAULT_INTENT_SERVICE_CLASS_NAME = + ".GCMIntentService"; + + /** + * The device can't read the response, or there was a 500/503 from the + * server that can be retried later. The application should use exponential + * back off and retry. + */ + public static final String ERROR_SERVICE_NOT_AVAILABLE = + "SERVICE_NOT_AVAILABLE"; + + /** + * There is no Google account on the phone. The application should ask the + * user to open the account manager and add a Google account. + */ + public static final String ERROR_ACCOUNT_MISSING = + "ACCOUNT_MISSING"; + + /** + * Bad password. The application should ask the user to enter his/her + * password, and let user retry manually later. Fix on the device side. + */ + public static final String ERROR_AUTHENTICATION_FAILED = + "AUTHENTICATION_FAILED"; + + /** + * The request sent by the phone does not contain the expected parameters. + * This phone doesn't currently support GCM. + */ + public static final String ERROR_INVALID_PARAMETERS = + "INVALID_PARAMETERS"; + /** + * The sender account is not recognized. Fix on the device side. + */ + public static final String ERROR_INVALID_SENDER = + "INVALID_SENDER"; + + /** + * Incorrect phone registration with Google. This phone doesn't currently + * support GCM. + */ + public static final String ERROR_PHONE_REGISTRATION_ERROR = + "PHONE_REGISTRATION_ERROR"; + + private GCMConstants() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/android/com/google/android/gcm/GCMRegistrar.java b/src/android/com/google/android/gcm/GCMRegistrar.java new file mode 100644 index 000000000..c390a4cd9 --- /dev/null +++ b/src/android/com/google/android/gcm/GCMRegistrar.java @@ -0,0 +1,534 @@ +/* + * 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. + *

+ * Note: 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. + * + *

+ * This instance cannot be the same as the one defined in the manifest + * because it needs a different permission. + */ + // guarded by GCMRegistrar.class + private static GCMBroadcastReceiver sRetryReceiver; + + // guarded by GCMRegistrar.class + private static Context sRetryReceiverContext; + // guarded by GCMRegistrar.class + private static String sRetryReceiverClassName; + + // guarded by GCMRegistrar.class + private static PendingIntent sAppPendingIntent; + + /** + * Checks if the device has the proper dependencies installed. + *

+ * 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. + *

+ * A proper configuration means: + *

    + *
  1. It creates a custom permission called + * {@code PACKAGE_NAME.permission.C2D_MESSAGE}. + *
  2. It defines at least one {@link BroadcastReceiver} with category + * {@code PACKAGE_NAME}. + *
  3. The {@link BroadcastReceiver}(s) uses the + * {@value GCMConstants#PERMISSION_GCM_INTENTS} + * permission. + *
  4. The {@link BroadcastReceiver}(s) handles the 2 GCM intents + * ({@value GCMConstants#INTENT_FROM_GCM_MESSAGE} + * and + * {@value GCMConstants#INTENT_FROM_GCM_REGISTRATION_CALLBACK}). + *
+ * ...where {@code PACKAGE_NAME} is the application package. + *

+ * 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 allowedReceivers = new HashSet(); + 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 allowedReceivers, String action) { + PackageManager pm = context.getPackageManager(); + String packageName = context.getPackageName(); + Intent intent = new Intent(action); + intent.setPackage(packageName); + List 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. + *

+ * 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); + setPackageNameExtra(context, intent); + intent.putExtra(GCMConstants.EXTRA_SENDER, flatSenderIds); + context.startService(intent); + } + + /** + * Unregister the application. + *

+ * 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); + } + + 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); + setPackageNameExtra(context, intent); + 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(); + } + + /** + * Clear internal resources. + * + *

+ * 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 retry receiver"); + sRetryReceiverContext.unregisterReceiver(sRetryReceiver); + sRetryReceiver = null; + sRetryReceiverContext = null; + } + } + + static synchronized void cancelAppPendingIntent() { + if (sAppPendingIntent != null) { + sAppPendingIntent.cancel(); + sAppPendingIntent = null; + } + } + + private synchronized static void setPackageNameExtra(Context context, + Intent intent) { + if (sAppPendingIntent == null) { + Log.v(TAG, "Creating pending intent to get package name"); + sAppPendingIntent = PendingIntent.getBroadcast(context, 0, + new Intent(), 0); + } + intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT, + sAppPendingIntent); + } + + /** + * 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 retry receiver"); + sRetryReceiverContext = context; + sRetryReceiverContext.registerReceiver(sRetryReceiver, filter, + permission, null); + } + } + + /** + * Sets the name of the retry receiver class. + */ + static synchronized 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. + *

+ * 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.apply(); + 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.apply(); + } + + /** + * Checks whether the device was successfully registered in the server side, + * as set by {@link #setRegisteredOnServer(Context, boolean)}. + * + *

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); + return prefs.getLong(PROPERTY_ON_SERVER_LIFESPAN, + DEFAULT_ON_SERVER_LIFESPAN_MS); + } + + /** + * 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.apply(); + } + + /** + * 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. + *

+ * 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. + *

+ * 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.apply(); + } + + private static SharedPreferences getGCMPreferences(Context context) { + return context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE); + } + + private GCMRegistrar() { + throw new UnsupportedOperationException(); + } +}