Merge fxteam to m-c
authorWes Kocher <wkocher@mozilla.com>
Tue, 15 Oct 2013 20:26:55 -0700
changeset 165605 9f63bbc005279333095be0000696dcf12629b16d
parent 165596 1990042c0ae6cb896c03d637edb2159bb1c7c90f (current diff)
parent 165604 1960e499d49ff65ed613b0ed1dbc5f95c98b5502 (diff)
child 165630 e3c8bbbfec049f67fc3e8611d96f106247a8d2f8
child 165648 85d564125d719798589f5609582c6721df1b95fd
child 165695 9988599193391944b60d3d2a600b2e3fc71d1b0f
child 171412 78e1a20c4c78c7f32c5f09b12cd4478743bef1ff
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone27.0a1
first release with
nightly linux32
9f63bbc00527 / 27.0a1 / 20131016030202 / files
nightly linux64
9f63bbc00527 / 27.0a1 / 20131016030202 / files
nightly mac
9f63bbc00527 / 27.0a1 / 20131016030202 / files
nightly win32
9f63bbc00527 / 27.0a1 / 20131016030202 / files
nightly win64
9f63bbc00527 / 27.0a1 / 20131016030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fxteam to m-c
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -570,17 +570,17 @@
 .variable-or-property > tooltip > label {
   margin: 0 2px 0 2px;
 }
 
 .variable-or-property[non-enumerable] > tooltip > label[value=enumerable],
 .variable-or-property[non-configurable] > tooltip > label[value=configurable],
 .variable-or-property[non-writable] > tooltip > label[value=writable],
 .variable-or-property[non-extensible] > tooltip > label[value=extensible] {
-  color: #800;
+  color: #f44;
   text-decoration: line-through;
 }
 
 .variable-or-property[safe-getter] > tooltip > label[value=WebIDL] {
   -moz-padding-start: 4px;
   -moz-border-start: 1px dotted #000;
   color: #080;
 }
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -127,16 +127,17 @@ FENNEC_JAVA_FILES = \
   OrderedBroadcastHelper.java \
   PageActionLayout.java \
   PrefsHelper.java \
   PrivateDataPreference.java \
   PrivateTab.java \
   prompts/Prompt.java \
   prompts/PromptInput.java \
   prompts/PromptService.java \
+  prompts/IconGridInput.java \
   Restarter.java \
   sqlite/ByteBufferInputStream.java \
   sqlite/MatrixBlobCursor.java \
   sqlite/SQLiteBridge.java \
   sqlite/SQLiteBridgeException.java \
   ReaderModeUtils.java \
   RemoteTabs.java \
   RobocopAPI.java \
@@ -442,16 +443,18 @@ RES_LAYOUT = \
   res/layout/home_history_list.xml \
   res/layout/home_most_recent_page.xml \
   res/layout/home_pager.xml \
   res/layout/home_reading_list_page.xml \
   res/layout/home_search_item_row.xml \
   res/layout/home_banner.xml \
   res/layout/home_suggestion_prompt.xml \
   res/layout/home_top_sites_page.xml \
+  res/layout/icon_grid.xml \
+  res/layout/icon_grid_item.xml \
   res/layout/web_app.xml \
   res/layout/launch_app_list.xml \
   res/layout/launch_app_listitem.xml \
   res/layout/menu_action_bar.xml \
   res/layout/menu_item_action_view.xml \
   res/layout/menu_popup.xml \
   res/layout/notification_icon_text.xml \
   res/layout/notification_progress.xml \
@@ -608,16 +611,18 @@ RES_DRAWABLE_MDPI = \
   res/drawable-mdpi/alert_camera.png \
   res/drawable-mdpi/alert_mic.png \
   res/drawable-mdpi/alert_mic_camera.png \
   res/drawable-mdpi/arrow_popup_bg.9.png \
   res/drawable-mdpi/autocomplete_list_bg.9.png \
   res/drawable-mdpi/bookmark_folder_closed.png \
   res/drawable-mdpi/bookmark_folder_opened.png \
   res/drawable-mdpi/desktop_notification.png \
+  res/drawable-mdpi/grid_icon_bg_activated.9.png \
+  res/drawable-mdpi/grid_icon_bg_focused.9.png \
   res/drawable-mdpi/home_tab_menu_strip.9.png \
   res/drawable-mdpi/ic_menu_addons_filler.png \
   res/drawable-mdpi/ic_menu_bookmark_add.png \
   res/drawable-mdpi/ic_menu_bookmark_remove.png \
   res/drawable-mdpi/ic_menu_character_encoding.png \
   res/drawable-mdpi/close.png \
   res/drawable-mdpi/ic_menu_forward.png \
   res/drawable-mdpi/ic_menu_guest.png \
@@ -713,16 +718,18 @@ RES_DRAWABLE_LDPI = \
 
 RES_DRAWABLE_HDPI = \
   $(SYNC_RES_DRAWABLE_HDPI) \
   res/drawable-hdpi/blank.png \
   res/drawable-hdpi/favicon.png \
   res/drawable-hdpi/folder.png \
   res/drawable-hdpi/home_bg.png \
   res/drawable-hdpi/home_star.png \
+  res/drawable-hdpi/grid_icon_bg_activated.9.png \
+  res/drawable-hdpi/grid_icon_bg_focused.9.png \
   res/drawable-hdpi/abouthome_thumbnail.png \
   res/drawable-hdpi/alert_addon.png \
   res/drawable-hdpi/alert_app.png \
   res/drawable-hdpi/alert_download.png \
   res/drawable-hdpi/bookmark_folder_closed.png \
   res/drawable-hdpi/bookmark_folder_opened.png \
   res/drawable-hdpi/alert_camera.png \
   res/drawable-hdpi/alert_mic.png \
@@ -825,16 +832,18 @@ RES_DRAWABLE_XHDPI = \
   res/drawable-xhdpi/alert_download.png \
   res/drawable-xhdpi/bookmark_folder_closed.png \
   res/drawable-xhdpi/bookmark_folder_opened.png \
   res/drawable-xhdpi/alert_camera.png \
   res/drawable-xhdpi/alert_mic.png \
   res/drawable-xhdpi/alert_mic_camera.png \
   res/drawable-xhdpi/arrow_popup_bg.9.png \
   res/drawable-xhdpi/home_tab_menu_strip.9.png \
