Bug 1319496 - 3. Convert prompts to BundleEventListener; r=sebastian
authorJim Chen <nchen@mozilla.com>
Tue, 29 Nov 2016 12:25:53 -0500
changeset 324719 10a6565daede03fcb1b5d4c7bfbfa499dfec404c
parent 324718 15407fdf9c663cfd195b28920da5a2d9084eba49
child 324720 16dbb57af3e2888827ef2db6d6e17d28cdab1379
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewerssebastian
bugs1319496
milestone53.0a1
Bug 1319496 - 3. Convert prompts to BundleEventListener; r=sebastian Convert prompts to use BundleEventListener and GeckoBundle. DefaultDoorHanger.setOptions accepts a JSONObject argument, but if we converted it to GeckoBundle, it would involve a lot of extra changes in the other doorhanger code. So this patch adds GeckoBundle.fromJSONObject and converts JSONObject to GeckoBundle within DefaultDoorHanger.setOptions. In the future, another patch would convert all doorhanger code to use GeckoBundle instead of JSONObject.
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
mobile/android/modules/Prompt.jsm
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -93,19 +93,21 @@ import org.mozilla.gecko.toolbar.Autocom
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.PostUpdateHandler;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
+import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.StringUtils;
@@ -195,17 +197,18 @@ public class BrowserApp extends GeckoApp
                                    View.OnKeyListener,
                                    LayerView.DynamicToolbarListener,
                                    BrowserSearch.OnSearchListener,
                                    BrowserSearch.OnEditSuggestionListener,
                                    OnUrlOpenListener,
                                    OnUrlOpenInBackgroundListener,
                                    AnchoredPopup.OnVisibilityChangeListener,
                                    ActionModeCompat.Presenter,
-                                   LayoutInflater.Factory {
+                                   LayoutInflater.Factory,
+                                   BundleEventListener {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     // Intent String extras used to specify custom Switchboard configurations.
     private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
 
     // TODO: Replace with kinto endpoint.
@@ -724,17 +727,16 @@ public class BrowserApp extends GeckoApp
         mDoorhangerOverlay = findViewById(R.id.doorhanger_overlay);
 
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
             "Gecko:DelayedStartup",
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
-            "Prompt:ShowTop",
             "Tab:Added",
             "Video:Play");
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
             "CharEncoding:Data",
             "CharEncoding:State",
             "Download:AndroidDownloadManager",
             "Experiments:GetActive",
@@ -746,16 +748,18 @@ public class BrowserApp extends GeckoApp
             "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch",
             "Website:Metadata");
 
+        getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
+
         final GeckoProfile profile = getProfile();
 
         // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
         // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
         final Distribution distribution = Distribution.init(getApplicationContext());
         distribution.addOnDistributionReadyCallback(
                 new DistributionStoreCallback(getApplicationContext(), profile.getName()));
 
@@ -1055,18 +1059,17 @@ public class BrowserApp extends GeckoApp
     @Override
     public void onResume() {
         super.onResume();
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         if (!mHasResumed) {
-            EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
-                    "Prompt:ShowTop");
+            getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
             mHasResumed = true;
         }
 
         processTabQueue();
 
         for (BrowserAppDelegate delegate : delegates) {
             delegate.onResume(this);
         }
@@ -1076,18 +1079,17 @@ public class BrowserApp extends GeckoApp
     public void onPause() {
         super.onPause();
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         if (mHasResumed) {
             // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
-            EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
-                "Prompt:ShowTop");
+            getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
             mHasResumed = false;
         }
 
         for (BrowserAppDelegate delegate : delegates) {
             delegate.onPause(this);
         }
     }
 
@@ -1429,17 +1431,16 @@ public class BrowserApp extends GeckoApp
         mSearchEngineManager.unregisterListeners();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Gecko:DelayedStartup",
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
-            "Prompt:ShowTop",
             "Tab:Added",
             "Video:Play");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "CharEncoding:Data",
             "CharEncoding:State",
             "Download:AndroidDownloadManager",
             "Experiments:GetActive",
@@ -1451,16 +1452,18 @@ public class BrowserApp extends GeckoApp
             "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch",
             "Website:Metadata");
 
