Bug 1207714 - Part 4: Add singleton PushService. r?rnewman draft
authorNick Alexander <nalexander@mozilla.com>
Wed, 02 Mar 2016 14:14:37 -0800
changeset 336298 4d7dd8db20c99f5716ce9f170a00e1fca331920b
parent 336297 5e075b996af6f37cc96059352e4162539d8b00a3
child 515361 3fdca276f55816d312bc99584dd8e7adca9458a7
push id12029
push usernalexander@mozilla.com
push dateThu, 03 Mar 2016 00:10:36 +0000
reviewersrnewman
bugs1207714
milestone47.0a1
Bug 1207714 - Part 4: Add singleton PushService. r?rnewman MozReview-Commit-ID: CFSINSP7uFp
mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
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/push/PushService.java
mobile/android/base/moz.build
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -8,16 +8,17 @@ import org.mozilla.gecko.AdjustConstants
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mdns.MulticastDNSManager;
+import org.mozilla.gecko.push.PushService;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Application;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
@@ -160,16 +161,36 @@ public class GeckoApplication extends Ap
                 return new LocalBrowserDB(profileName);
             }
         });
 
         GeckoService.register();
 
         super.onCreate();
 
+        if (AppConstants.MOZ_ANDROID_GCM) {
+            // TODO: only run in main process.
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                    // It's fine to throw GCM initialization onto a background thread; the registration process requires
+                    // network access, so is naturally asynchronous.  This, of course, races against Gecko page load of
+                    // content requiring GCM-backed services, like Web Push.  There's nothing to be done here.
+                    PushService.createInstance(context);
+
+                    try {
+                        PushService.getInstance().onStartup();
+                    } catch (Exception e) {
+                        Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
+                        return;
+                    }
+                }
+            });
+        }
+
         if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
             DownloadContentService.startStudy(this);
         }
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmInstanceIDListenerService.java
@@ -4,16 +4,17 @@
  * 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.push.PushService;
 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 {
     /**
@@ -22,13 +23,13 @@ public class GcmInstanceIDListenerServic
      * 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();
+                PushService.getInstance().onRefresh();
             }
         });
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gcm/GcmMessageListenerService.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko.gcm;
 
 import android.os.Bundle;
 import android.util.Log;
 
 import com.google.android.gms.gcm.GcmListenerService;
 
+import org.mozilla.gecko.push.PushService;
 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 {
     /**
@@ -24,13 +25,13 @@ public class GcmMessageListenerService e
      * @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);
+                PushService.getInstance().onMessageReceived(bundle);
             }
         });
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -0,0 +1,129 @@
+/* -*- 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.push;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.gcm.GcmTokenClient;
+import org.mozilla.gecko.push.autopush.AutopushClientException;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Class that handles messages used in the Google Cloud Messaging and DOM push API integration.
+ * <p/>
+ * This singleton services Gecko messages from dom/push/PushServiceAndroidGCM.jsm and Google Cloud
+ * Messaging requests.
+ * <p/>
+ * It's worth noting that we allow the DOM push API in restricted profiles.
+ */
+public class PushService {
+    private static final String LOG_TAG = "GeckoPushService";
+
+    public static final String SERVICE_WEBPUSH = "webpush";
+
+    private static PushService sInstance;
+
+    public static synchronized PushService getInstance() {
+        if (sInstance == null) {
+            throw new IllegalStateException("PushService not yet created!");
+        }
+        return sInstance;
+    }
+
+    public static synchronized PushService createInstance(Context context) {
+        if (sInstance != null) {
+            throw new IllegalStateException("PushService already created!");
+        }
+        sInstance = new PushService(context);
+        return sInstance;
+    }
+
+    protected final PushManager pushManager;
+
+    public PushService(Context context) {
+        pushManager = new PushManager(new PushState(context, "GeckoPushState.json"), new GcmTokenClient(context), new PushManager.PushClientFactory() {
+            @Override
+            public PushClient getPushClient(String autopushEndpoint, boolean debug) {
+                return new PushClient(autopushEndpoint);
+            }
+        });
+    }
+
+    public void onStartup() {
+        Log.i(LOG_TAG, "Starting up.");
+        ThreadUtils.assertOnBackgroundThread();
+
+        try {
+            pushManager.startup(System.currentTimeMillis());
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
+            return;
+        }
+    }
+
+    public void onRefresh() {
+        Log.i(LOG_TAG, "Google Play Services requested GCM token refresh; invalidating GCM token and running startup again.");
+        ThreadUtils.assertOnBackgroundThread();
+
+        pushManager.invalidateGcmToken();
+        try {
+            pushManager.startup(System.currentTimeMillis());
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
+            return;
+        }
+    }
+
+    public void onMessageReceived(final @NonNull Bundle bundle) {
+        Log.i(LOG_TAG, "Google Play Services GCM message received; delivering.");
+        ThreadUtils.assertOnBackgroundThread();
+
+        final String chid = bundle.getString("chid");
+        if (chid == null) {
+            Log.w(LOG_TAG, "No chid found; ignoring message.");
+            return;
+        }
+
+        final PushRegistration registration = pushManager.registrationForSubscription(chid);
+        if (registration == null) {
+            Log.w(LOG_TAG, "Cannot find registration corresponding to subscription for chid: " + chid + "; ignoring message.");
+            return;
+        }
+
+        final PushSubscription subscription = registration.getSubscription(chid);
+        if (subscription == null) {
+            // This should never happen.  There's not much to be done; in the future, perhaps we
+            // could try to drop the remote subscription?
+            Log.e(LOG_TAG, "No subscription found for chid: " + chid + "; ignoring message.");
+            return;
+        }
+
+        Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
+
+        if (SERVICE_WEBPUSH.equals(subscription.service)) {
+            // Nothing yet.
+            Log.i(LOG_TAG, "Message directed to unimplemented service; ignoring: " + subscription.service);
+            return;
+        } else {
+            Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -646,16 +646,30 @@ gbjar.extra_jars += [
     CONFIG['ANDROID_SUPPORT_V4_AAR_LIB'],
     CONFIG['ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB'],
     'constants.jar'
 ]
 if CONFIG['MOZ_CRASHREPORTER']:
     gbjar.sources += [ 'java/org/mozilla/gecko/CrashReporter.java' ]
     ANDROID_RES_DIRS += [ 'crashreporter/res' ]
 
+if CONFIG['MOZ_ANDROID_GCM']:
+    gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
+        'gcm/GcmInstanceIDListenerService.java',
+        'gcm/GcmMessageListenerService.java',
+        'gcm/GcmTokenClient.java',
+        'push/Fetched.java',
+        'push/PushClient.java',
+        'push/PushManager.java',
+        'push/PushRegistration.java',
+        'push/PushService.java',
+        'push/PushState.java',
+        'push/PushSubscription.java',
+    ]]
+
 if (CONFIG['MOZ_ANDROID_MAX_SDK_VERSION']):
     max_sdk_version = int(CONFIG['MOZ_ANDROID_MAX_SDK_VERSION'])
 else:
     max_sdk_version = 999
 
 # Only bother to include new tablet code if we're building for tablet-capable
 # OS releases.
 if max_sdk_version >= 11: