Bug 1207714 - Part 1: Register no-op GCM message listeners. r?rnewman
MozReview-Commit-ID: 4n7IcTuGQVE
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -349,10 +349,14 @@
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
#include ../search/manifests/SearchAndroidManifest_services.xml.in
#endif
#ifdef MOZ_ANDROID_MLS_STUMBLER
#include ../stumbler/manifests/StumblerManifest_services.xml.in
#endif
+#ifdef MOZ_ANDROID_GCM
+#include GcmAndroidManifest_services.xml.in
+#endif
+
</application>
</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/GcmAndroidManifest_services.xml.in
@@ -0,0 +1,29 @@
+ <!-- Handle GCM registration updates from on-device Google Play Services. -->
+ <service
+ android:name="org.mozilla.gecko.gcm.GcmInstanceIDListenerService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.gms.iid.InstanceID"/>
+ </intent-filter>
+ </service>
+
+ <!-- Provided by on-device Google Play Services. Directs inbound messages to internal listener service. -->
+ <receiver
+ android:name="com.google.android.gms.gcm.GcmReceiver"
+ android:exported="true"
+ android:permission="com.google.android.c2dm.permission.SEND">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+ <category android:name="@ANDROID_PACKAGE_NAME@" />
+ </intent-filter>
+ </receiver>
+
+ <!-- Handle messages directed by the GCM receiver. -->
+ <service
+ android:name="org.mozilla.gecko.gcm.GcmMessageListenerService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ </intent-filter>
+ </service>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
@@ -0,0 +1,34 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gcm;
+
+import android.util.Log;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * This service is notified by the on-device Google Play Services library if an
+ * in-use token needs to be updated. We simply pass through to AndroidPushService.
+ */
+public class GcmInstanceIDListenerService extends InstanceIDListenerService {
+ /**
+ * Called if InstanceID token is updated. This may occur if the security of
+ * the previous token had been compromised. This call is initiated by the
+ * InstanceID provider.
+ */
+ @Override
+ public void onTokenRefresh() {
+ Log.d("GeckoPushGCM", "Token refresh request received. Processing on background thread.");
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // TODO: PushService.getInstance().onRefresh();
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
@@ -0,0 +1,36 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gcm;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.android.gms.gcm.GcmListenerService;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * This service actually handles messages directed from the on-device Google
+ * Play Services package. We simply route them to the AndroidPushService.
+ */
+public class GcmMessageListenerService extends GcmListenerService {
+ /**
+ * Called when message is received.
+ *
+ * @param from SenderID of the sender.
+ * @param bundle Data bundle containing message data as key/value pairs.
+ */
+ @Override
+ public void onMessageReceived(final String from, final Bundle bundle) {
+ Log.d("GeckoPushGCM", "Message received. Processing on background thread.");
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ // PushService.getInstance().onMessageReceived(bundle);
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java
@@ -0,0 +1,131 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.gcm;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import com.google.android.gms.common.ConnectionResult;
+import com.google.android.gms.common.GoogleApiAvailability;
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.push.Fetched;
+
+import java.io.IOException;
+
+/**
+ * Fetch and cache GCM tokens.
+ * <p/>
+ * GCM tokens are stable and long lived. Google Play Services will periodically request that
+ * they are rotated, however: see
+ * <a href="https://developers.google.com/instance-id/guides/android-implementation">https://developers.google.com/instance-id/guides/android-implementation</a>.
+ * <p/>
+ * The GCM token is cached in the App-wide shared preferences. There's no particular harm in
+ * requesting new tokens, so if the user clears the App data, that's fine -- we'll get a fresh
+ * token and Push will react accordingly.
+ */
+public class GcmTokenClient {
+ private static final String LOG_TAG = "GeckoPushGCM";
+
+ private static final String KEY_GCM_TOKEN = "gcm_token";
+ private static final String KEY_GCM_TOKEN_TIMESTAMP = "gcm_token_timestamp";
+
+ private final Context context;
+
+ public GcmTokenClient(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Check the device to make sure it has the Google Play Services APK.
+ * @param context Android context.
+ */
+ protected void ensurePlayServices(Context context) throws NeedsGooglePlayServicesException {
+ final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+ int resultCode = apiAvailability.isGooglePlayServicesAvailable(context);
+ if (resultCode != ConnectionResult.SUCCESS) {
+ Log.w(LOG_TAG, "This device does not support GCM! isGooglePlayServicesAvailable returned: " + resultCode);
+ Log.w(LOG_TAG, "isGooglePlayServicesAvailable message: " + apiAvailability.getErrorString(resultCode));
+ throw new NeedsGooglePlayServicesException(resultCode);
+ }
+ }
+
+ /**
+ * Get a GCM token (possibly cached).
+ *
+ * @param senderID to request token for.
+ * @param debug whether to log debug details.
+ * @return token and timestamp.
+ * @throws NeedsGooglePlayServicesException if user action is needed to use Google Play Services.
+ * @throws IOException if the token fetch failed.
+ */
+ public @NonNull Fetched getToken(@NonNull String senderID, boolean debug) throws NeedsGooglePlayServicesException, IOException {
+ ensurePlayServices(this.context);
+
+ final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context);
+ String token = sharedPrefs.getString(KEY_GCM_TOKEN, null);
+ long timestamp = sharedPrefs.getLong(KEY_GCM_TOKEN_TIMESTAMP, 0L);
+ if (token != null && timestamp > 0L) {
+ if (debug) {
+ Log.i(LOG_TAG, "Cached GCM token exists: " + token);
+ } else {
+ Log.i(LOG_TAG, "Cached GCM token exists.");
+ }
+ return new Fetched(token, timestamp);
+ }
+
+ Log.i(LOG_TAG, "Cached GCM token does not exist; requesting new token with sender ID: " + senderID);
+
+ final InstanceID instanceID = InstanceID.getInstance(context);
+ token = instanceID.getToken(senderID, GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
+ timestamp = System.currentTimeMillis();
+
+ if (debug) {
+ Log.i(LOG_TAG, "Got fresh GCM token; caching: " + token);
+ } else {
+ Log.i(LOG_TAG, "Got fresh GCM token; caching.");
+ }
+ sharedPrefs
+ .edit()
+ .putString(KEY_GCM_TOKEN, token)
+ .putLong(KEY_GCM_TOKEN_TIMESTAMP, timestamp)
+ .apply();
+
+ return new Fetched(token, timestamp);
+ }
+
+ /**
+ * Remove any cached GCM token.
+ */
+ public void invalidateToken() {
+ final SharedPreferences sharedPrefs = GeckoSharedPrefs.forApp(context);
+ sharedPrefs
+ .edit()
+ .remove(KEY_GCM_TOKEN)
+ .remove(KEY_GCM_TOKEN_TIMESTAMP)
+ .apply();
+ }
+
+ public class NeedsGooglePlayServicesException extends Exception {
+ private static final long serialVersionUID = 4132853166L;
+
+ private final int resultCode;
+
+ NeedsGooglePlayServicesException(int resultCode) {
+ super();
+ this.resultCode = resultCode;
+ }
+
+ public void showErrorNotification() {
+ final GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
+ apiAvailability.showErrorNotification(context, resultCode);
+ }
+ }
+}