+        getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
+
         if (AppConstants.MOZ_ANDROID_BEAM) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 // null this out even though the docs say it's not needed,
                 // because the source code looks like it will only do this
                 // automatically on API 14+
                 nfc.setNdefPushMessageCallback(null, this);
             }
@@ -1695,16 +1698,31 @@ public class BrowserApp extends GeckoApp
 
         if (mTabStrip != null) {
             mTabStrip.refresh();
         }
 
         mBrowserToolbar.refresh();
     }
 
+    @Override // BundleEventListener
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        switch (event) {
+            case "Prompt:ShowTop":
+                // Bring this activity to front so the prompt is visible..
+                Intent bringToFrontIntent = new Intent();
+                bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
+                                                AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+                bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+                startActivity(bringToFrontIntent);
+                break;
+        }
+    }
+
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
         switch (event) {
             case "CharEncoding:Data":
                 final NativeJSObject[] charsets = message.getObjectArray("charsets");
                 final int selected = message.getInt("selected");
 
@@ -2106,24 +2124,16 @@ public class BrowserApp extends GeckoApp
                             public void run() {
                                 mVideoPlayer.start(Uri.parse(uri));
                                 Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTENT, "playhls");
                             }
                         });
                     }
                     break;
 
-                case "Prompt:ShowTop":
-                    // Bring this activity to front so the prompt is visible..
-                    Intent bringToFrontIntent = new Intent();
-                    bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
-                    bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
-                    startActivity(bringToFrontIntent);
-                    break;
-
                 case "Tab:Added":
                     if (message.getBoolean("cancelEditMode")) {
                         ThreadUtils.postToUiThread(new Runnable() {
                             @Override
                             public void run() {
                                 // Set the target tab to null so it does not get selected (on editing
                                 // mode exit) in lieu of the tab that we're going to open and select.
                                 mTargetTabForEditingMode = null;
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/ColorPickerInput.java
@@ -1,34 +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.prompts;
 
-import org.json.JSONObject;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.widget.BasicColorPicker;
 
 import android.content.Context;
 import android.graphics.Color;
 import android.view.LayoutInflater;
 import android.view.View;
 
 public class ColorPickerInput extends PromptInput {
     public static final String INPUT_TYPE = "color";
     public static final String LOGTAG = "GeckoColorPickerInput";
 
     private final boolean mShowAdvancedButton = true;
     private final int mInitialColor;
 
-    public ColorPickerInput(JSONObject obj) {
+    public ColorPickerInput(GeckoBundle obj) {
         super(obj);
-        String init = obj.optString("value");
+        String init = obj.getString("value");
         mInitialColor = Color.rgb(Integer.parseInt(init.substring(1, 3), 16),
                                   Integer.parseInt(init.substring(3, 5), 16),
                                   Integer.parseInt(init.substring(5, 7), 16));
     }
 
     @Override
     public View getView(Context context) throws UnsupportedOperationException {
         LayoutInflater inflater = LayoutInflater.from(context);
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/IconGridInput.java
@@ -3,20 +3,19 @@
  * 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.prompts;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ResourceDrawableUtils;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.view.Display;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -34,21 +33,22 @@ public class IconGridInput extends Promp
     public static final String LOGTAG = "GeckoIconGridInput";
 
     private ArrayAdapter<IconGridItem> mAdapter; // An adapter holding a list of items to show in the grid
 
     private static int mColumnWidth = -1;  // The maximum width of columns
     private static int mMaxColumns = -1;  // The maximum number of columns to show
     private static int mIconSize = -1;    // Size of icons in the grid
     private int mSelected;                // Current selection
-    private final JSONArray mArray;
+    private final GeckoBundle[] mArray;
 
-    public IconGridInput(JSONObject obj) {
+    public IconGridInput(GeckoBundle obj) {
         super(obj);
-        mArray = obj.optJSONArray("items");
+        final GeckoBundle[] array = obj.getBundleArray("items");
+        mArray = array != null ? array : new GeckoBundle[0];
     }
 
     @Override
     public View getView(Context context) throws UnsupportedOperationException {
         if (mColumnWidth < 0) {
             // getColumnWidth isn't available on pre-ICS, so we pull it out and assign it here
             mColumnWidth = context.getResources().getDimensionPixelSize(R.dimen.icongrid_columnwidth);
         }
@@ -65,19 +65,19 @@ public class IconGridInput extends Promp
         final WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
         final Display display = wm.getDefaultDisplay();
         final int screenWidth = display.getWidth();
         int maxColumns = Math.min(mMaxColumns, screenWidth / mColumnWidth);
 
         final GridView view = (GridView) LayoutInflater.from(context).inflate(R.layout.icon_grid, null, false);
         view.setColumnWidth(mColumnWidth);
 
-        final ArrayList<IconGridItem> items = new ArrayList<IconGridItem>(mArray.length());
-        for (int i = 0; i < mArray.length(); i++) {
-            IconGridItem item = new IconGridItem(context, mArray.optJSONObject(i));
+        final ArrayList<IconGridItem> items = new ArrayList<IconGridItem>(mArray.length);
+        for (int i = 0; i < mArray.length; i++) {
+            IconGridItem item = new IconGridItem(context, mArray[i]);
             items.add(item);
             if (item.selected) {
                 mSelected = i;
             }
         }
 
         view.setNumColumns(Math.min(items.size(), maxColumns));
         view.setOnItemClickListener(this);
@@ -146,21 +146,21 @@ public class IconGridInput extends Promp
     }
 
     private class IconGridItem {
         final String label;
         final String description;
         final boolean selected;
         Drawable icon;
 
-        public IconGridItem(final Context context, final JSONObject obj) {
-            label = obj.optString("name");
-            final String iconUrl = obj.optString("iconUri");
-            description = obj.optString("description");
-            selected = obj.optBoolean("selected");
+        public IconGridItem(final Context context, final GeckoBundle obj) {
+            label = obj.getString("name");
+            final String iconUrl = obj.getString("iconUri");
+            description = obj.getString("description");
+            selected = obj.getBoolean("selected");
 
             ResourceDrawableUtils.getDrawable(context, iconUrl, new ResourceDrawableUtils.BitmapLoader() {
                 @Override
                 public void onBitmapFound(Drawable d) {
                     icon = d;
                     if (mAdapter != null) {
                         mAdapter.notifyDataSetChanged();
                     }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
@@ -6,16 +6,17 @@
 package org.mozilla.gecko.prompts;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 
 import android.app.AlertDialog;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
@@ -73,48 +74,44 @@ public class Prompt implements OnClickLi
     private View applyInputStyle(View view, PromptInput input) {
         // Don't add padding to color picker views
         if (input.canApplyInputStyle()) {
             view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
         }
         return view;
     }
 
-    public void show(JSONObject message) {
-        String title = message.optString("title");
-        String text = message.optString("text");
-        mGuid = message.optString("guid");
+    public void show(GeckoBundle message) {
+        String title = message.getString("title");
+        String text = message.getString("text");
+        mGuid = message.getString("guid");
 
-        mButtons = getStringArray(message, "buttons");
+        mButtons = message.getStringArray("buttons");
         final int buttonCount = mButtons == null ? 0 : mButtons.length;
-        mDoubleTapButtonType = convertIndexToButtonType(message.optInt("doubleTapButton", -1), buttonCount);
+        mDoubleTapButtonType = convertIndexToButtonType(message.getInt("doubleTapButton", -1), buttonCount);
         mPreviousInputValue = null;
 
-        JSONArray inputs = getSafeArray(message, "inputs");
-        mInputs = new PromptInput[inputs.length()];
+        GeckoBundle[] inputs = message.getBundleArray("inputs");
+        mInputs = new PromptInput[inputs != null ? inputs.length : 0];
         for (int i = 0; i < mInputs.length; i++) {
-            try {
-                mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
-                mInputs[i].setListener(this);
-            } catch (Exception ex) { }
+            mInputs[i] = PromptInput.getInput(inputs[i]);
+            mInputs[i].setListener(this);
         }
 
-        PromptListItem[] menuitems = PromptListItem.getArray(message.optJSONArray("listitems"));
-        String selected = message.optString("choiceMode");
+        PromptListItem[] menuitems = PromptListItem.getArray(message.getBundleArray("listitems"));
+        String selected = message.getString("choiceMode");
 
         int choiceMode = ListView.CHOICE_MODE_NONE;
         if ("single".equals(selected)) {
             choiceMode = ListView.CHOICE_MODE_SINGLE;
         } else if ("multiple".equals(selected)) {
             choiceMode = ListView.CHOICE_MODE_MULTIPLE;
         }
 
-        if (message.has("tabId")) {
-            mTabId = message.optInt("tabId", Tabs.INVALID_TAB_ID);
-        }
+        mTabId = message.getInt("tabId", Tabs.INVALID_TAB_ID);
 
         show(title, text, menuitems, choiceMode);
     }
 
     private int convertIndexToButtonType(final int buttonIndex, final int buttonCount) {
         if (buttonIndex < 0 || buttonIndex >= buttonCount) {
             // All valid DialogInterface button values are < 0,
             // so we return 0 as an invalid value.
@@ -535,51 +532,16 @@ public class Prompt implements OnClickLi
         if (mDoubleTapButtonType != 0 && inputValue == mPreviousInputValue) {
             closeDialog(mDoubleTapButtonType);
             return true;
         }
         mPreviousInputValue = inputValue;
         return false;
     }
 
-    private static JSONArray getSafeArray(JSONObject json, String key) {
-        try {
-            return json.getJSONArray(key);
-        } catch (Exception e) {
-            return new JSONArray();
-        }
-    }
-
-    public static String[] getStringArray(JSONObject aObject, String aName) {
-        JSONArray items = getSafeArray(aObject, aName);
-        int length = items.length();
-        String[] list = new String[length];
-        for (int i = 0; i < length; i++) {
-            try {
-                list[i] = items.getString(i);
-            } catch (Exception ex) { }
-        }
-        return list;
-    }
-
-    private static boolean[] getBooleanArray(JSONObject aObject, String aName) {
-        JSONArray items = new JSONArray();
-        try {
-            items = aObject.getJSONArray(aName);
-        } catch (Exception ex) { return null; }
-        int length = items.length();
-        boolean[] list = new boolean[length];
-        for (int i = 0; i < length; i++) {
-            try {
-                list[i] = items.getBoolean(i);
-            } catch (Exception ex) { }
-        }
-        return list;
-    }
-
     public interface PromptCallback {
 
         /**
          * Called when the Prompt has been completed (i.e. when the user has selected an item or action in the Prompt).
          * This callback is run on the UI thread.
          */
         public void onPromptFinished(String jsonResult);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptInput.java
@@ -4,18 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.prompts;
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
 
-import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.widget.AllCapsTextView;
 import org.mozilla.gecko.widget.DateTimePicker;
 
 import android.content.Context;
 import android.content.res.Configuration;
 import android.support.design.widget.TextInputLayout;
 import android.support.v7.widget.AppCompatCheckBox;
 import android.text.Html;
@@ -54,20 +54,20 @@ public abstract class PromptInput {
         mListener = listener;
     }
 
     public static class EditInput extends PromptInput {
         protected final String mHint;
         protected final boolean mAutofocus;
         public static final String INPUT_TYPE = "textbox";
 
-        public EditInput(JSONObject object) {
+        public EditInput(GeckoBundle object) {
             super(object);
-            mHint = object.optString("hint");
-            mAutofocus = object.optBoolean("autofocus");
+            mHint = object.getString("hint");
+            mAutofocus = object.getBoolean("autofocus");
         }
 
         @Override
         public View getView(final Context context) throws UnsupportedOperationException {
             EditText input = new EditText(context);
             input.setInputType(InputType.TYPE_CLASS_TEXT);
             input.setText(mValue);
 
@@ -98,34 +98,34 @@ public abstract class PromptInput {
         public Object getValue() {
             final TextInputLayout inputLayout = (TextInputLayout) mView;
             return inputLayout.getEditText().getText();
         }
     }
 
     public static class NumberInput extends EditInput {
         public static final String INPUT_TYPE = "number";
-        public NumberInput(JSONObject obj) {
+        public NumberInput(GeckoBundle obj) {
             super(obj);
         }
 
         @Override
         public View getView(final Context context) throws UnsupportedOperationException {
             final TextInputLayout inputLayout = (TextInputLayout) super.getView(context);
             final EditText input = inputLayout.getEditText();
             input.setRawInputType(Configuration.KEYBOARD_12KEY);
             input.setInputType(InputType.TYPE_CLASS_NUMBER |
                                InputType.TYPE_NUMBER_FLAG_SIGNED);
             return input;
         }
     }
 
     public static class PasswordInput extends EditInput {
         public static final String INPUT_TYPE = "password";
-        public PasswordInput(JSONObject obj) {
+        public PasswordInput(GeckoBundle obj) {
             super(obj);
         }
 
         @Override
         public View getView(Context context) throws UnsupportedOperationException {
             final TextInputLayout inputLayout = (TextInputLayout) super.getView(context);
             inputLayout.getEditText().setInputType(InputType.TYPE_CLASS_TEXT |
                                InputType.TYPE_TEXT_VARIATION_PASSWORD |
@@ -133,19 +133,19 @@ public abstract class PromptInput {
             return inputLayout;
         }
     }
 
     public static class CheckboxInput extends PromptInput {
         public static final String INPUT_TYPE = "checkbox";
         private final boolean mChecked;
 
-        public CheckboxInput(JSONObject obj) {
+        public CheckboxInput(GeckoBundle obj) {
             super(obj);
-            mChecked = obj.optBoolean("checked");
+            mChecked = obj.getBoolean("checked");
         }
 
         @Override
         public View getView(Context context) throws UnsupportedOperationException {
             final CheckBox checkbox = new AppCompatCheckBox(context);
             checkbox.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
             checkbox.setText(mLabel);
             checkbox.setChecked(mChecked);
@@ -165,17 +165,17 @@ public abstract class PromptInput {
             "date",
             "week",
             "time",
             "datetime-local",
             "datetime",
             "month"
         };
 
-        public DateTimeInput(JSONObject obj) {
+        public DateTimeInput(GeckoBundle obj) {
             super(obj);
         }
 
         @Override
         public View getView(Context context) throws UnsupportedOperationException {
             if (mType.equals("date")) {
                 try {
                     DateTimePicker input = new DateTimePicker(context, "yyyy-MM-dd", mValue,
@@ -260,26 +260,27 @@ public abstract class PromptInput {
                 }
             }
             return super.getValue();
         }
     }
 
     public static class MenulistInput extends PromptInput {
         public static final String INPUT_TYPE = "menulist";
-        private static String[] mListitems;
-        private static int mSelected;
+        private final String[] mListitems;
+        private final int mSelected;
 
         public Spinner spinner;
         public AllCapsTextView textView;
 
-        public MenulistInput(JSONObject obj) {
+        public MenulistInput(GeckoBundle obj) {
             super(obj);
-            mListitems = Prompt.getStringArray(obj, "values");
-            mSelected = obj.optInt("selected");
+            final String[] listitems = obj.getStringArray("values");
+            mListitems = listitems != null ? listitems : new String[0];
+            mSelected = obj.getInt("selected");
         }
 
         @Override
         public View getView(final Context context) throws UnsupportedOperationException {
             spinner = new Spinner(context, Spinner.MODE_DIALOG);
             try {
                 if (mListitems.length > 0) {
                     ArrayAdapter<String> adapter = new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item, mListitems);
@@ -309,42 +310,42 @@ public abstract class PromptInput {
         @Override
         public Object getValue() {
             return spinner.getSelectedItemPosition();
         }
     }
 
     public static class LabelInput extends PromptInput {
         public static final String INPUT_TYPE = "label";
-        public LabelInput(JSONObject obj) {
+        public LabelInput(GeckoBundle obj) {
             super(obj);
         }
 
         @Override
         public View getView(Context context) throws UnsupportedOperationException {
             // not really an input, but a way to add labels and such to the dialog
             TextView view = new TextView(context);
             view.setText(Html.fromHtml(mLabel));
             mView = view;
             return mView;
         }
     }
 
-    public PromptInput(JSONObject obj) {
-        mLabel = obj.optString("label");
-        mType = obj.optString("type");
-        String id = obj.optString("id");
+    public PromptInput(GeckoBundle obj) {
+        mLabel = obj.getString("label");
+        mType = obj.getString("type");
+        String id = obj.getString("id");
         mId = TextUtils.isEmpty(id) ? mType : id;
-        mValue = obj.optString("value");
-        mMaxValue = obj.optString("max");
-        mMinValue = obj.optString("min");
+        mValue = obj.getString("value");
+        mMaxValue = obj.getString("max");
+        mMinValue = obj.getString("min");
     }
 
-    public static PromptInput getInput(JSONObject obj) {
-        String type = obj.optString("type");
+    public static PromptInput getInput(GeckoBundle obj) {
+        String type = obj.getString("type");
         switch (type) {
             case EditInput.INPUT_TYPE:
                 return new EditInput(obj);
             case NumberInput.INPUT_TYPE:
                 return new NumberInput(obj);
             case PasswordInput.INPUT_TYPE:
                 return new PasswordInput(obj);
             case CheckboxInput.INPUT_TYPE:
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
@@ -1,20 +1,17 @@
 package org.mozilla.gecko.prompts;
 
-import org.json.JSONException;
 import org.mozilla.gecko.IntentHelper;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.ThumbnailHelper;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ResourceDrawableUtils;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
-import org.json.JSONArray;
-import org.json.JSONObject;
-
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
 
 import java.util.List;
 import java.util.ArrayList;
 
 // This class should die and be replaced with normal menu items
@@ -27,42 +24,42 @@ public class PromptListItem {
     public final int id;
     public final boolean showAsActions;
     public final boolean isParent;
 
     public Intent mIntent;
     public boolean mSelected;
     public Drawable mIcon;
 
-    PromptListItem(JSONObject aObject) {
+    PromptListItem(GeckoBundle aObject) {
         Context context = GeckoAppShell.getContext();
-        label = aObject.isNull("label") ? "" : aObject.optString("label");
-        isGroup = aObject.optBoolean("isGroup");
-        inGroup = aObject.optBoolean("inGroup");
-        disabled = aObject.optBoolean("disabled");
-        id = aObject.optInt("id");
-        mSelected = aObject.optBoolean("selected");
+        label = aObject.getString("label", "");
+        isGroup = aObject.getBoolean("isGroup");
+        inGroup = aObject.getBoolean("inGroup");
+        disabled = aObject.getBoolean("disabled");
+        id = aObject.getInt("id");
+        mSelected = aObject.getBoolean("selected");
 
-        JSONObject obj = aObject.optJSONObject("showAsActions");
+        GeckoBundle obj = aObject.getBundle("showAsActions");
         if (obj != null) {
             showAsActions = true;
-            String uri = obj.isNull("uri") ? "" : obj.optString("uri");
-            String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE :
-                                               obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
+            String uri = obj.getString("uri", "");
+            String type = obj.getString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
 
             mIntent = IntentHelper.getShareIntent(context, uri, type, "");
             isParent = true;
         } else {
             mIntent = null;
             showAsActions = false;
-            // Support both "isParent" (backwards compat for older consumers), and "menu" for the new Tabbed prompt ui.
-            isParent = aObject.optBoolean("isParent") || aObject.optBoolean("menu");
+            // Support both "isParent" (backwards compat for older consumers), and "menu"
+            // for the new Tabbed prompt ui.
+            isParent = aObject.getBoolean("isParent") || aObject.getBoolean("menu");
         }
 
-        final String iconStr = aObject.optString("icon");
+        final String iconStr = aObject.getString("icon");
         if (iconStr != null) {
             final ResourceDrawableUtils.BitmapLoader loader = new ResourceDrawableUtils.BitmapLoader() {
                     @Override
                     public void onBitmapFound(Drawable d) {
                         mIcon = d;
                     }
                 };
 
@@ -104,25 +101,23 @@ public class PromptListItem {
         isGroup = false;
         inGroup = false;
         isParent = false;
         disabled = false;
         id = 0;
         showAsActions = false;
     }
 
-    static PromptListItem[] getArray(JSONArray items) {
+    static PromptListItem[] getArray(GeckoBundle[] items) {
         if (items == null) {
             return new PromptListItem[0];
         }
 
-        int length = items.length();
+        int length = items.length;
         List<PromptListItem> list = new ArrayList<>(length);
         for (int i = 0; i < length; i++) {
-            try {
-                PromptListItem item = new PromptListItem(items.getJSONObject(i));
-                list.add(item);
-            } catch (JSONException ex) { }
+            PromptListItem item = new PromptListItem(items[i]);
+            list.add(item);
         }
 
         return list.toArray(new PromptListItem[length]);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
@@ -1,72 +1,50 @@
 /* -*- 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.prompts;
 
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.util.Log;
 
-public class PromptService implements GeckoEventListener {
+public class PromptService implements BundleEventListener {
     private static final String LOGTAG = "GeckoPromptService";
 
     private final Context mContext;
 
     public PromptService(Context context) {
-        GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
+        GeckoApp.getEventDispatcher().registerUiThreadListener(this,
             "Prompt:Show",
             "Prompt:ShowTop");
         mContext = context;
     }
 
     public void destroy() {
-        GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
+        GeckoApp.getEventDispatcher().unregisterUiThreadListener(this,
             "Prompt:Show",
             "Prompt:ShowTop");
     }
 
-    public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList,
-                     final int aChoiceMode, final Prompt.PromptCallback callback) {
-        // The dialog must be created on the UI thread.
-        ThreadUtils.postToUiThread(new Runnable() {
+    // BundleEventListener implementation
+    @Override
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        Prompt p;
+        p = new Prompt(mContext, new Prompt.PromptCallback() {
             @Override
-            public void run() {
-                Prompt p;
-                p = new Prompt(mContext, callback);
-                p.show(aTitle, aText, aMenuList, aChoiceMode);
+            public void onPromptFinished(String jsonResult) {
+                callback.sendSuccess(jsonResult);
             }
         });
-    }
-
-    // GeckoEventListener implementation
-    @Override
-    public void handleMessage(String event, final JSONObject message) {
-        // The dialog must be created on the UI thread.
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                Prompt p;
-                p = new Prompt(mContext, new Prompt.PromptCallback() {
-                    @Override
-                    public void onPromptFinished(String jsonResult) {
-                        try {
-                            EventDispatcher.sendResponse(message, new JSONObject(jsonResult));
-                        } catch (JSONException ex) {
-                            Log.i(LOGTAG, "Error building json response", ex);
-                        }
-                    }
-                });
-                p.show(message);
-            }
-        });
+        p.show(message);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.prompts;
 
 import java.util.LinkedHashMap;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.AdapterView;
 import android.widget.ListView;
@@ -28,29 +29,25 @@ public class TabInput extends PromptInpu
     public static final String LOGTAG = "GeckoTabInput";
 
     /* Keeping the order of this in sync with the JSON is important. */
     final private LinkedHashMap<String, PromptListItem[]> mTabs;
 
     private TabHost mHost;
     private int mPosition;
 
-    public TabInput(JSONObject obj) {
+    public TabInput(GeckoBundle obj) {
         super(obj);
         mTabs = new LinkedHashMap<String, PromptListItem[]>();
-        try {
-            JSONArray tabs = obj.getJSONArray("items");
-            for (int i = 0; i < tabs.length(); i++) {
-                JSONObject tab = tabs.getJSONObject(i);
-                String title = tab.getString("label");
-                JSONArray items = tab.getJSONArray("items");
-                mTabs.put(title, PromptListItem.getArray(items));
-            }
-        } catch (JSONException ex) {
-            Log.e(LOGTAG, "Exception", ex);
+        GeckoBundle[] tabs = obj.getBundleArray("items");
+        for (int i = 0; i < (tabs != null ? tabs.length : 0); i++) {
+            GeckoBundle tab = tabs[i];
+            String title = tab.getString("label");
+            GeckoBundle[] items = tab.getBundleArray("items");
+            mTabs.put(title, PromptListItem.getArray(items));
         }
     }
 
     @Override
     public View getView(final Context context) throws UnsupportedOperationException {
         final LayoutInflater inflater = LayoutInflater.from(context);
         mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null);
         mHost.setup();
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
@@ -10,16 +10,17 @@ import android.text.Html;
 import android.text.Spanned;
 import android.util.Log;
 import android.widget.Button;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.prompts.PromptInput;
+import org.mozilla.gecko.util.GeckoBundle;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.view.View;
@@ -104,17 +105,18 @@ public class DefaultDoorHanger extends D
         if (inputs != null) {
             mInputs = new ArrayList<PromptInput>();
 
             final ViewGroup group = (ViewGroup) findViewById(R.id.doorhanger_inputs);
             group.setVisibility(VISIBLE);
 
             for (int i = 0; i < inputs.length(); i++) {
                 try {
-                    PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
+                    PromptInput input = PromptInput.getInput(
+                            GeckoBundle.fromJSONObject(inputs.getJSONObject(i)));
                     mInputs.add(input);
 
                     final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium);
                     View v = input.getView(getContext());
                     styleInput(input, v);
                     v.setPadding(0, 0, 0, padding);
                     group.addView(v);
                 } catch (JSONException ex) { }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoBundle.java
@@ -3,20 +3,25 @@
  * 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.util;
 
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
 
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
 import android.support.v4.util.SimpleArrayMap;
 
 import java.lang.reflect.Array;
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.Set;
 
 /**
  * A lighter-weight version of Bundle that adds support for type coercion (e.g.
  * int to double) in order to better cooperate with JS objects.
  */
 @RobocopTarget
 public final class GeckoBundle {
@@ -613,9 +618,73 @@ public final class GeckoBundle {
     /**
      * Returns number of mappings in this GeckoBundle.
      *
      * @return Number of mappings.
      */
     public int size() {
         return mMap.size();
     }
+
+    private static Object fromJSONValue(Object value) throws JSONException {
+        if (value instanceof JSONObject || value == JSONObject.NULL) {
+            return fromJSONObject((JSONObject) value);
+        }
+        if (value instanceof JSONArray) {
+            final JSONArray array = (JSONArray) value;
+            final int len = array.length();
+            if (len == 0) {
+                return EMPTY_BOOLEAN_ARRAY;
+            }
+            Object out = null;
+            for (int i = 0; i < len; i++) {
+                final Object element = fromJSONValue(array.opt(0));
+                if (element == null) {
+                    continue;
+                }
+                if (out == null) {
+                    Class<?> type = element.getClass();
+                    if (type == Boolean.class) {
+                        type = boolean.class;
+                    } else if (type == Integer.class) {
+                        type = int.class;
+                    } else if (type == Double.class) {
+                        type = double.class;
+                    }
+                    out = Array.newInstance(type, len);
+                }
+                Array.set(out, i, element);
+            }
+            if (out == null) {
+                // Treat all-null arrays as String arrays.
+                return new String[len];
+            }
+            return out;
+        }
+        if (value instanceof Boolean) {
+            return value;
+        }
+        if (value instanceof Byte || value instanceof Short || value instanceof Integer) {
+            return ((Number) value).intValue();
+        }
+        if (value instanceof Float || value instanceof Double || value instanceof Long) {
+            return ((Number) value).doubleValue();
+        }
+        return value != null ? value.toString() : null;
+    }
+
+    public static GeckoBundle fromJSONObject(final JSONObject obj) throws JSONException {
+        if (obj == null || obj == JSONObject.NULL) {
+            return null;
+        }
+
+        final String[] keys = new String[obj.length()];
+        final Object[] values = new String[obj.length()];
+
+        final Iterator<String> iter = obj.keys();
+        for (int i = 0; iter.hasNext(); i++) {
+            final String key = iter.next();
+            keys[i] = key;
+            values[i] = fromJSONValue(obj.opt(key));
+        }
+        return new GeckoBundle(keys, values);
+    }
 }
--- a/mobile/android/modules/Prompt.jsm
+++ b/mobile/android/modules/Prompt.jsm
@@ -3,17 +3,16 @@
  * 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"
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
-Components.utils.import("resource://gre/modules/Messaging.jsm");
 
 this.EXPORTED_SYMBOLS = ["Prompt"];
 
 function log(msg) {
   Services.console.logStringMessage(msg);
 }
 
 function Prompt(aOptions) {
@@ -171,17 +170,18 @@ Prompt.prototype = {
 
   show: function(callback) {
     this.callback = callback;
     log("Sending message");
     this._innerShow();
   },
 
   _innerShow: function() {
-    Messaging.sendRequestForResult(this.msg).then((data) => {
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    window.WindowEventDispatcher.sendRequestForResult(this.msg).then((data) => {
       if (this.callback)
         this.callback(data);
     });
   },
 
   _setListItems: function(aItems) {
     let hasSelected = false;
     this.msg.listitems = [];