+  res/drawable-xhdpi/grid_icon_bg_activated.9.png \
+  res/drawable-xhdpi/grid_icon_bg_focused.9.png \
   res/drawable-xhdpi/ic_menu_addons_filler.png \
   res/drawable-xhdpi/ic_menu_bookmark_add.png \
   res/drawable-xhdpi/ic_menu_bookmark_remove.png \
   res/drawable-xhdpi/close.png \
   res/drawable-xhdpi/ic_menu_character_encoding.png \
   res/drawable-xhdpi/ic_menu_forward.png \
   res/drawable-xhdpi/ic_menu_guest.png \
   res/drawable-xhdpi/ic_menu_new_private_tab.png \
@@ -1074,16 +1083,17 @@ endif
 RES_DRAWABLE += \
   $(SYNC_RES_DRAWABLE)                                \
   res/drawable/action_bar_button.xml                  \
   res/drawable/action_bar_button_inverse.xml          \
   res/drawable/top_sites_thumbnail_bg.xml             \
   res/drawable/url_bar_bg.xml                         \
   res/drawable/url_bar_entry.xml                      \
   res/drawable/url_bar_nav_button.xml                 \
+  res/drawable/icon_grid_item_bg.xml                  \
   res/drawable/url_bar_right_edge.xml                 \
   res/drawable/bookmark_folder.xml                    \
   res/drawable/divider_horizontal.xml                 \
   res/drawable/divider_vertical.xml                   \
   res/drawable/favicon_bg.xml                         \
   res/drawable/handle_end_level.xml                   \
   res/drawable/handle_start_level.xml                 \
   res/drawable/home_history_tabs_indicator.xml        \
--- a/mobile/android/base/gfx/LayerView.java
+++ b/mobile/android/base/gfx/LayerView.java
@@ -16,22 +16,24 @@ import org.mozilla.gecko.mozglue.Generat
 import org.mozilla.gecko.util.EventDispatcher;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.PixelFormat;
+import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.os.Build;
 import android.os.Handler;
 import android.util.AttributeSet;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.TextureView;
 import android.view.View;
 import android.view.ViewGroup;
@@ -120,16 +122,28 @@ public class LayerView extends FrameLayo
         mInputConnectionHandler = null;
 
         setFocusable(true);
         setFocusableInTouchMode(true);
 
         GeckoAccessibility.setDelegate(this);
     }
 
