Bug 870371 - Add Javascript interface to Android OrderedBroadcast. r=rnewman
authorNick Alexander <nalexander@mozilla.com>
Fri, 10 May 2013 19:47:40 -0700
changeset 142598 a0be79631c9edd8be98126e0740c7fd1249c6449
parent 142597 42c50c90ca58101cc3375840332df2e9ad88b989
child 142599 a5af2642d1d701f595971e3a679ef6d59124d050
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman
bugs870371
milestone23.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 870371 - Add Javascript interface to Android OrderedBroadcast. r=rnewman
mobile/android/base/BrowserApp.java
mobile/android/base/Makefile.in
mobile/android/base/OrderedBroadcastHelper.java
mobile/android/modules/Makefile.in
mobile/android/modules/OrderedBroadcast.jsm
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -125,16 +125,18 @@ abstract public class BrowserApp extends
     private Integer mPrefObserverId;
 
     // Tag for the AboutHome fragment. The fragment is automatically attached
     // after restoring from a saved state, so we use this tag to identify it.
     private static final String ABOUTHOME_TAG = "abouthome";
 
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
+    private OrderedBroadcastHelper mOrderedBroadcastHelper;
+
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch(msg) {
             case LOCATION_CHANGE:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     maybeCancelFaviconLoad(tab);
                 }
                 // fall through
@@ -396,16 +398,17 @@ abstract public class BrowserApp extends
         registerEventListener("Feedback:LastUrl");
         registerEventListener("Feedback:OpenPlayStore");
         registerEventListener("Feedback:MaybeLater");
         registerEventListener("Telemetry:Gather");
 
         Distribution.init(this, getPackageResourcePath());
         JavaAddonManager.getInstance().init(getApplicationContext());
         mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
+        mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
                         Tab tab = Tabs.getInstance().getSelectedTab();
@@ -496,16 +499,21 @@ abstract public class BrowserApp extends
         if (mBrowserToolbar != null)
             mBrowserToolbar.onDestroy();
 
         if (mSharedPreferencesHelper != null) {
             mSharedPreferencesHelper.uninit();
             mSharedPreferencesHelper = null;
         }
 
+        if (mOrderedBroadcastHelper != null) {
+            mOrderedBroadcastHelper.uninit();
+            mOrderedBroadcastHelper = null;
+        }
+
         unregisterEventListener("CharEncoding:Data");
         unregisterEventListener("CharEncoding:State");
         unregisterEventListener("Feedback:LastUrl");
         unregisterEventListener("Feedback:OpenPlayStore");
         unregisterEventListener("Feedback:MaybeLater");
         unregisterEventListener("Telemetry:Gather");
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -68,17 +68,17 @@ FENNEC_JAVA_FILES = \
   BrowserToolbar.java \
   BrowserToolbarBackground.java \
   BrowserToolbarLayout.java \
   CameraImageResultHandler.java \
   CameraVideoResultHandler.java \
   CanvasDelegate.java \
   CheckableLinearLayout.java \
   ClickableWhenDisabledEditText.java \
-  SyncPreference.java \
+  CustomEditText.java \
   db/BrowserDB.java \
   db/LocalBrowserDB.java \
   db/DBUtils.java \
   Distribution.java \
   Divider.java \
   DoorHanger.java \
   DoorHangerPopup.java \
   Favicons.java \
@@ -125,17 +125,17 @@ FENNEC_JAVA_FILES = \
   MenuPanel.java \
   MenuPopup.java \
   MotionEventInterceptor.java \
   MultiChoicePreference.java \
   NotificationClient.java \
   NotificationHandler.java \
   NotificationService.java \
   NSSBridge.java \
-  CustomEditText.java \
+  OrderedBroadcastHelper.java \
   PrefsHelper.java \
   PrivateDataPreference.java \
   PrivateTab.java \
   ProfileMigrator.java \
   PromptService.java \
   Restarter.java \
   SearchEngine.java \
   sqlite/ByteBufferInputStream.java \
@@ -149,16 +149,17 @@ FENNEC_JAVA_FILES = \
   SessionParser.java \
   SetupScreen.java \
   ShapedButton.java \
   SharedPreferencesHelper.java \
   SiteIdentityPopup.java \
   SmsManager.java \
   SuggestClient.java \
   SurfaceBits.java \
+  SyncPreference.java \
   Tab.java \
   TabCounter.java \
   Tabs.java \
   TabsPanel.java \
   TabsTray.java \
   TabsAccessor.java \
   TailTouchDelegate.java \
   Telemetry.java \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/OrderedBroadcastHelper.java
