From f7981eb37203e7b954329a99e7d208616663a12a Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 2 Mar 2016 15:45:24 -0800 Subject: [PATCH] Bug 1207714 - Part 1: Register no-op GCM message listeners. r=rnewman MozReview-Commit-ID: 4n7IcTuGQVE --- mobile/android/base/AndroidManifest.xml.in | 4 + .../base/GcmAndroidManifest_services.xml.in | 29 ++++ .../gcm/GcmInstanceIDListenerService.java | 34 +++++ .../gecko/gcm/GcmMessageListenerService.java | 36 +++++ .../org/mozilla/gecko/gcm/GcmTokenClient.java | 131 ++++++++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 mobile/android/base/GcmAndroidManifest_services.xml.in create mode 100644 mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java create mode 100644 mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java create mode 100644 mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java diff --git a/mobile/android/base/AndroidManifest.xml.in b/mobile/android/base/AndroidManifest.xml.in index 2e4d2dbf54e..3d46b053e5b 100644 --- a/mobile/android/base/AndroidManifest.xml.in +++ b/mobile/android/base/AndroidManifest.xml.in @@ -357,6 +357,10 @@ #endif #ifdef MOZ_ANDROID_MLS_STUMBLER #include ../stumbler/manifests/StumblerManifest_services.xml.in +#endif + +#ifdef MOZ_ANDROID_GCM +#include GcmAndroidManifest_services.xml.in #endif diff --git a/mobile/android/base/GcmAndroidManifest_services.xml.in b/mobile/android/base/GcmAndroidManifest_services.xml.in new file mode 100644 index 00000000000..de1c77b3be0 --- /dev/null +++ b/mobile/android/base/GcmAndroidManifest_services.xml.in @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java new file mode 100644 index 00000000000..371049126af --- /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(); + } + }); + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java new file mode 100644 index 00000000000..d7ba8447105 --- /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); + } + }); + } +} diff --git a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmTokenClient.java new file mode 100644 index 00000000000..024905eb090 --- /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. + *

+ * GCM tokens are stable and long lived. Google Play Services will periodically request that + * they are rotated, however: see + * https://developers.google.com/instance-id/guides/android-implementation. + *

+ * 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); + } + } +}