+    private Point getEventRadius(MotionEvent event) {
+        if (Build.VERSION.SDK_INT >= 9) {
+            return new Point((int)event.getToolMajor()/2,
+                             (int)event.getToolMinor()/2);
+        }
+
+        float size = event.getSize();
+        DisplayMetrics displaymetrics = getContext().getResources().getDisplayMetrics();
+        size = size * Math.min(displaymetrics.heightPixels, displaymetrics.widthPixels);
+        return new Point((int)size, (int)size);
+    }
+
     public void geckoConnected() {
         // See if we want to force 16-bit colour before doing anything
         PrefsHelper.getPref("gfx.android.rgb16.force", new PrefsHelper.PrefHandlerBase() {
             @Override public void prefValue(String pref, boolean force16bit) {
                 if (force16bit) {
                     GeckoAppShell.setScreenDepthOverride(16);
                 }
             }
@@ -152,18 +166,20 @@ public class LayerView extends FrameLayo
 
                 int action = event.getActionMasked();
                 PointF point = new PointF(event.getX(), event.getY());
                 if (action == MotionEvent.ACTION_DOWN) {
                     mInitialTouchPoint = point;
                 }
 
                 if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
+                    Point p = getEventRadius(event);
+
                     if (PointUtils.subtract(point, mInitialTouchPoint).length() <
-                        PanZoomController.PAN_THRESHOLD) {
+                        Math.max(PanZoomController.CLICK_THRESHOLD, Math.min(Math.min(p.x, p.y), PanZoomController.PAN_THRESHOLD))) {
                         // Don't send the touchmove event if if the users finger hasn't moved far.
                         // Necessary for Google Maps to work correctly. See bug 771099.
                         return true;
                     } else {
                         mInitialTouchPoint = null;
                     }
                 }
 
--- a/mobile/android/base/gfx/PanZoomController.java
+++ b/mobile/android/base/gfx/PanZoomController.java
@@ -13,16 +13,19 @@ import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 
 public interface PanZoomController {
     // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
     // between the touch-down and touch-up of a click). In units of density-independent pixels.
     public static final float PAN_THRESHOLD = 1/16f * GeckoAppShell.getDpi();
 
+    // Threshold for sending touch move events to content
+    public static final float CLICK_THRESHOLD = 1/50f * GeckoAppShell.getDpi();
+
     static class Factory {
         static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
             return new JavaPanZoomController(target, view, dispatcher);
         }
     }
 
     public void destroy();
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/IconGridInput.java
@@ -0,0 +1,172 @@
+/* -*- 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.JSONArray;
+import org.json.JSONObject;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.LayoutInflater;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.GridView;
+import android.widget.TextView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class IconGridInput extends PromptInput implements OnItemClickListener {
+    public static final String INPUT_TYPE = "icongrid";
+    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 = -1;           // Current selection
+    private JSONArray mArray;
+
+    public IconGridInput(JSONObject obj) {
+        super(obj);
+        mArray = obj.optJSONArray("items");
+    }
+
+    @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);
+        }
+
+        if (mIconSize < 0) {
+            mIconSize = GeckoAppShell.getPreferredIconSize();
+        }
+
+        if (mMaxColumns < 0) {
+            mMaxColumns = context.getResources().getInteger(R.integer.max_icon_grid_columns);
+        }
+
+        // TODO: Dynamically handle size changes
+        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));
+            items.add(item);
+            if (item.selected) {
+                mSelected = i;
+                view.setSelection(i);
+            }
+        }
+
+        view.setNumColumns(Math.min(items.size(), maxColumns));
+        view.setOnItemClickListener(this);
+
+        mAdapter = new IconGridAdapter(context, -1, items);
+        view.setAdapter(mAdapter);
+        mView = view;
+        return mView;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        mSelected = position;
+    }
+
+    @Override
+    public String getValue() {
+        return Integer.toString(mSelected);
+    }
+
+    @Override
+    public boolean getScrollable() {
+        return true;
+    }
+
+    private class IconGridAdapter extends ArrayAdapter<IconGridItem> {
+        public IconGridAdapter(Context context, int resource, List<IconGridItem> items) {
+            super(context, resource, items);
+        }
+
+        @Override
+        public View getView(int position, View convert, ViewGroup parent) {
+            final Context context = parent.getContext();
+            if (convert == null) {
+                convert = LayoutInflater.from(context).inflate(R.layout.icon_grid_item, parent, false);
+            }
+            bindView(convert, context, position);
+            return convert;
+        }
+
+        private void bindView(View v, Context c, int position) {
+            final IconGridItem item = getItem(position);
+            final TextView text1 = (TextView) v.findViewById(android.R.id.text1);
+            text1.setText(item.label);
+
+            final TextView text2 = (TextView) v.findViewById(android.R.id.text2);
+            if (TextUtils.isEmpty(item.description)) {
+                text2.setVisibility(View.GONE);
+            } else {
+                text2.setVisibility(View.VISIBLE);
+                text2.setText(item.description);
+            }
+
+            final ImageView icon = (ImageView) v.findViewById(R.id.icon);
+            icon.setImageDrawable(item.icon);
+            ViewGroup.LayoutParams lp = icon.getLayoutParams();
+            lp.width = lp.height = mIconSize;
+        }
+    }
+ 
+    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");
+
+            BitmapUtils.getDrawable(context, iconUrl, new BitmapUtils.BitmapLoader() {
+                public void onBitmapFound(Drawable d) {
+                    icon = d;
+                    if (mAdapter != null) {
+                        mAdapter.notifyDataSetChanged();
+                    }
+                }
+            });
+        }
+    }
+}
--- a/mobile/android/base/prompts/Prompt.java
+++ b/mobile/android/base/prompts/Prompt.java
@@ -30,16 +30,17 @@ import android.text.InputType;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.CheckBox;
 import android.widget.CheckedTextView;
 import android.widget.DatePicker;
 import android.widget.EditText;
 import android.widget.LinearLayout;
@@ -111,99 +112,40 @@ public class Prompt implements OnClickLi
         view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
         return view;
     }
 
     public void show(JSONObject message) {
         processMessage(message);
     }
 
-    public void show(String aTitle, String aText, PromptListItem[] aMenuList, boolean aMultipleSelection) {
+    public void show(String title, String text, PromptListItem[] listItems, boolean multipleSelection) {
         ThreadUtils.assertOnUiThread();
 
         GeckoAppShell.getLayerView().abortPanning();
 
         AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
-        if (!TextUtils.isEmpty(aTitle)) {
-            builder.setTitle(aTitle);
+        if (!TextUtils.isEmpty(title)) {
+            builder.setTitle(title);
         }
 
-        if (!TextUtils.isEmpty(aText)) {
-            builder.setMessage(aText);
+        if (!TextUtils.isEmpty(text)) {
+            builder.setMessage(text);
         }
 
-        int length = mInputs == null ? 0 : mInputs.length;
-        if (aMenuList != null && aMenuList.length > 0) {
-            int resourceId = android.R.layout.simple_list_item_1;
-            if (mSelected != null && mSelected.length > 0) {
-                if (aMultipleSelection) {
-                    resourceId = R.layout.select_dialog_multichoice;
-                } else {
-                    resourceId = R.layout.select_dialog_singlechoice;
-                }
-            }
-            PromptListAdapter adapter = new PromptListAdapter(mContext, resourceId, aMenuList);
-            if (mSelected != null && mSelected.length > 0) {
-                if (aMultipleSelection) {
-                    adapter.listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
-                    adapter.listView.setOnItemClickListener(this);
-                    builder.setInverseBackgroundForced(true);
-                    adapter.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
-                    adapter.listView.setAdapter(adapter);
-                    builder.setView(adapter.listView);
-                } else {
-                    int selectedIndex = -1;
-                    for (int i = 0; i < mSelected.length; i++) {
-                        if (mSelected[i]) {
-                            selectedIndex = i;
-                            break;
-                        }
-                    }
-                    mSelected = null;
-                    builder.setSingleChoiceItems(adapter, selectedIndex, this);
-                }
-            } else {
-                builder.setAdapter(adapter, this);
-                mSelected = null;
-            }
-        } else if (length == 1) {
-            try {
-                ScrollView view = new ScrollView(mContext);
-                view.addView(mInputs[0].getView(mContext));
-                builder.setView(applyInputStyle(view));
-            } catch(UnsupportedOperationException ex) {
-                // We cannot display these input widgets with this sdk version,
-                // do not display any dialog and finish the prompt now.
-                try {
-                    finishDialog(new JSONObject("{\"button\": -1}"));
-                } catch(JSONException e) { }
-                return;
-            }
-        } else if (length > 1) {
-            try {
-                LinearLayout linearLayout = new LinearLayout(mContext);
-                linearLayout.setOrientation(LinearLayout.VERTICAL);
-                for (int i = 0; i < length; i++) {
-                    View content = mInputs[i].getView(mContext);
-                    linearLayout.addView(content);
-                }
-                ScrollView view = new ScrollView(mContext);
-                view.addView(linearLayout);
-                builder.setView(applyInputStyle(view));
-            } catch(UnsupportedOperationException ex) {
-                // We cannot display these input widgets with this sdk version,
-                // do not display any dialog and finish the prompt now.
-                try {
-                    finishDialog(new JSONObject("{\"button\": -1}"));
-                } catch(JSONException e) { }
-                return;
-            }
+        // Because lists are currently added through the normal Android AlertBuilder interface, they're
+        // incompatible with also adding additional input elements to a dialog.
+        if (listItems != null && listItems.length > 0) {
+            addlistItems(builder, listItems, multipleSelection);
+        } else if (!addInputs(builder)) {
+            // If we failed to add any requested input elements, don't show the dialog
+            return;
         }
 
-        length = mButtons == null ? 0 : mButtons.length;
+        int length = mButtons == null ? 0 : mButtons.length;
         if (length > 0) {
             builder.setPositiveButton(mButtons[0], this);
             if (length > 1) {
                 builder.setNeutralButton(mButtons[1], this);
                 if (length > 2) {
                     builder.setNegativeButton(mButtons[2], this);
                 }
             }
@@ -217,74 +159,252 @@ public class Prompt implements OnClickLi
     public void setButtons(String[] buttons) {
         mButtons = buttons;
     }
 
     public void setInputs(PromptInput[] inputs) {
         mInputs = inputs;
     }
 
+    /* Adds to a result value from the lists that can be shown in dialogs.
+     *  Will set the selected value(s) to the button attribute of the
+     *  object that's passed in. If this is a multi-select dialog, can set
+     *  the button attribute to an array.
+     */
+    private void addListResult(final JSONObject result, int which) {
+        try {
+            if (mSelected != null) {
+                JSONArray selected = new JSONArray();
+                for (int i = 0; i < mSelected.length; i++) {
+                    selected.put(mSelected[i]);
+                }
+                result.put("button", selected);
+            } else {
+                result.put("button", which);
+            }
+        } catch(JSONException ex) { }
+    }
+
+    /* Adds to a result value from the inputs that can be shown in dialogs.
+     * Each input will set its own value in the result.
+     */
+    private void addInputValues(final JSONObject result) {
+        try {
+            if (mInputs != null) {
+                for (int i = 0; i < mInputs.length; i++) {
+                    result.put(mInputs[i].getId(), mInputs[i].getValue());
+                }
+            }
+        } catch(JSONException ex) { }
+    }
+
+    /* Adds the selected button to a result. This should only be called if there
+     * are no lists shown on the dialog, since they also write their results to the button
+     * attribute.
+     */
+    private void addButtonResult(final JSONObject result, int which) {
+        int button = -1;
+        switch(which) {
+            case DialogInterface.BUTTON_POSITIVE : button = 0; break;
+            case DialogInterface.BUTTON_NEUTRAL  : button = 1; break;
+            case DialogInterface.BUTTON_NEGATIVE : button = 2; break;
+        }
+        try {
+            result.put("button", button);
+        } catch(JSONException ex) { }
+    }
+
     @Override
-    public void onClick(DialogInterface aDialog, int aWhich) {
+    public void onClick(DialogInterface dialog, int which) {
         ThreadUtils.assertOnUiThread();
         JSONObject ret = new JSONObject();
         try {
-            int button = -1;
             ListView list = mDialog.getListView();
             if (list != null || mSelected != null) {
-                button = aWhich;
-                if (mSelected != null) {
-                    JSONArray selected = new JSONArray();
-                    for (int i = 0; i < mSelected.length; i++) {
-                        selected.put(mSelected[i]);
-                    }
-                    ret.put("button", selected);
-                } else {
-                    ret.put("button", button);
-                }
+                addListResult(ret, which);
             } else {
-                switch(aWhich) {
-                    case DialogInterface.BUTTON_POSITIVE : button = 0; break;
-                    case DialogInterface.BUTTON_NEUTRAL  : button = 1; break;
-                    case DialogInterface.BUTTON_NEGATIVE : button = 2; break;
-                }
-                ret.put("button", button);
+                addButtonResult(ret, which);
             }
-            if (mInputs != null) {
-                for (int i = 0; i < mInputs.length; i++) {
-                    ret.put(mInputs[i].getId(), mInputs[i].getValue());
-                }
-            }
+            addInputValues(ret);
         } catch(Exception ex) {
             Log.i(LOGTAG, "Error building return: " + ex);
         }
 
-        if (mDialog != null) {
-            mDialog.dismiss();
+        if (dialog != null) {
+            dialog.dismiss();
         }
 
         finishDialog(ret);
     }
 
+    /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists,
+     * or multiple selection lists. If mSelected is set in the prompt before addlistItems is called, the items will be
+     * shown with "checkmarks" on their left side.
+     *
+     * @param builder
+     *        The alert builder currently building this dialog.
+     * @param listItems
+     *        The items to add.
+     * @param multipleSelection
+     *        If true, and mSelected is defined to be a non-zero-length list, the list will show checkmarks on the
+     *        left and allow multiple selection. 
+    */
+    private void addlistItems(AlertDialog.Builder builder, PromptListItem[] listItems, boolean multipleSelection) {
+        if (mSelected != null && mSelected.length > 0) {
+            if (multipleSelection) {
+                addMultiSelectList(builder, listItems);
+            } else {
+                addSingleSelectList(builder, listItems);
+            }
+        } else {
+            addMenuList(builder, listItems);
+        }
+    }
+
+    /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for
+     * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things
+     * to the rows like disabling/indenting them.
+     *
+     * @param builder
+     *        The alert builder currently building this dialog.
+     * @param listItems
+     *        The items to add.
+     */
+    private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
+        PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
+        adapter.listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
+        adapter.listView.setOnItemClickListener(this);
+        builder.setInverseBackgroundForced(true);
+        adapter.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+        adapter.listView.setAdapter(adapter);
+        builder.setView(adapter.listView);
+    }
+
+    /* Shows a single-select list with radio boxes on the side.
+     *
+     * @param builder
+     *        the alert builder currently building this dialog.
+     * @param listItems
+     *        The items to add.
+     */
+    private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
+        PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
+        // For single select, we only maintain a single index of the selected row
+        int selectedIndex = -1;
+        for (int i = 0; i < mSelected.length; i++) {
+            if (mSelected[i]) {
+                selectedIndex = i;
+                break;
+            }
+        }
+        mSelected = null;
+
+        builder.setSingleChoiceItems(adapter, selectedIndex, this);
+    }
+
+    /* Shows a single-select list.
+     *
+     * @param builder
+     *        the alert builder currently building this dialog.
+     * @param listItems
+     *        The items to add.
+     */
+    private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) {
+        PromptListAdapter adapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
+        builder.setAdapter(adapter, this);
+        mSelected = null;
+    }
+
+    /* Add the requested input elements to the dialog.
+     *
+     * @param builder
+     *        the alert builder currently building this dialog.
+     * @return 
+     *         return true if the inputs were added successfully. This may fail
+     *         if the requested input is compatible with this Android verison
+     */
+    private boolean addInputs(AlertDialog.Builder builder) {
+        int length = mInputs == null ? 0 : mInputs.length;
+        if (length == 0) {
+            return true;
+        }
+
+        try {
+            View root = null;
+            boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView
+
+            if (length == 1) {
+                root = mInputs[0].getView(mContext);
+                scrollable |= mInputs[0].getScrollable();
+            } else if (length > 1) {
+                LinearLayout linearLayout = new LinearLayout(mContext);
+                linearLayout.setOrientation(LinearLayout.VERTICAL);
+                for (int i = 0; i < length; i++) {
+                    View content = mInputs[i].getView(mContext);
+                    linearLayout.addView(content);
+                    scrollable |= mInputs[i].getScrollable();
+                }
+                root = linearLayout;
+            }
+
+            if (scrollable) {
+                builder.setView(applyInputStyle(root));
+            } else {
+                ScrollView view = new ScrollView(mContext);
+                view.addView(root);
+                builder.setView(applyInputStyle(view));
+            }
+        } catch(Exception ex) {
+            Log.e(LOGTAG, "Error showing prompt inputs", ex);
+            // We cannot display these input widgets with this sdk version,
+            // do not display any dialog and finish the prompt now.
+            cancelDialog();
+            return false;
+        }
+
+        return true;
+    }
+
+    /* AdapterView.OnItemClickListener
+     * Called when a list item is clicked
+     */
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         ThreadUtils.assertOnUiThread();
         mSelected[position] = !mSelected[position];
     }
 