@@ -0,0 +1,120 @@
+/* -*- 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;
+
+import org.mozilla.gecko.util.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.app.Activity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import android.util.Log;
+
+/**
+ * Helper class to send Android Ordered Broadcasts.
+ */
+public final class OrderedBroadcastHelper
+             implements GeckoEventListener
+{
+    public static final String LOGTAG = "GeckoOrdBroadcast";
+
+    public static final String SEND_EVENT = "OrderedBroadcast:Send";
+
+    protected final Context mContext;
+
+    public OrderedBroadcastHelper(Context context) {
+        mContext = context;
+
+        EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.registerEventListener(SEND_EVENT, this);
+    }
+
+    public synchronized void uninit() {
+        EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.unregisterEventListener(SEND_EVENT, this);
+    }
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        if (!SEND_EVENT.equals(event)) {
+            Log.e(LOGTAG, "OrderedBroadcastHelper got unexpected message " + event);
+            return;
+        }
+
+        try {
+            final String action = message.getString("action");
+            if (action == null) {
+                Log.e(LOGTAG, "action must not be null");
+                return;
+            }
+
+            final String responseEvent = message.getString("responseEvent");
+            if (responseEvent == null) {
+                Log.e(LOGTAG, "responseEvent must not be null");
+                return;
+            }
+
+            // It's fine if the caller-provided token is missing or null.
+            final Object token = (message.has("token") && !message.isNull("token")) ?
+                message.getJSONObject("token") : null;
+
+            // And a missing or null permission means no permission.
+            final String permission = (message.has("permission") && !message.isNull("permission")) ?
+                message.getString("permission") : null;
+
+            final BroadcastReceiver resultReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    int code = getResultCode();
+
+                    if (code == Activity.RESULT_OK) {
+                        String data = getResultData();
+
+                        JSONObject res = new JSONObject();
+                        try {
+                            res.put("action", action);
+                            res.put("token", token);
+                            res.put("data", data);
+                        } catch (JSONException e) {
+                            Log.e(LOGTAG, "Got exception in onReceive handling action " + action, e);
+                            return;
+                        }
+
+                        GeckoEvent event = GeckoEvent.createBroadcastEvent(responseEvent, res.toString());
+                        GeckoAppShell.sendEventToGecko(event);
+                    }
+                }
+            };
+
+            Intent intent = new Intent(action);
+            mContext.sendOrderedBroadcast(intent,
+                                          permission,
+                                          resultReceiver,
+                                          null,
+                                          Activity.RESULT_OK,
+                                          null,
+                                          null);
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e);
+            return;
+        }
+    }
+}
--- a/mobile/android/modules/Makefile.in
+++ b/mobile/android/modules/Makefile.in
@@ -7,13 +7,14 @@ topsrcdir  = @top_srcdir@
 srcdir     = @srcdir@
 VPATH      = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
   LightweightThemeConsumer.jsm \
   JNI.jsm \
+  OrderedBroadcast.jsm \
   Sanitizer.jsm \
   SharedPreferences.jsm \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/OrderedBroadcast.jsm
@@ -0,0 +1,84 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["sendOrderedBroadcast"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// For adding observers.
+Cu.import("resource://gre/modules/Services.jsm");
+
+function sendMessageToJava(message) {
+  return Cc["@mozilla.org/android/bridge;1"]
+    .getService(Ci.nsIAndroidBridge)
+    .handleGeckoMessage(JSON.stringify(message));
+}
+
+let _callbackId = 1;
+
+/**
+ * Send an ordered broadcast to Java.
+ *
+ * Internally calls Context.sendOrderedBroadcast.
+ *
+ * action {String} should be a string with a qualified name (like
+ * org.mozilla.gecko.action) that will be broadcast.
+ *
+ * token {Object} is a piece of arbitrary data that will be given as
+ * a parameter to the callback (possibly null).
+ *
+ * callback {function} should accept three arguments: the data
+ * returned from Java as an Object; the specified token; and the
+ * specified action.
+ *
+ * permission {String} should be a string with an Android permission
+ * that packages must have to respond to the ordered broadcast, or
+ * null to allow all packages to respond.
+ */
+function sendOrderedBroadcast(action, token, callback, permission) {
+  let callbackId = _callbackId++;
+  let responseEvent = "OrderedBroadcast:Response:" + callbackId;
+
+  let observer = {
+    callbackId: callbackId,
+    callback: callback,
+
+    observe: function observe(subject, topic, data) {
+      if (topic != responseEvent) {
+        return;
+      }
+
+      // Unregister observer as soon as possible.
+      Services.obs.removeObserver(observer, responseEvent);
+
+      let msg = JSON.parse(data);
+      if (!msg.action || !msg.token || !msg.token.callbackId)
+        return;
+
+      let theToken = msg.token.data;
+      let theAction = msg.action;
+      let theData = msg.data ? JSON.parse(msg.data) : null;
+
+      let theCallback = this.callback;
+      if (!theCallback)
+        return;
+
+      // This is called from within a notified observer, so we don't
+      // need to take special pains to catch exceptions.
+      theCallback(theData, theToken, theAction);
+    },
+  };
+
+  Services.obs.addObserver(observer, responseEvent, false);
+
+  sendMessageToJava({
+    type: "OrderedBroadcast:Send",
+    action: action,
+    responseEvent: responseEvent,
+    token: { callbackId: callbackId, data: token || null },
+    permission: permission || null,
+  });
+};