Bug 1207714 - Part 1: Register no-op GCM message listeners. r?rnewman draft
authorNick Alexander <nalexander@mozilla.com>
Wed, 02 Mar 2016 15:45:24 -0800
changeset 336294 74dd6ab391fcd67dfcd2d5794edfad327e72b76e
parent 336293 aa358e6914932fd7372e56703a53797336633093
child 336295 bd71061e26bad26960dfaa5e3eaefb0cfd33939d
push id12029
push usernalexander@mozilla.com
push dateThu, 03 Mar 2016 00:10:36 +0000
reviewersrnewman
bugs1207714
milestone47.0a1
Bug 1207714 - Part 1: Register no-op GCM message listeners. r?rnewman MozReview-Commit-ID: 4n7IcTuGQVE
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/GcmAndroidManifest_services.xml.in
mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java
--- 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);
+        }
+    }
+}