+    /* @DialogInterface.OnCancelListener
+     * Called when the user hits back to cancel a dialog. The dialog will close itself when this
+     * ends. Setup the correct return values here.
+     *
+     * @param aDialog
+     *          A dialog interface for the dialog that's being closed.
+     */
     @Override
     public void onCancel(DialogInterface aDialog) {
         ThreadUtils.assertOnUiThread();
+        cancelDialog();
+    }
+
+    /* Called in situations where we want to cancel the dialog . This can happen if the user hits back,
+     *  or if the dialog can't be created because of invalid JSON.
+     */
+    private void cancelDialog() {
         JSONObject ret = new JSONObject();
         try {
             ret.put("button", -1);
         } catch(Exception ex) { }
+        addInputValues(ret);
         finishDialog(ret);
     }
 
+    /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
+     * is closing.
+     */
     public void finishDialog(JSONObject aReturn) {
         mInputs = null;
         mButtons = null;
         mDialog = null;
         mSelected = null;
         try {
             aReturn.put("guid", mGuid);
         } catch(JSONException ex) { }
@@ -297,20 +417,22 @@ public class Prompt implements OnClickLi
         GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
 
         if (mCallback != null) {
             mCallback.onPromptFinished(aReturn.toString());
         }
         mGuid = null;
     }
 
+    /* Handles parsing the initial JSON sent to show dialogs
+     */
     private void processMessage(JSONObject geckoObject) {
-        String title = getSafeString(geckoObject, "title");
-        String text = getSafeString(geckoObject, "text");
-        mGuid = getSafeString(geckoObject, "guid");
+        String title = geckoObject.optString("title");
+        String text = geckoObject.optString("text");
+        mGuid = geckoObject.optString("guid");
 
         mButtons = getStringArray(geckoObject, "buttons");
 
         JSONArray inputs = getSafeArray(geckoObject, "inputs");
         mInputs = new PromptInput[inputs.length()];
         for (int i = 0; i < mInputs.length; i++) {
             try {
                 mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
@@ -318,48 +440,24 @@ public class Prompt implements OnClickLi
         }
 
         PromptListItem[] menuitems = getListItemArray(geckoObject, "listitems");
         mSelected = getBooleanArray(geckoObject, "selected");
         boolean multiple = geckoObject.optBoolean("multiple");
         show(title, text, menuitems, multiple);
     }
 
-    private static String getSafeString(JSONObject json, String key) {
-        try {
-            return json.getString(key);
-        } catch (Exception e) {
-            return "";
-        }
-    }
-
     private static JSONArray getSafeArray(JSONObject json, String key) {
         try {
             return json.getJSONArray(key);
         } catch (Exception e) {
             return new JSONArray();
         }
     }
 
-    private static boolean getSafeBool(JSONObject json, String key) {
-        try {
-            return json.getBoolean(key);
-        } catch (Exception e) {
-            return false;
-        }
-    }
-
-    private static int getSafeInt(JSONObject json, String key ) {
-        try {
-            return json.getInt(key);
-        } catch (Exception e) {
-            return 0;
-        }
-    }
-
     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) { }
@@ -401,22 +499,22 @@ public class Prompt implements OnClickLi
         public final boolean disabled;
         public final int id;
         public final boolean isParent;
 
         // This member can't be accessible from JS, see bug 733749.
         public Drawable icon;
 
         PromptListItem(JSONObject aObject) {
-            label = getSafeString(aObject, "label");
-            isGroup = getSafeBool(aObject, "isGroup");
-            inGroup = getSafeBool(aObject, "inGroup");
-            disabled = getSafeBool(aObject, "disabled");
-            id = getSafeInt(aObject, "id");
-            isParent = getSafeBool(aObject, "isParent");
+            label = aObject.optString("label");
+            isGroup = aObject.optBoolean("isGroup");
+            inGroup = aObject.optBoolean("inGroup");
+            disabled = aObject.optBoolean("disabled");
+            id = aObject.optInt("id");
+            isParent = aObject.optBoolean("isParent");
         }
 
         public PromptListItem(String aLabel) {
             label = aLabel;
             isGroup = false;
             inGroup = false;
             disabled = false;
             id = 0;
--- a/mobile/android/base/prompts/PromptInput.java
+++ b/mobile/android/base/prompts/PromptInput.java
@@ -37,18 +37,16 @@ import android.widget.TimePicker;
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.TimeUnit;
 
 public class PromptInput {
-    private final JSONObject mJSONInput;
-
     protected final String mLabel;
     protected final String mType;
     protected final String mId;
     protected final String mValue;
     protected View mView;
     public static final String LOGTAG = "GeckoPromptInput";
 
     public static class EditInput extends PromptInput {
@@ -324,17 +322,16 @@ public class PromptInput {
         }
 
         public String getValue() {
             return "";
         }
     }
 
     public PromptInput(JSONObject obj) {
-        mJSONInput = obj;
         mLabel = obj.optString("label");
         mType = obj.optString("type");
         String id = obj.optString("id");
         mId = TextUtils.isEmpty(id) ? mType : id;
         mValue = obj.optString("value");
     }
 
     public static PromptInput getInput(JSONObject obj) {
@@ -346,16 +343,18 @@ public class PromptInput {
         } else if (PasswordInput.INPUT_TYPE.equals(type)) {
             return new PasswordInput(obj);
         } else if (CheckboxInput.INPUT_TYPE.equals(type)) {
             return new CheckboxInput(obj);
         } else if (MenulistInput.INPUT_TYPE.equals(type)) {
             return new MenulistInput(obj);
         } else if (LabelInput.INPUT_TYPE.equals(type)) {
             return new LabelInput(obj);
+        } else if (IconGridInput.INPUT_TYPE.equals(type)) {
+            return new IconGridInput(obj);
         } else {
             for (String dtType : DateTimeInput.INPUT_TYPES) {
                 if (dtType.equals(type)) {
                     return new DateTimeInput(obj);
                 }
             }
         }
         return null;
@@ -367,9 +366,13 @@ public class PromptInput {
 
     public String getId() {
         return mId;
     }
 
     public String getValue() {
         return "";
     }
+
+    public boolean getScrollable() {
+        return false;
+    }
 }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e591a7b009ad931342bf25593aa270489ce00436
GIT binary patch
literal 326
zc%17D@N?(olHy`uVBq!ia0vp^;vmey1|%P7U0DF6Sc;uILpXq-h9ji|$mcBZh%9Dc
z;O+!rM)Q-W*8&A!c)B=-cyw-^YRK2DAkt><`#kQzRpuEtPA+6$alJ5qSGBCI1G~8}
z|ASYoRgKqKH8K@dx#|xw@Y}^T%x~`y(AaqE|4F5k`^{fB#5S_6XfSHn%*1VQHuvKe
zX0|x~+mbIDqCL{&dL8VyALUlq&g6b@#n-v-o+R`=(zubcphE4lnoz{*jSoXpDp(7G
zWbd!8U}<>Qw^3-q9l?%ePd+4cziU(vF8Xo)zmV`eAJ2!kT(t9)ck&1=65a9Q%=Ebe
z^TmG1oEDL^liaaNAz9_a3hha?3u{|=-HtNK<lAKNiVAKz^Lp`w`r|tzTq>BA^Q^xs
TcBLK$`kBGg)z4*}Q$iB}9#MTD
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e591a7b009ad931342bf25593aa270489ce00436
GIT binary patch
literal 326
zc%17D@N?(olHy`uVBq!ia0vp^;vmey1|%P7U0DF6Sc;uILpXq-h9ji|$mcBZh%9Dc
z;O+!rM)Q-W*8&A!c)B=-cyw-^YRK2DAkt><`#kQzRpuEtPA+6$alJ5qSGBCI1G~8}
z|ASYoRgKqKH8K@dx#|xw@Y}^T%x~`y(AaqE|4F5k`^{fB#5S_6XfSHn%*1VQHuvKe
zX0|x~+mbIDqCL{&dL8VyALUlq&g6b@#n-v-o+R`=(zubcphE4lnoz{*jSoXpDp(7G
zWbd!8U}<>Qw^3-q9l?%ePd+4cziU(vF8Xo)zmV`eAJ2!kT(t9)ck&1=65a9Q%=Ebe
z^TmG1oEDL^liaaNAz9_a3hha?3u{|=-HtNK<lAKNiVAKz^Lp`w`r|tzTq>BA^Q^xs
TcBLK$`kBGg)z4*}Q$iB}9#MTD
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ea27290d76dd858d01935506622f40dcb2c64e56
GIT binary patch
literal 263
zc%17D@N?(olHy`uVBq!ia0vp^;vmey1|%P7U0DF6Sc;uILpXq-h9ji|$mcBZh%9Dc
z;O+!rM)Q-W*8&Asdb&7<cyzv<a*_9t0guc14&7g;*;2d9SlSXq+7epIxOX|$Ss(u>
zyim)kBl@a5hu<l8rTg8fFL@>vbae;oe=Dx{T_$(Ca38}9#yK)QED1AqJ&S8i|4^Db
znV~@2-lAhs9dpF5zC|xgyH;kOGflES*MDic>(!rmT<z_yFLxhTYThDl<Fl|pbWw{;
zhpU5)*U`Hn&ku7waF2fx_Q(5_$Dud5eKytZvpgSh@=w>j^!WN~1`zOc^>bP0l+XkK
D9ZzIW
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7dfea4c00546a18eeb6eeccd12d751247caa65c5
GIT binary patch
literal 284
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
zaCd?*qxs3xYk`9MJY5_^EPA(2J}cN{Akezs;NI6Kt=zT~^j;j`+Q4;8YYUV00`3=$
z+~R&w2R|nmHA%44-}6b^bo0B}>`G}BL&lw7tuzyBCQN%Pb%R;VgJ~AqiiYI5N5db)
zDd^tjEnwRg{PxQQ#@$xk`V*~Bh<6m;kvSl~f%}M@Si;k$Cz_QU9bNbA7)4k+HVYai
zUcSQ=U!HJp)4!XI<qH#ExELn&U1Ydc9#C6Q$=a_zXGj0@m=eFw&F57(lil1}(vlsf
a9O2zlruQsf{=Y5ICk&phelF{r5}E*wSZS{S
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..99b0279469264df100cdea866a3302c376e8c30a
GIT binary patch
literal 242
zc%17D@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
zaCd?*qxs3xYk`7OJY5_^EPD4&I>>uiL4f7?nv#e8)+Y|XJY@RQ)%TrJ-415Eb;+L<
zg?2I&iX7EeJb3rr#w14}UX8Uyo7dWMPLn^Dx}V9|AmoQ2$LlNk=bfH&=o+pxf8Ofd
zc);rG_DNMS-*)eIxOedSi;N>BB?r#)y-|KWpRa~XP~uYdb%D)$E7=bUdOa#Qb34+&
kbH-lQz~9pPE}k|Fdqk9v&oxZF0(2jPr>mdKI;Vst01kFqRR910
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f01a79e3bf44ced72d89e813da41d336d5d0a2ce
GIT binary patch
literal 413
zc%17D@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
zaCd?*qxs3xYZ(|A?LA!_Lp(a4PB+XuWFX=Yue78~+T~Qkf*u}^l6R~{?eA<^PigM@
z&KI+wT7~TxLz2TO-T<+>_CGsRTs?Uuw4QoODp~(Oy7BYh-i`vspxtle#12|tH;8@{
z-zaFn%9M~JY0YYsz?m~i<Ik${6&>l_d--`9B_tSXy%Xm$Z(x3Qz^wDoWBrX@_6?jL
zN|tW$<6G14J?Pzh6Fynbi!+2v6OLFd->Cd@rA^p$$&VdLezC>@(*^x^)+zpN-M6d9
zVn_296^0iteQ(!GMYGK}-_pzYQQFF4>Fh_{Wu4I-@=oSQ)4xBd1~R@MNnPicdG5OJ
z&g;`eDm2~a+%+hWmi$xmO4T*-*{tSWJr&M@yt~qtuk)Jvlc(m<x_74<x2ten4~V@Q
z-6dpFt`fVF`3FC1?OWD6@`)}i=d*pUZ>ug#b`X3h9m^J}uJl3G85q6{p00i_>zopr
E0KAQ$-2eap
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7bea197d0c510cf6cf5cb7f37fdc624e88ff9b7c
GIT binary patch
literal 324
zc%17D@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
zaCd?*qxs3xYk`8#JY5_^JUZV_v*l}Y5OBLM#h<uj>xSc(q-PXx@h2Qrv|wJY@Q`g<
z5pz`Ub_wq70e2Xu%@;U2{lbPb-;IKKx;ri~uC%E;dUsC!KAy$0Ek8?nj~w8-QSofr
z0a=eO=Yrze4J#(P9oW|_61|ySY4+EHL7F|eiSd00#4JMj=g5ARc^_c0c}w5@W6?5A
zbNe;VtQXfktrT^9v9bS073u1Ei|6^QdG~tb(a8q(8@H|Yjw{Pa{r)S+>Yn7<$JP9S
zt95+mFuY{AbR})8@a@-LY;y|xS9M;xmUipv^YibMU09N(u5W#BzuPRprGmk()a<7F
T%n2+&A2WEm`njxgN@xNAyaj?H
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/icon_grid_item_bg.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_focused="true"
+          android:state_pressed="true"
+          android:drawable="@drawable/grid_icon_bg_focused" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/grid_icon_bg_activated" />
+    <item android:drawable="@android:color/transparent" />
+
+</selector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/icon_grid.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<GridView xmlns:android="http://schemas.android.com/apk/res/android"
+          android:layout_height="wrap_content"
+          android:layout_width="match_parent"
+          android:choiceMode="singleChoice"
+          android:padding="@dimen/icongrid_padding"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/icon_grid_item.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  //device/apps/common/res/any/layout/resolve_list_item.xml
+  Copyright 2006, The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:gravity="center"
+              android:orientation="vertical"
+              android:layout_height="wrap_content"
+              android:layout_width="match_parent"
+              android:background="@drawable/icon_grid_item_bg"
+              android:padding="16dp">
+
+    <!-- Extended activity info to distinguish between duplicate activity names -->
+    <TextView android:id="@android:id/text2"
+              android:textAppearance="?android:attr/textAppearance"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:gravity="center"
+              android:minLines="2"
+              android:maxLines="2"
+              android:paddingTop="4dip"
+              android:paddingBottom="4dip" />
+
+    <!-- Activity icon when presenting dialog
+         Size will be filled in by ResolverActivity -->
+    <ImageView android:id="@+id/icon"
+               android:layout_width="0dp"
+               android:layout_height="0dp"
+               android:scaleType="fitCenter" />
+
+    <!-- Activity name -->
+    <TextView android:id="@android:id/text1"
+              android:textAppearance="?android:attr/textAppearanceSmall"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:gravity="center"
+              android:minLines="2"
+              android:maxLines="2"
+              android:paddingTop="4dip"
+              android:paddingBottom="4dip" />
+</LinearLayout>
+
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -91,9 +91,13 @@
          50dp * 2 Views + 30dp padding + 4dp dividers-->
     <dimen name="history_tab_widget_height">134dp</dimen>
 
     <!-- PageActionButtons dimensions -->
     <dimen name="page_action_button_width">32dp</dimen>
 
     <!-- Banner -->
     <dimen name="home_banner_height">72dp</dimen>
+
+    <!-- Icon Grid -->
+    <dimen name="icongrid_columnwidth">128dp</dimen>
+    <dimen name="icongrid_padding">16dp</dimen>
 </resources>
--- a/mobile/android/base/resources/values/integers.xml
+++ b/mobile/android/base/resources/values/integers.xml
@@ -2,10 +2,11 @@
 <!-- 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/. -->
 
 <resources>
 
     <integer name="number_of_top_sites">6</integer>
     <integer name="number_of_top_sites_cols">2</integer>
+    <integer name="max_icon_grid_columns">4</integer>
 
 </resources>
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -15,16 +15,17 @@
 [testFlingCorrectness]
 [testOverscroll]
 [testAxisLocking]
 [testAboutPage]
 [testLinkContextMenu]
 [testMailToContextMenu]
 [testPictureLinkContextMenu]
 [testPasswordProvider]
+[testPromptGridInput]
 # [testPasswordEncrypt] # see bug 824067
 [testFormHistory]
 [testBrowserProvider]
 [testSearchSuggestions]
 [testSharedPreferences]
 # [testThumbnails] # see bug 813107
 [testAddonManager]
 # [testHistory] # see bug 915350
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/roboextender/robocop_prompt_gridinput.html
@@ -0,0 +1,51 @@
+<html>
+  <head>
+    <title>IconGrid test page</title>
+    <meta name="viewport" content="initial-scale=1.0"/>
+    <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+    <script type="application/javascript">
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Prompt.jsm");
+
+function start() {
+  var test = location.hash.substring(1);
+  window[test]();
+}
+
+function test1() {
+  var p = new Prompt({
+    title: "Prompt 1",
+    buttons: [
+      "OK"
+    ],
+  }).addIconGrid({
+    items: [
+      { iconUri: "drawable://alert_app", name: "Icon 1", selected: true },
+      { iconUri: "drawable://alert_download", name: "Icon 2" },
+      { iconUri: "drawable://alert_addon", name: "Icon 3" },
+      { iconUri: "drawable://alert_addon", name: "Icon 4" },
+      { iconUri: "drawable://alert_addon", name: "Icon 5" },
+      { iconUri: "drawable://alert_addon", name: "Icon 6" },
+      { iconUri: "drawable://alert_addon", name: "Icon 7" },
+      { iconUri: "drawable://alert_addon", name: "Icon 8" },
+      { iconUri: "drawable://alert_addon", name: "Icon 9" },
+      { iconUri: "drawable://alert_addon", name: "Icon 10" },
+      { iconUri: "drawable://alert_addon", name: "Icon 11" },
+    ]
+  });
+  p.show(function(data) {
+    sendResult(data.icongrid0 == 10, "Got result " + data.icongrid0);
+  });
+}
+
+function sendResult(pass, message) {
+  setTimeout(function() {
+    alert((pass ? "PASS " : "FAIL ") + message);
+  }, 1000);
+}
+    </script>
+  </head>
+  <body onload="start();">
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testPromptGridInput.java.in
@@ -0,0 +1,61 @@
+#filter substitution
+package @ANDROID_PACKAGE_NAME@.tests;
+
+import @ANDROID_PACKAGE_NAME@.*;
+
+import android.graphics.drawable.Drawable;
+import android.widget.EditText;
+import android.widget.CheckedTextView;
+import android.widget.TextView;
+import android.text.InputType;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.util.Log;
+
+import org.json.JSONObject;
+
+public class testPromptGridInput extends BaseTest {
+    @Override
+    protected int getTestType() {
+        return TEST_MOCHITEST;
+    }
+
+    protected int index = 1;
+    public void testPromptGridInput() {
+        blockForGeckoReady();
+
+        test(1);
+
+        testGridItem("Icon 1");
+        testGridItem("Icon 2");
+        testGridItem("Icon 3");
+        testGridItem("Icon 4");
+        testGridItem("Icon 5");
+        testGridItem("Icon 6");
+        testGridItem("Icon 7");
+        testGridItem("Icon 8");
+        testGridItem("Icon 9");
+        testGridItem("Icon 10");
+        testGridItem("Icon 11");
+
+        mSolo.clickOnText("Icon 11");
+        mSolo.clickOnText("OK");
+
+        mAsserter.ok(waitForText("PASS"), "test passed", "PASS");
+        mActions.sendSpecialKey(Actions.SpecialKey.BACK);
+    }
+
+    public void testGridItem(String title) {
+        // Force the list to scroll if necessary
+        mSolo.waitForText(title, 1, 500, true);
+        mAsserter.ok(waitForText(title), "Found grid item", title);
+    }
+
+    public void test(final int num) {
+        // Load about:blank between each test to ensure we reset state
+        loadUrl("about:blank");
+        mAsserter.ok(waitForText("about:blank"), "Loaded blank page", "page title match");
+
+        loadUrl("chrome://roboextender/content/robocop_prompt_gridinput.html#test" + num);
+    }
+}
--- a/mobile/android/components/PromptService.js
+++ b/mobile/android/components/PromptService.js
@@ -219,31 +219,31 @@ InternalPrompt.prototype = {
   alert: function alert(aTitle, aText) {
     let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ]);
     this.showPrompt(p);
   },
 
   alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, [ PromptUtils.getLocaleString("OK") ], aCheckMsg, aCheckState);
     let data = this.showPrompt(p);
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
   },
 
   confirm: function confirm(aTitle, aText) {
     let p = this._getPrompt(aTitle, aText);
     let data = this.showPrompt(p);
     return (data.button == 0);
   },
 
   confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
     let data = this.showPrompt(p);
     let ok = data.button == 0;
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     return ok;
   },
 
   confirmEx: function confirmEx(aTitle, aText, aButtonFlags, aButton0,
                       aButton1, aButton2, aCheckMsg, aCheckState) {
     let buttons = [];
     let titles = [aButton0, aButton1, aButton2];
@@ -279,31 +279,31 @@ InternalPrompt.prototype = {
       if (bTitle)
         buttons.push(bTitle);
 
       aButtonFlags >>= 8;
     }
 
     let p = this._getPrompt(aTitle, aText, buttons, aCheckMsg, aCheckState);
     let data = this.showPrompt(p);
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     return data.button;
   },
 
   nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
     let p = this._getPrompt(aTitle, aText, null, aCheckMsg, aCheckState);
     p.addTextbox({
       value: aValue.value,
       autofocus: true
     });
     let data = this.showPrompt(p);
 
     let ok = data.button == 0;
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     if (ok)
       aValue.value = data.textbox0;
     return ok;
   },
 
   nsIPrompt_promptPassword: function nsIPrompt_promptPassword(
       aTitle, aText, aPassword, aCheckMsg, aCheckState) {
@@ -311,17 +311,17 @@ InternalPrompt.prototype = {
     p.addPassword({
       value: aPassword.value || "",
       autofocus: true,
       hint: PromptUtils.getLocaleString("password", "passwdmgr")
     });
     let data = this.showPrompt(p);
 
     let ok = data.button == 0;
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     if (ok)
       aPassword.value = data.password0;
     return ok;
   },
 
   nsIPrompt_promptUsernameAndPassword: function nsIPrompt_promptUsernameAndPassword(
       aTitle, aText, aUsername, aPassword, aCheckMsg, aCheckState) {
@@ -332,17 +332,17 @@ InternalPrompt.prototype = {
       hint: PromptUtils.getLocaleString("username", "passwdmgr")
     }).addPassword({
       value: aPassword.value,
       hint: PromptUtils.getLocaleString("password", "passwdmgr")
     });
     let data = this.showPrompt(p);
 
     let ok = data.button == 0;
-    if (aCheckState)
+    if (aCheckState && data.button > -1)
       aCheckState.value = data.checkbox0 == "true";
     if (ok) {
       aUsername.value = data.textbox0;
       aPassword.value = data.password0;
     }
     return ok;
   },
 
--- a/mobile/android/modules/Prompt.jsm
+++ b/mobile/android/modules/Prompt.jsm
@@ -117,16 +117,24 @@ Prompt.prototype = {
   addMenulist: function(aOptions) {
     return this._addInput({
       type: "menulist",
       values: aOptions.values,
       id: aOptions.id
     });
   },
 
+  addIconGrid: function(aOptions) {
+    return this._addInput({
+      type: "icongrid",
+      items: aOptions.items,
+      id: aOptions.id
+    });
+  },
+
   show: function(callback) {
     this.callback = callback;
     log("Sending message");
     Services.obs.addObserver(this, "Prompt:Reply", false);
     this._innerShow();
   },
 
   _innerShow: function() {
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -56,18 +56,18 @@
 // is executed. This ensures a Refresh() is executed before proceeding, if a
 // batch is ongoing.
 // Note that there's no point in using this in node getters, since after a
 // batch the node would be replaced by a new one, so it would hand out outdated
 // information regardless.
 #define END_RESULT_BATCH_AND_REFRESH_CONTENTS() \
   PR_BEGIN_MACRO \
   nsNavHistoryResult* result = GetResult(); \
-  NS_ENSURE_STATE(result); \
-  if (result->mBatchInProgress) { \
+  NS_WARN_IF_FALSE(result, "Working with a non-live-updating Places container"); \
+  if (result && result->mBatchInProgress) { \
     result->EndBatch(); \
   } \
   PR_END_MACRO
 
 // Emulate string comparison (used for sorting) for PRTime and int.
 inline int32_t ComparePRTime(PRTime a, PRTime b)
 {
   if (a < b)