Merge f-t to m-c
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 15 Feb 2014 10:10:31 -0800
changeset 169400 3051177649294795c3791d489c5ac6975c3be7bd
parent 169370 4ebfb26de52c10299784c7254703fe6b4c1e020c (current diff)
parent 169399 9eb23b6b7f4b3d26a9ce6c9aaaa8bd4ff82a1dcd (diff)
child 169401 e8afde67c29031370a84c6ff50fe6746b39cc50d
child 169408 267e352db60c98f2307c7a2e8bb2c95496e5ad6d
child 169415 b80230b78810b299e25bd3cfb30945b3fc1e0a6a
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
milestone30.0a1
Merge f-t to m-c
browser/components/customizableui/content/aboutCustomizing.xhtml
browser/components/customizableui/src/CustomizableUI.jsm
browser/devtools/shared/widgets/Tooltip.js
mobile/android/base/CameraImageResultHandler.java
mobile/android/base/CameraVideoResultHandler.java
mobile/android/base/FilePickerResultHandlerSync.java
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1355,16 +1355,19 @@ pref("geo.wifi.uri", "https://www.google
 
 // Necko IPC security checks only needed for app isolation for cookies/cache/etc:
 // currently irrelevant for desktop e10s
 pref("network.disable.ipc.security", true);
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
+// CustomizableUI state of the browser's user interface
+pref("browser.uiCustomization.state", "");
+
 // The URL where remote content that composes the UI for Firefox Accounts should
 // be fetched. Must use HTTPS.
 pref("identity.fxaccounts.remote.uri", "https://accounts.firefox.com/?service=sync&context=fx_desktop_v1");
 
 // The URL where remote content that forces re-authentication for Firefox Accounts
 // should be fetched.  Must use HTTPS.
 pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v1");
 
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -45,19 +45,16 @@
              oncommand="gFindBar.onFindCommand();"
              observes="isImage"/>
     <command id="cmd_findAgain"
              oncommand="gFindBar.onFindAgainCommand(false);"
              observes="isImage"/>
     <command id="cmd_findPrevious"
              oncommand="gFindBar.onFindAgainCommand(true);"
              observes="isImage"/>
-#ifdef XP_MACOSX
-    <command id="cmd_findSelection" oncommand="gFindBar.onFindSelectionCommand();"/>
-#endif
     <!-- work-around bug 392512 -->
     <command id="Browser:AddBookmarkAs"
              oncommand="PlacesCommandHook.bookmarkCurrentPage(true, PlacesUtils.bookmarksMenuFolderId);"/>
     <!-- The command disabled state must be manually updated through
          PlacesCommandHook.updateBookmarkAllTabsCommand() -->
     <command id="Browser:BookmarkAllTabs"
              oncommand="PlacesCommandHook.bookmarkCurrentPages();"/>
     <command id="Browser:Home"    oncommand="BrowserHome();"/>
@@ -348,19 +345,16 @@
     <key key="&reloadCmd.commandkey;" command="Browser:ReloadSkipCache" modifiers="accel,shift"/>
     <key id="key_viewSource" key="&pageSourceCmd.commandkey;" command="View:PageSource" modifiers="accel"/>
 #ifndef XP_WIN
     <key id="key_viewInfo"   key="&pageInfoCmd.commandkey;"   command="View:PageInfo"   modifiers="accel"/>
 #endif
     <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
     <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
     <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
-#ifdef XP_MACOSX
-    <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
-#endif
     <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
     <key keycode="&findAgainCmd.commandkey2;"  command="cmd_findPrevious" modifiers="shift"/>
 
     <key id="addBookmarkAsKb" key="&bookmarkThisPageCmd.commandkey;" command="Browser:AddBookmarkAs" modifiers="accel"/>
 # Accel+Shift+A-F are reserved on GTK
 #ifndef MOZ_WIDGET_GTK
     <key id="bookmarkAllTabsKb" key="&bookmarkThisPageCmd.commandkey;" oncommand="PlacesCommandHook.bookmarkCurrentPages();" modifiers="accel,shift"/>
     <key id="manBookmarkKb" key="&bookmarksCmd.commandkey;" command="Browser:ShowAllBookmarks" modifiers="accel,shift"/>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4233,16 +4233,17 @@ function onViewToolbarsPopupShowing(aEve
   let toolbarItem = popup.triggerNode;
 
   if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
     toolbarItem = toolbarItem.firstChild;
   } else {
     while (toolbarItem && toolbarItem.parentNode) {
       let parent = toolbarItem.parentNode;
       if ((parent.classList && parent.classList.contains("customization-target")) ||
+          parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
           parent.localName == "toolbarpaletteitem" ||
           parent.localName == "toolbar")
         break;
       toolbarItem = parent;
     }
   }
 
   // Right-clicking on an empty part of the tabstrip will exit
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -264,17 +264,24 @@
                 label="&customizeMenu.moveToPanel.label;"
                 class="customize-context-moveToPanel"/>
       <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
                 accesskey="&customizeMenu.removeFromToolbar.accesskey;"
                 label="&customizeMenu.removeFromToolbar.label;"
                 class="customize-context-removeFromToolbar"/>
       <menuseparator/>
       <menuseparator id="viewToolbarsMenuSeparator"/>
-      <menuitem command="cmd_CustomizeToolbars"
+      <!-- XXXgijs: we're using oncommand handler here to avoid the event being
+                    redirected to the command element, thus preventing
+                    listeners on the menupopup or further up the tree from
+                    seeing the command event pass by. The observes attribute is
+                    here so that the menuitem is still disabled and re-enabled
+                    correctly. -->
+      <menuitem oncommand="BrowserCustomizeToolbar()"
+                observes="cmd_CustomizeToolbars"
                 class="viewCustomizeToolbar"
                 label="&viewCustomizeToolbar.label;"
                 accesskey="&viewCustomizeToolbar.accesskey;"/>
     </menupopup>
 
     <menupopup id="blockedPopupOptions"
                onpopupshowing="gPopupBlockerObserver.fillPopupList(event);"
                onpopuphiding="gPopupBlockerObserver.onPopupHiding(event);">
--- a/browser/base/content/test/general/browser_bug537013.js
+++ b/browser/base/content/test/general/browser_bug537013.js
@@ -6,19 +6,16 @@
 let tabs = [];
 let texts = [
   "This side up.",
   "The world is coming to an end. Please log off.",
   "Klein bottle for sale. Inquire within.",
   "To err is human; to forgive is not company policy."
 ];
 
-let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
-let HasFindClipboard = Clipboard.supportsFindClipboard();
-
 function addTabWithText(aText, aCallback) {
   let newTab = gBrowser.addTab("data:text/html,<h1 id='h1'>" + aText + "</h1>");
   tabs.push(newTab);
   gBrowser.selectedTab = newTab;
 }
 
 function setFindString(aString) {
   gFindBar.open();
@@ -59,19 +56,17 @@ function continueTests1() {
   gFindBar.open();
   is(gFindBar._findField.value, texts[0],
      "Second tab kept old find value for new initialization!");
   setFindString(texts[1]);
 
   // Confirm the first tab is still correct, ensure re-hiding works as expected
   gBrowser.selectedTab = tabs[0];
   ok(!gFindBar.hidden, "First tab shows find bar!");
-  // When the Find Clipboard is supported, this test not relevant.
-  if (!HasFindClipboard)
-    is(gFindBar._findField.value, texts[0], "First tab persists find value!");
+  is(gFindBar._findField.value, texts[0], "First tab persists find value!");
   ok(gFindBar.getElement("highlight").checked,
      "Highlight button state persists!");
 
   // While we're here, let's test bug 253793
   gBrowser.reload();
   gBrowser.addEventListener("DOMContentLoaded", continueTests2, true);
 }
 
@@ -94,27 +89,25 @@ function continueTests2() {
 
   // Now we jump to the second, then first, and then fourth
   gBrowser.selectedTab = tabs[1];
   gBrowser.selectedTab = tabs[0];
   gBrowser.selectedTab = tabs[3];
   ok(gFindBar.hidden, "Fourth tab doesn't show find bar!");
   is(gFindBar, gBrowser.getFindBar(), "Find bar is right one!");
   gFindBar.open();
-  let toTest = HasFindClipboard ? texts[2] : texts[1];
-  is(gFindBar._findField.value, toTest,
+  is(gFindBar._findField.value, texts[1],
      "Fourth tab has second tab's find value!");
 
   newWindow = gBrowser.replaceTabWithWindow(tabs.pop());
   whenDelayedStartupFinished(newWindow, checkNewWindow);
 }
 
 // Test that findbar gets restored when a tab is moved to a new window.
 function checkNewWindow() {
   ok(!newWindow.gFindBar.hidden, "New window shows find bar!");
-  let toTest = HasFindClipboard ? texts[2] : texts[1];
-  is(newWindow.gFindBar._findField.value, toTest,
+  is(newWindow.gFindBar._findField.value, texts[1],
      "New window find bar has correct find value!");
   ok(!newWindow.gFindBar.getElement("find-next").disabled,
      "New window findbar has enabled buttons!");
   newWindow.close();
   finish();
 }
--- a/browser/base/content/test/general/browser_bug567306.js
+++ b/browser/base/content/test/general/browser_bug567306.js
@@ -1,16 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
-const {Ci: interfaces, Cc: classes} = Components;
-
-let Clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
-let HasFindClipboard = Clipboard.supportsFindClipboard();
+let Ci = Components.interfaces;
 
 function test() {
   waitForExplicitFinish();
 
   whenNewWindowLoaded(undefined, function (win) {
     whenDelayedStartupFinished(win, function () {
       let selectedBrowser = win.gBrowser.selectedBrowser;
       selectedBrowser.addEventListener("pageshow", function() {
@@ -35,16 +32,13 @@ function selectText(win) {
   selection.addRange(range);
 }
 
 function onFocus(win) {
   ok(!win.gFindBarInitialized, "find bar is not yet initialized");
   let findBar = win.gFindBar;
   selectText(win.content);
   findBar.onFindCommand();
-  // When the OS supports the Find Clipboard (OSX), the find field value is
-  // persisted across Fx sessions, thus not useful to test.
-  if (!HasFindClipboard)
-    is(findBar._findField.value, "Select Me", "Findbar is initialized with selection");
+  is(findBar._findField.value, "Select Me", "Findbar is initialized with selection");
   findBar.close();
   win.close();
   finish();
 }
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -90,17 +90,17 @@ static RedirEntry kRedirMap[] = {
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
   { "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
   { "app-manager", "chrome://browser/content/devtools/app-manager/index.xul",
     nsIAboutModule::ALLOW_SCRIPT },
-  { "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xhtml",
+  { "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 };
 static const int kRedirTotal = ArrayLength(kRedirMap);
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
deleted file mode 100644
--- a/browser/components/customizableui/content/aboutCustomizing.xhtml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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/. -->
-
-<!DOCTYPE html [
-  <!ENTITY % htmlDTD
-    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-    "DTD/xhtml1-strict.dtd">
-  %htmlDTD;
-  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
-  %brandDTD;
-  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
-  %browserDTD;
-]>
-
-<html xmlns="http://www.w3.org/1999/xhtml"
-      disablefastfind="true">
-	<head>
-		<title>&customizeMode.tabTitle;</title>
-		<link rel="icon" type="image/x-icon"
-		      href="chrome://browser/skin/customizableui/customizeFavicon.ico"/>
-	</head>
-	<body></body>
-</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/content/aboutCustomizing.xul
@@ -0,0 +1,22 @@
+<?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/. -->
+
+<!DOCTYPE window [
+  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+  %brandDTD;
+  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+  %browserDTD;
+]>
+
+<window id="aboutCustomizingWindow"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        xmlns:html="http://www.w3.org/1999/xhtml"
+        title="&customizeMode.tabTitle;">
+  <html:head>
+    <html:link rel="icon" type="image/x-icon"
+               href="chrome://browser/skin/customizableui/customizeFavicon.ico"/>
+  </html:head>
+</window>
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -25,17 +25,17 @@
 #NB: because oncommand fires after click, by the time we've fired, the checkbox binding
 #    will already have switched the button's state, so this is correct:
               oncommand="gCustomizeMode.toggleTitlebar(this.hasAttribute('checked'))"/>
 #endif
       <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
         <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
       </button>
       <spacer flex="1"/>
-      <button id="customization-undo-reset"
+      <button id="customization-undo-reset-button"
               class="customizationmode-button"
               hidden="true"
               oncommand="gCustomizeMode.undoReset();"
               label="&undoCmd.label;"/>
       <button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
     </hbox>
   </vbox>
   <vbox id="customization-panel-container">
--- a/browser/components/customizableui/content/jar.mn
+++ b/browser/components/customizableui/content/jar.mn
@@ -1,11 +1,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/.
 
 browser.jar:
-  content/browser/customizableui/aboutCustomizing.xhtml
+  content/browser/customizableui/aboutCustomizing.xul
   content/browser/customizableui/panelUI.css
 * content/browser/customizableui/panelUI.js
   content/browser/customizableui/panelUI.xml
   content/browser/customizableui/toolbar.xml
 
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -217,13 +217,15 @@
     </svg:defs>
   </svg:svg>
 </panel>
 
 <panel id="widget-overflow"
        role="group"
        type="arrow"
        level="top"
+       context="toolbar-context-menu"
        hidden="true">
   <vbox id="widget-overflow-scroller">
-    <vbox id="widget-overflow-list" class="widget-overflow-list"/>
+    <vbox id="widget-overflow-list" class="widget-overflow-list"
+          overflowfortoolbar="nav-bar"/>
   </vbox>
 </panel>
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -31,16 +31,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const kSpecialWidgetPfx = "customizableui-special-";
 
 const kPrefCustomizationState        = "browser.uiCustomization.state";
 const kPrefCustomizationAutoAdd      = "browser.uiCustomization.autoAdd";
 const kPrefCustomizationDebug        = "browser.uiCustomization.debug";
+const kPrefDrawInTitlebar            = "browser.tabs.drawInTitlebar";
 
 /**
  * The keys are the handlers that are fired when the event type (the value)
  * is fired on the subview. A widget that provides a subview has the option
  * of providing onViewShowing and onViewHiding event handlers.
  */
 const kSubviewEvents = [
   "ViewShowing",
@@ -123,17 +124,20 @@ let gBuildAreas = new Map();
  */
 let gBuildWindows = new Map();
 
 let gNewElementCount = 0;
 let gGroupWrapperCache = new Map();
 let gSingleWrapperCache = new WeakMap();
 let gListeners = new Set();
 
-let gUIStateBeforeReset = null;
+let gUIStateBeforeReset = {
+  uiCustomizationState: null,
+  drawInTitlebar: null,
+};
 
 let gModuleName = "[CustomizableUI]";
 #include logging.js
 
 let CustomizableUIInternal = {
   initialize: function() {
     LOG("Initializing");
 
@@ -692,17 +696,17 @@ let CustomizableUIInternal = {
 
     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
   },
 
   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
     this.insertNode(aWidgetId, aArea, aPosition, true);
 
     if (!gResetting) {
-      gUIStateBeforeReset = null;
+      this._clearPreviousUIState();
     }
   },
 
   onWidgetRemoved: function(aWidgetId, aArea) {
     let areaNodes = gBuildAreas.get(aArea);
     if (!areaNodes) {
       return;
     }
@@ -751,29 +755,29 @@ let CustomizableUIInternal = {
       }
 
       let windowCache = gSingleWrapperCache.get(window);
       if (windowCache) {
         windowCache.delete(aWidgetId);
       }
     }
     if (!gResetting) {
-      gUIStateBeforeReset = null;
+      this._clearPreviousUIState();
     }
   },
 
   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
     this.insertNode(aWidgetId, aArea, aNewPosition);
     if (!gResetting) {
-      gUIStateBeforeReset = null;
+      this._clearPreviousUIState();
     }
   },
 
   onCustomizeEnd: function(aWindow) {
-    gUIStateBeforeReset = null;
+    this._clearPreviousUIState();
   },
 
   registerBuildArea: function(aArea, aNode) {
     // We ensure that the window is registered to have its customization data
     // cleaned up when unloading.
     let window = aNode.ownerDocument.defaultView;
     if (window.closed) {
       return;
@@ -2070,20 +2074,22 @@ let CustomizableUIInternal = {
     // was reset above.
     this._rebuildRegisteredAreas();
 
     gResetting = false;
   },
 
   _resetUIState: function() {
     try {
-      gUIStateBeforeReset = Services.prefs.getCharPref(kPrefCustomizationState);
+      gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
+      gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
     } catch(e) { }
 
     Services.prefs.clearUserPref(kPrefCustomizationState);
+    Services.prefs.clearUserPref(kPrefDrawInTitlebar);
     LOG("State reset");
 
     // Reset placements to make restoring default placements possible.
     gPlacements = new Map();
     gDirtyAreaCache = new Set();
     gSeenWidgets = new Set();
     // Clear the saved state to ensure that defaults will be used.
     gSavedState = null;
@@ -2108,27 +2114,41 @@ let CustomizableUIInternal = {
       }
     }
   },
 
   /**
    * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
    */
   undoReset: function() {
-    if (!gUIStateBeforeReset) {
+    if (gUIStateBeforeReset.uiCustomizationState == null ||
+        gUIStateBeforeReset.drawInTitlebar == null) {
       return;
     }
-    Services.prefs.setCharPref(kPrefCustomizationState, gUIStateBeforeReset);
+    let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
+    let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
+
+    // Need to clear the previous state before setting the prefs
+    // because pref observers may check if there is a previous UI state.
+    this._clearPreviousUIState();
+
+    Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
+    Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
     this.loadSavedState();
     for (let areaId of Object.keys(gSavedState.placements)) {
       let placements = gSavedState.placements[areaId];
       gPlacements.set(areaId, placements);
     }
     this._rebuildRegisteredAreas();
-    gUIStateBeforeReset = null;
+  },
+
+  _clearPreviousUIState: function() {
+    Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
+      gUIStateBeforeReset[prop] = null;
+    });
   },
 
   /**
    * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
    * @return {Boolean} whether the widget is removable
    */
   isWidgetRemovable: function(aWidget) {
     let widgetId;
@@ -2265,16 +2285,21 @@ let CustomizableUIInternal = {
         if (currentPlacements[i] != defaultPlacements[i]) {
           LOG("Found " + currentPlacements[i] + " in " + areaId + " where " +
               defaultPlacements[i] + " was expected!");
           return false;
         }
       }
     }
 
+    if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
+      LOG(kPrefDrawInTitlebar + " pref is non-default");
+      return false;
+    }
+
     return true;
   }
 };
 Object.freeze(CustomizableUIInternal);
 
 this.CustomizableUI = {
   /**
    * Constant reference to the ID of the menu panel.
@@ -2888,17 +2913,18 @@ this.CustomizableUI = {
 
   /**
    * Can the last Restore Defaults operation be undone.
    *
    * @return A boolean stating whether an undo of the
    *         Restore Defaults can be performed.
    */
   get canUndoReset() {
-    return !!gUIStateBeforeReset;
+    return gUIStateBeforeReset.uiCustomizationState != null ||
+           gUIStateBeforeReset.drawInTitlebar != null;
   },
 
   /**
    * Get the placement of a widget. This is by far the best way to obtain
    * information about what the state of your widget is. The internals of
    * this call are cheap (no DOM necessary) and you will know where the user
    * has put your widget.
    *
@@ -3390,17 +3416,21 @@ OverflowableToolbar.prototype = {
   },
 
   handleEvent: function(aEvent) {
     switch(aEvent.type) {
       case "resize":
         this._onResize(aEvent);
         break;
       case "command":
-        this._onClickChevron(aEvent);
+        if (aEvent.target == this._chevron) {
+          this._onClickChevron(aEvent);
+        } else {
+          this._panel.hidePopup();
+        }
         break;
       case "popuphiding":
         this._onPanelHiding(aEvent);
         break;
       case "customizationstarting":
         this._disable();
         break;
       case "aftercustomization":
@@ -3425,29 +3455,34 @@ OverflowableToolbar.prototype = {
       this.removeEventListener("popupshown", onPopupShown);
       deferred.resolve();
     });
 
     return deferred.promise;
   },
 
   _onClickChevron: function(aEvent) {
-    if (this._chevron.open)
+    if (this._chevron.open) {
       this._panel.hidePopup();
-    else {
+    } else {
       let doc = aEvent.target.ownerDocument;
       this._panel.hidden = false;
+      let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
+      gELS.addSystemEventListener(contextMenu, 'command', this, true);
       let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
       this._panel.openPopup(anchor || this._chevron, "bottomcenter topright");
     }
     this._chevron.open = !this._chevron.open;
   },
 
   _onPanelHiding: function(aEvent) {
     this._chevron.open = false;
+    let doc = aEvent.target.ownerDocument;
+    let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
+    gELS.removeSystemEventListener(contextMenu, 'command', this, true);
   },
 
   onOverflow: function(aEvent) {
     if (!this._enabled ||
         (aEvent && aEvent.target != this._toolbar.customizationTarget))
       return;
 
     let child = this._target.lastChild;
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -492,22 +492,44 @@ CustomizeMode.prototype = {
 
   dispatchToolboxEvent: function(aEventType, aDetails={}) {
     let evt = this.document.createEvent("CustomEvent");
     evt.initCustomEvent(aEventType, true, true, {changed: this._changed});
     let result = this.window.gNavToolbox.dispatchEvent(evt);
   },
 
   _getCustomizableChildForNode: function(aNode) {
-    let area = this._getCustomizableParent(aNode);
-    area = area.customizationTarget || area;
-    while (aNode && aNode.parentNode != area) {
-      aNode = aNode.parentNode;
+    // NB: adjusted from _getCustomizableParent to keep that method fast
+    // (it's used during drags), and avoid multiple DOM loops
+    let areas = CustomizableUI.areas;
+    // Caching this length is important because otherwise we'll also iterate
+    // over items we add to the end from within the loop.
+    let numberOfAreas = areas.length;
+    for (let i = 0; i < numberOfAreas; i++) {
+      let area = areas[i];
+      let areaNode = aNode.ownerDocument.getElementById(area);
+      let customizationTarget = areaNode && areaNode.customizationTarget;
+      if (customizationTarget && customizationTarget != areaNode) {
+        areas.push(customizationTarget.id);
+      }
+      let overflowTarget = areaNode.getAttribute("overflowtarget");
+      if (overflowTarget) {
+        areas.push(overflowTarget);
+      }
     }
-    return aNode;
+    areas.push(kPaletteId);
+
+    while (aNode && aNode.parentNode) {
+      let parent = aNode.parentNode;
+      if (areas.indexOf(parent.id) != -1) {
+        return aNode;
+      }
+      aNode = parent;
+    }
+    return null;
   },
 
   addToToolbar: function(aNode) {
     aNode = this._getCustomizableChildForNode(aNode);
     if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
       aNode = aNode.firstChild;
     }
     CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
@@ -994,18 +1016,18 @@ CustomizeMode.prototype = {
   },
 
   _updateResetButton: function() {
     let btn = this.document.getElementById("customization-reset-button");
     btn.disabled = CustomizableUI.inDefaultState;
   },
 
   _updateUndoResetButton: function() {
-    let undoReset =  this.document.getElementById("customization-undo-reset");
-    undoReset.hidden = !CustomizableUI.canUndoReset;
+    let undoResetButton =  this.document.getElementById("customization-undo-reset-button");
+    undoResetButton.hidden = !CustomizableUI.canUndoReset;
   },
 
   handleEvent: function(aEvent) {
     switch(aEvent.type) {
       case "toolbarvisibilitychange":
         this._onToolbarVisibilityChange(aEvent);
         break;
       case "dragstart":
@@ -1047,17 +1069,19 @@ CustomizeMode.prototype = {
 #endif
     }
   },
 
 #ifdef CAN_DRAW_IN_TITLEBAR
   observe: function(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "nsPref:changed":
+        this._updateResetButton();
         this._updateTitlebarButton();
+        this._updateUndoResetButton();
         break;
     }
   },
 
   _updateTitlebarButton: function() {
     let drawInTitlebar = true;
     try {
       drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref);
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -6,16 +6,18 @@ support-files =
 [browser_876926_customize_mode_wrapping.js]
 [browser_876944_customize_mode_create_destroy.js]
 [browser_877006_missing_view.js]
 [browser_877178_unregisterArea.js]
 [browser_877447_skip_missing_ids.js]
 [browser_878452_drag_to_panel.js]
 [browser_880164_customization_context_menus.js]
 [browser_880382_drag_wide_widgets_in_panel.js]
+[browser_884402_customize_from_overflow.js]
+skip-if = os == "linux"
 [browser_885052_customize_mode_observers_disabed.js]
 # Bug 951403 - Disabled on OSX for frequent failures
 skip-if = os == "mac"
 
 [browser_885530_showInPrivateBrowsing.js]
 [browser_886323_buildArea_removable_nodes.js]
 [browser_887438_currentset_shim.js]
 [browser_888817_currentset_updating.js]
--- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js
@@ -300,63 +300,8 @@ add_task(function() {
   contextMenu.hidePopup();
   yield hiddenContextPromise;
 
   let hiddenPromise = promisePanelHidden(window);
   PanelUI.hide();
   yield hiddenPromise;
 });
 
-function contextMenuShown(aContextMenu) {
-  let deferred = Promise.defer();
-  let win = aContextMenu.ownerDocument.defaultView;
-  let timeoutId = win.setTimeout(() => {
-    deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
-  }, 20000);
-  function onPopupShown(e) {
-    aContextMenu.removeEventListener("popupshown", onPopupShown);
-    win.clearTimeout(timeoutId);
-    deferred.resolve();
-  };
-  aContextMenu.addEventListener("popupshown", onPopupShown);
-  return deferred.promise;
-}
-
-function contextMenuHidden(aContextMenu) {
-  let deferred = Promise.defer();
-  let win = aContextMenu.ownerDocument.defaultView;
-  let timeoutId = win.setTimeout(() => {
-    deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
-  }, 20000);
-  function onPopupHidden(e) {
-    win.clearTimeout(timeoutId);
-    aContextMenu.removeEventListener("popuphidden", onPopupHidden);
-    deferred.resolve();
-  };
-  aContextMenu.addEventListener("popuphidden", onPopupHidden);
-  return deferred.promise;
-}
-
-// This is a simpler version of the context menu check that
-// exists in contextmenu_common.js.
-function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
-  let childNodes = aContextMenu.childNodes;
-  for (let i = 0; i < childNodes.length; i++) {
-    let menuitem = childNodes[i];
-    try {
-      if (aExpectedEntries[i][0] == "---") {
-        is(menuitem.localName, "menuseparator", "menuseparator expected");
-        continue;
-      }
-
-      let selector = aExpectedEntries[i][0];
-      ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector");
-      let commandValue = menuitem.getAttribute("command");
-      let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null;
-      let menuItemDisabled = relatedCommand ?
-                               relatedCommand.getAttribute("disabled") == "true" :
-                               menuitem.getAttribute("disabled") == "true";
-      is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector);
-    } catch (e) {
-      ok(false, "Exception when checking context menu: " + e);
-    }
-  }
-}
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js
@@ -0,0 +1,78 @@
+"use strict";
+
+let overflowPanel = document.getElementById("widget-overflow");
+
+const isOSX = (Services.appinfo.OS === "Darwin");
+
+let originalWindowWidth;
+registerCleanupFunction(function() {
+  window.resizeTo(originalWindowWidth, window.outerHeight);
+});
+
+// Right-click on an item within the overflow panel should
+// show a context menu with options to move it.
+add_task(function() {
+  originalWindowWidth = window.outerWidth;
+  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+  ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
+  let oldChildCount = navbar.customizationTarget.childElementCount;
+  window.resizeTo(400, window.outerHeight);
+
+  yield waitForCondition(() => navbar.hasAttribute("overflowing"));
+  ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
+
+  let chevron = document.getElementById("nav-bar-overflow-button");
+  let shownPanelPromise = promisePanelElementShown(window, overflowPanel);
+  chevron.click();
+  yield shownPanelPromise;
+
+  let contextMenu = document.getElementById("toolbar-context-menu");
+  let shownContextPromise = contextMenuShown(contextMenu);
+  let homeButton = document.getElementById("home-button");
+  ok(homeButton, "home-button was found");
+  ok(homeButton.classList.contains("overflowedItem"), "Home button is overflowing");
+  EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2});
+  yield shownContextPromise;
+
+  is(overflowPanel.state, "open", "The widget overflow panel should still be open.");
+
+  let expectedEntries = [
+    [".customize-context-moveToPanel", true],
+    [".customize-context-removeFromToolbar", true],
+    ["---"]
+  ];
+  if (!isOSX) {
+    expectedEntries.push(["#toggle_toolbar-menubar", true]);
+  }
+  expectedEntries.push(
+    ["#toggle_PersonalToolbar", true],
+    ["---"],
+    [".viewCustomizeToolbar", true]
+  );
+  checkContextMenu(contextMenu, expectedEntries);
+
+  let hiddenContextPromise = contextMenuHidden(contextMenu);
+  let hiddenPromise = promisePanelElementHidden(window, overflowPanel);
+  let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel");
+  if (moveToPanel) {
+    moveToPanel.click();
+  }
+  contextMenu.hidePopup();
+  yield hiddenContextPromise;
+  yield hiddenPromise;
+
+  let homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button");
+  ok(homeButtonPlacement, "Home button should still have a placement");
+  is(homeButtonPlacement && homeButtonPlacement.area, "PanelUI-contents", "Home button should be in the panel now");
+  CustomizableUI.reset();
+
+  // In some cases, it can take a tick for the navbar to overflow again. Wait for it:
+  yield waitForCondition(() => navbar.hasAttribute("overflowing"));
+  ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
+
+  let homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button");
+  ok(homeButtonPlacement, "Home button should still have a placement");
+  is(homeButtonPlacement && homeButtonPlacement.area, "nav-bar", "Home button should be back in the navbar now");
+
+  ok(homeButton.classList.contains("overflowedItem"), "Home button should still be overflowed");
+});
--- a/browser/components/customizableui/test/browser_970511_undo_restore_default.js
+++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js
@@ -6,60 +6,102 @@
 
 // Restoring default should show an "undo" option which undoes the restoring operation.
 add_task(function() {
   let homeButtonId = "home-button";
   CustomizableUI.removeWidgetFromArea(homeButtonId);
   yield startCustomizing();
   ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
   is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
-  let undoReset = document.getElementById("customization-undo-reset");
-  is(undoReset.hidden, true, "The undo button is hidden before reset");
+  let undoResetButton = document.getElementById("customization-undo-reset-button");
+  is(undoResetButton.hidden, true, "The undo button is hidden before reset");
 
   yield gCustomizeMode.reset();
 
   ok(CustomizableUI.inDefaultState, "In default state after reset");
-  is(undoReset.hidden, false, "The undo button is visible after reset");
+  is(undoResetButton.hidden, false, "The undo button is visible after reset");
 
-  undoReset.click();
+  undoResetButton.click();
   yield waitForCondition(function() !gCustomizeMode.resetting);
   ok(!CustomizableUI.inDefaultState, "Not in default state after reset-undo");
-  is(undoReset.hidden, true, "The undo button is hidden after clicking on the undo button");
+  is(undoResetButton.hidden, true, "The undo button is hidden after clicking on the undo button");
   is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
 
   yield gCustomizeMode.reset();
 });
 
 // Performing an action after a reset will hide the reset button.
 add_task(function() {
   let homeButtonId = "home-button";
   CustomizableUI.removeWidgetFromArea(homeButtonId);
   ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
   is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
-  let undoReset = document.getElementById("customization-undo-reset");
-  is(undoReset.hidden, true, "The undo button is hidden before reset");
+  let undoResetButton = document.getElementById("customization-undo-reset-button");
+  is(undoResetButton.hidden, true, "The undo button is hidden before reset");
 
   yield gCustomizeMode.reset();
 
   ok(CustomizableUI.inDefaultState, "In default state after reset");
-  is(undoReset.hidden, false, "The undo button is visible after reset");
+  is(undoResetButton.hidden, false, "The undo button is visible after reset");
 
   CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL);
-  is(undoReset.hidden, true, "The undo button is hidden after another change");
+  is(undoResetButton.hidden, true, "The undo button is hidden after another change");
 });
 
 // "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button
 add_task(function() {
-  let undoReset = document.getElementById("customization-undo-reset");
-  is(undoReset.hidden, true, "The undo button is hidden before a reset");
+  let undoResetButton = document.getElementById("customization-undo-reset-button");
+  is(undoResetButton.hidden, true, "The undo button is hidden before a reset");
   ok(!CustomizableUI.inDefaultState, "The browser should not be in default state");
   yield gCustomizeMode.reset();
 
-  is(undoReset.hidden, false, "The undo button is hidden after a reset");
+  is(undoResetButton.hidden, false, "The undo button is visible after a reset");
   yield endCustomizing();
   yield startCustomizing();
-  is(undoReset.hidden, true, "The undo reset button should be hidden after entering customization mode");
+  is(undoResetButton.hidden, true, "The undo reset button should be hidden after entering customization mode");
+});
+
+// Bug 971626 - Restore Defaults should collapse the Title Bar
+add_task(function() {
+  if (Services.appinfo.OS != "WINNT" &&
+      Services.appinfo.OS != "Darwin") {
+    return;
+  }
+  let prefName = "browser.tabs.drawInTitlebar";
+  let defaultValue = Services.prefs.getBoolPref(prefName);
+  let restoreDefaultsButton = document.getElementById("customization-reset-button");
+  let titleBarButton = document.getElementById("customization-titlebar-visibility-button");
+  let undoResetButton = document.getElementById("customization-undo-reset-button");
+  ok(CustomizableUI.inDefaultState, "Should be in default state at start of test");
+  ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled when in default state");
+  is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect pref value");
+  is(undoResetButton.hidden, true, "Undo reset button should be hidden at start of test");
+
+  Services.prefs.setBoolPref(prefName, !defaultValue);
+  ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled when pref changed");
+  is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect changed pref value");
+  ok(!CustomizableUI.inDefaultState, "With titlebar flipped, no longer default");
+  is(undoResetButton.hidden, true, "Undo reset button should be hidden after pref change");
+
+  yield gCustomizeMode.reset();
+  ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled after reset");
+  is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect default value after reset");
+  is(Services.prefs.getBoolPref(prefName), defaultValue, "Reset should reset drawInTitlebar");
+  ok(CustomizableUI.inDefaultState, "In default state after titlebar reset");
+  is(undoResetButton.hidden, false, "Undo reset button should be visible after reset");
+  ok(!undoResetButton.disabled, "Undo reset button should be enabled after reset");
+
+  yield gCustomizeMode.undoReset();
+  ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled after undo-reset");
+  is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect undo-reset value");
+  ok(!CustomizableUI.inDefaultState, "No longer in default state after undo");
+  is(Services.prefs.getBoolPref(prefName), !defaultValue, "Undo-reset goes back to previous pref value");
+  is(undoResetButton.hidden, true, "Undo reset button should be hidden after undo-reset clicked");
+
+  Services.prefs.clearUserPref(prefName);
+  ok(CustomizableUI.inDefaultState, "In default state after pref cleared");
+  is(undoResetButton.hidden, true, "Undo reset button should be hidden at end of test");
 });
 
 add_task(function asyncCleanup() {
   yield gCustomizeMode.reset();
   yield endCustomizing();
 });
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -371,8 +371,65 @@ function promiseTabHistoryNavigation(aDi
     }
   }
   gBrowser.addEventListener("pageshow", listener, true);
 
   content.history.go(aDirection);
 
   return deferred.promise;
 }
+
+function contextMenuShown(aContextMenu) {
+  let deferred = Promise.defer();
+  let win = aContextMenu.ownerDocument.defaultView;
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
+  }, 20000);
+  function onPopupShown(e) {
+    aContextMenu.removeEventListener("popupshown", onPopupShown);
+    win.clearTimeout(timeoutId);
+    deferred.resolve();
+  };
+  aContextMenu.addEventListener("popupshown", onPopupShown);
+  return deferred.promise;
+}
+
+function contextMenuHidden(aContextMenu) {
+  let deferred = Promise.defer();
+  let win = aContextMenu.ownerDocument.defaultView;
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
+  }, 20000);
+  function onPopupHidden(e) {
+    win.clearTimeout(timeoutId);
+    aContextMenu.removeEventListener("popuphidden", onPopupHidden);
+    deferred.resolve();
+  };
+  aContextMenu.addEventListener("popuphidden", onPopupHidden);
+  return deferred.promise;
+}
+
+
+// This is a simpler version of the context menu check that
+// exists in contextmenu_common.js.
+function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
+  let childNodes = aContextMenu.childNodes;
+  for (let i = 0; i < childNodes.length; i++) {
+    let menuitem = childNodes[i];
+    try {
+      if (aExpectedEntries[i][0] == "---") {
+        is(menuitem.localName, "menuseparator", "menuseparator expected");
+        continue;
+      }
+
+      let selector = aExpectedEntries[i][0];
+      ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector");
+      let commandValue = menuitem.getAttribute("command");
+      let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null;
+      let menuItemDisabled = relatedCommand ?
+                               relatedCommand.getAttribute("disabled") == "true" :
+                               menuitem.getAttribute("disabled") == "true";
+      is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector);
+    } catch (e) {
+      ok(false, "Exception when checking context menu: " + e);
+    }
+  }
+}
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -42,16 +42,17 @@ support-files =
   doc_editor-mode.html
   doc_empty-tab-01.html
   doc_empty-tab-02.html
   doc_event-listeners.html
   doc_event-listeners-02.html
   doc_frame-parameters.html
   doc_function-display-name.html
   doc_function-search.html
+  doc_global-method-override.html
   doc_iframes.html
   doc_included-script.html
   doc_inline-debugger-statement.html
   doc_inline-script.html
   doc_large-array-buffer.html
   doc_minified.html
   doc_minified_bogus_map.html
   doc_no-page-sources.html
@@ -117,16 +118,17 @@ support-files =
 [browser_dbg_controller-evaluate-01.js]
 [browser_dbg_controller-evaluate-02.js]
 [browser_dbg_debugger-statement.js]
 [browser_dbg_editor-contextmenu.js]
 [browser_dbg_editor-mode.js]
 [browser_dbg_event-listeners.js]
 [browser_dbg_file-reload.js]
 [browser_dbg_function-display-name.js]
+[browser_dbg_global-method-override.js]
 [browser_dbg_globalactor.js]
 [browser_dbg_host-layout.js]
 [browser_dbg_iframes.js]
 [browser_dbg_instruments-pane-collapse.js]
 [browser_dbg_listaddons.js]
 [browser_dbg_listtabs-01.js]
 [browser_dbg_listtabs-02.js]
 [browser_dbg_location-changes-01-simple.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_global-method-override.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that scripts that override properties of the global object, like
+ * toString don't break the debugger. The test page used to cause the debugger
+ * to throw when trying to attach to the thread actor.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_global-method-override.html";
+
+function test() {
+  initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
+    let gDebugger = aPanel.panelWin;
+    ok(gDebugger, "Should have a debugger available.");
+    is(gDebugger.gThreadClient.state, "attached", "Debugger should be attached.");
+
+    closeDebuggerAndFinish(aPanel);
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/doc_global-method-override.html
@@ -0,0 +1,16 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Debugger global method override test page</title>
+  </head>
+  <body>
+    <script type="text/javascript">
+      console.log( "Error: " + toString( { x: 0, y: 0 } ) );
+      function toString(v) { return "[ " + v.x + ", " + v.y + " ]"; }
+    </script>
+  </body>
+</html>
--- a/browser/devtools/netmonitor/netmonitor-controller.js
+++ b/browser/devtools/netmonitor/netmonitor-controller.js
@@ -55,17 +55,20 @@ const EVENTS = {
   REQUEST_POST_PARAMS_DISPLAYED: "NetMonitor:RequestPostParamsAvailable",
 
   // When the response body is displayed in the UI.
   RESPONSE_BODY_DISPLAYED: "NetMonitor:ResponseBodyAvailable",
 
   // When the html response preview is displayed in the UI.
   RESPONSE_HTML_PREVIEW_DISPLAYED: "NetMonitor:ResponseHtmlPreviewAvailable",
 
-  // When `onTabSelect` is fired and subsequently rendered.
+  // When the image response thumbnail is displayed in the UI.
+  RESPONSE_IMAGE_THUMBNAIL_DISPLAYED: "NetMonitor:ResponseImageThumbnailAvailable",
+
+  // When a tab is selected in the NetworkDetailsView and subsequently rendered.
   TAB_UPDATED: "NetMonitor:TabUpdated",
 
   // Fired when Sidebar has finished being populated.
   SIDEBAR_POPULATED: "NetMonitor:SidebarPopulated",
 
   // Fired when NetworkDetailsView has finished being populated.
   NETWORKDETAILSVIEW_POPULATED: "NetMonitor:NetworkDetailsViewPopulated",
 
@@ -100,16 +103,17 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 const EventEmitter = require("devtools/shared/event-emitter");
 const Editor = require("devtools/sourceeditor/editor");
+const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chart",
   "resource:///modules/devtools/Chart.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
--- a/browser/devtools/netmonitor/netmonitor-view.js
+++ b/browser/devtools/netmonitor/netmonitor-view.js
@@ -6,16 +6,18 @@
 "use strict";
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const EPSILON = 0.001;
 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 102400; // 100 KB in bytes
 const RESIZE_REFRESH_RATE = 50; // ms
 const REQUESTS_REFRESH_RATE = 50; // ms
 const REQUESTS_HEADERS_SAFE_BOUNDS = 30; // px
+const REQUESTS_TOOLTIP_POSITION = "topcenter bottomleft";
+const REQUESTS_TOOLTIP_IMAGE_MAX_DIM = 400; // px
 const REQUESTS_WATERFALL_SAFE_BOUNDS = 90; // px
 const REQUESTS_WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_HEADER_TICKS_SPACING_MIN = 60; // px
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SCALES = 3;
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
 const REQUESTS_WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
@@ -313,17 +315,19 @@ ToolbarView.prototype = {
  * Functions handling the requests menu (containing details about each request,
  * like status, method, file, domain, as well as a waterfall representing
  * timing imformation).
  */
 function RequestsMenuView() {
   dumpn("RequestsMenuView was instantiated");
 
   this._flushRequests = this._flushRequests.bind(this);
+  this._onHover = this._onHover.bind(this);
   this._onSelect = this._onSelect.bind(this);
+  this._onSwap = this._onSwap.bind(this);
   this._onResize = this._onResize.bind(this);
   this._byFile = this._byFile.bind(this);
   this._byDomain = this._byDomain.bind(this);
   this._byType = this._byType.bind(this);
 }
 
 RequestsMenuView.prototype = Heritage.extend(WidgetMethods, {
   /**
@@ -340,16 +344,17 @@ RequestsMenuView.prototype = Heritage.ex
     Prefs.filters.forEach(type => this.filterOn(type));
     this.sortContents(this._byTiming);
 
     this.allowFocusOnRightClick = true;
     this.maintainSelectionVisible = true;
     this.widget.autoscrollWithAppendedItems = true;
 
     this.widget.addEventListener("select", this._onSelect, false);
+    this.widget.addEventListener("swap", this._onSwap, false);
     this._splitter.addEventListener("mousemove", this._onResize, false);
     window.addEventListener("resize", this._onResize, false);
 
     this.requestsMenuSortEvent = getKeyWithEvent(this.sortBy.bind(this));
     this.requestsMenuFilterEvent = getKeyWithEvent(this.filterOn.bind(this));
     this.reqeustsMenuClearEvent = this.clear.bind(this);
     this._onContextShowing = this._onContextShowing.bind(this);
     this._onContextNewTabCommand = this.openRequestInTab.bind(this);
@@ -385,16 +390,17 @@ RequestsMenuView.prototype = Heritage.ex
    * Destruction function, called when the network monitor is closed.
    */
   destroy: function() {
     dumpn("Destroying the SourcesView");
 
     Prefs.filters = this._activeFilters;
 
     this.widget.removeEventListener("select", this._onSelect, false);
+    this.widget.removeEventListener("swap", this._onSwap, false);
     this._splitter.removeEventListener("mousemove", this._onResize, false);
     window.removeEventListener("resize", this._onResize, false);
 
     $("#toolbar-labels").removeEventListener("click", this.requestsMenuSortEvent, false);
     $("#requests-menu-footer").removeEventListener("click", this.requestsMenuFilterEvent, false);
     $("#requests-menu-clear-button").removeEventListener("click", this.reqeustsMenuClearEvent, false);
     $("#network-request-popup").removeEventListener("popupshowing", this._onContextShowing, false);
     $("#request-menu-context-newtab").removeEventListener("command", this._onContextNewTabCommand, false);
@@ -458,21 +464,31 @@ RequestsMenuView.prototype = Heritage.ex
         startedDeltaMillis: unixTime - this._firstRequestStartedMillis,
         startedMillis: unixTime,
         method: aMethod,
         url: aUrl,
         isXHR: aIsXHR
       }
     });
 
+    // Create a tooltip for the newly appended network request item.
+    let requestTooltip = requestItem.attachment.tooltip = new Tooltip(document, {
+      closeOnEvents: [{
+        emitter: $("#requests-menu-contents"),
+        event: "scroll",
+        useCapture: true
+      }]
+    });
+
     $("#details-pane-toggle").disabled = false;
     $("#requests-menu-empty-notice").hidden = true;
 
     this.refreshSummary();
     this.refreshZebra();
+    this.refreshTooltip(requestItem);
 
     if (aId == this._preferredItemId) {
       this.selectedItem = requestItem;
     }
   },
 
   /**
    * Opens selected item in a new tab.
@@ -492,16 +508,17 @@ RequestsMenuView.prototype = Heritage.ex
   },
 
   /**
    * Copy image as data uri.
    */
   copyImageAsDataUri: function() {
     let selected = this.selectedItem.attachment;
     let { mimeType, text, encoding } = selected.responseContent.content;
+
     gNetwork.getString(text).then(aString => {
       let data = "data:" + mimeType + ";" + encoding + "," + aString;
       clipboardHelper.copyString(data, document);
     });
   },
 
   /**
    * Create a new custom request form populated with the data from
@@ -918,16 +935,29 @@ RequestsMenuView.prototype = Heritage.ex
       } else {
         requestTarget.setAttribute("odd", "");
         requestTarget.removeAttribute("even");
       }
     }
   },
 
   /**
+   * Refreshes the toggling anchor for the specified item's tooltip.
+   *
+   * @param object aItem
+   *        The network request item in this container.
+   */
+  refreshTooltip: function(aItem) {
+    let tooltip = aItem.attachment.tooltip;
+    tooltip.hide();
+    tooltip.startTogglingOnHover(aItem.target, this._onHover);
+    tooltip.defaultPosition = REQUESTS_TOOLTIP_POSITION;
+  },
+
+  /**
    * Schedules adding additional information to a network request.
    *
    * @param string aId
    *        An identifier coming from the network monitor controller.
    * @param object aData
    *        An object containing several { key: value } tuples of network info.
    *        Supported keys are "httpVersion", "status", "statusText" etc.
    */
@@ -1005,23 +1035,24 @@ RequestsMenuView.prototype = Heritage.ex
             requestItem.attachment.contentSize = value;
             this.updateMenuView(requestItem, key, value);
             break;
           case "mimeType":
             requestItem.attachment.mimeType = value;
             this.updateMenuView(requestItem, key, value);
             break;
           case "responseContent":
-            requestItem.attachment.responseContent = value;
             // If there's no mime type available when the response content
             // is received, assume text/plain as a fallback.
             if (!requestItem.attachment.mimeType) {
               requestItem.attachment.mimeType = "text/plain";
               this.updateMenuView(requestItem, "mimeType", "text/plain");
             }
+            requestItem.attachment.responseContent = value;
+            this.updateMenuView(requestItem, key, value);
             break;
           case "totalTime":
             requestItem.attachment.totalTime = value;
             requestItem.attachment.endedMillis = requestItem.attachment.startedMillis + value;
             this.updateMenuView(requestItem, key, value);
             this._registerLastRequestEnd(requestItem.attachment.endedMillis);
             break;
           case "eventTimings":
@@ -1107,19 +1138,19 @@ RequestsMenuView.prototype = Heritage.ex
         try {
           uri = nsIURL(aValue);
         } catch(e) {
           break; // User input may not make a well-formed url yet.
         }
         let nameWithQuery = this._getUriNameWithQuery(uri);
         let hostPort = this._getUriHostPort(uri);
 
-        let node = $(".requests-menu-file", target);
-        node.setAttribute("value", nameWithQuery);
-        node.setAttribute("tooltiptext", nameWithQuery);
+        let file = $(".requests-menu-file", target);
+        file.setAttribute("value", nameWithQuery);
+        file.setAttribute("tooltiptext", nameWithQuery);
 
         let domain = $(".requests-menu-domain", target);
         domain.setAttribute("value", hostPort);
         domain.setAttribute("tooltiptext", hostPort);
         break;
       }
       case "status": {
         let node = $(".requests-menu-status", target);
@@ -1143,16 +1174,31 @@ RequestsMenuView.prototype = Heritage.ex
       case "mimeType": {
         let type = this._getAbbreviatedMimeType(aValue);
         let node = $(".requests-menu-type", target);
         let text = CONTENT_MIME_TYPE_ABBREVIATIONS[type] || type;
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", aValue);
         break;
       }
+      case "responseContent": {
+        let { mimeType } = aItem.attachment;
+        let { text, encoding } = aValue.content;
+
+        if (mimeType.contains("image/")) {
+          gNetwork.getString(text).then(aString => {
+            let node = $(".requests-menu-icon", aItem.target);
+            node.src = "data:" + mimeType + ";" + encoding + "," + aString;
+            node.setAttribute("type", "thumbnail");
+            node.removeAttribute("hidden");
+            window.emit(EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED);
+          });
+        }
+        break;
+      }
       case "totalTime": {
         let node = $(".requests-menu-timings-total", target);
         let text = L10N.getFormatStr("networkMenu.totalMS", aValue); // integer
         node.setAttribute("value", text);
         node.setAttribute("tooltiptext", text);
         break;
       }
     }
@@ -1421,16 +1467,59 @@ RequestsMenuView.prototype = Heritage.ex
       NetMonitorView.Sidebar.populate(item.attachment);
       NetMonitorView.Sidebar.toggle(true);
     } else {
       NetMonitorView.Sidebar.toggle(false);
     }
   },
 
   /**
+   * The swap listener for this container.
+   * Called when two items switch places, when the contents are sorted.
+   */
+  _onSwap: function({ detail: [firstItem, secondItem] }) {
+    // Sorting will create new anchor nodes for all the swapped request items
+    // in this container, so it's necessary to refresh the Tooltip instances.
+    this.refreshTooltip(firstItem);
+    this.refreshTooltip(secondItem);
+  },
+
+  /**
+   * The predicate used when deciding whether a popup should be shown
+   * over a request item or not.
+   *
+   * @param nsIDOMNode aTarget
+   *        The element node currently being hovered.
+   * @param object aTooltip
+   *        The current tooltip instance.
+   */
+  _onHover: function(aTarget, aTooltip) {
+    let requestItem = this.getItemForElement(aTarget);
+    if (!requestItem || !requestItem.attachment.responseContent) {
+      return;
+    }
+
+    let hovered = requestItem.attachment;
+    let { url } = hovered;
+    let { mimeType, text, encoding } = hovered.responseContent.content;
+
+    if (mimeType && mimeType.contains("image/") && (
+      aTarget.classList.contains("requests-menu-icon") ||
+      aTarget.classList.contains("requests-menu-file")))
+    {
+      return gNetwork.getString(text).then(aString => {
+        let anchor = $(".requests-menu-icon", requestItem.target);
+        let src = "data:" + mimeType + ";" + encoding + "," + aString;
+        aTooltip.setImageContent(src, { maxDim: REQUESTS_TOOLTIP_IMAGE_MAX_DIM });
+        return anchor;
+      });
+    }
+  },
+
+  /**
    * The resize listener for this container's window.
    */
   _onResize: function(e) {
     // Allow requests to settle down first.
     setNamedTimeout(
       "resize-events", RESIZE_REFRESH_RATE, () => this._flushWaterfallViews(true));
   },
 
@@ -1621,17 +1710,17 @@ SidebarView.prototype = {
    */
   populate: function(aData) {
     let isCustom = aData.isCustom;
     let view = isCustom ?
       NetMonitorView.CustomRequest :
       NetMonitorView.NetworkDetails;
 
     return view.populate(aData).then(() => {
-      $("#details-pane").selectedIndex = isCustom ? 0 : 1
+      $("#details-pane").selectedIndex = isCustom ? 0 : 1;
       window.emit(EVENTS.SIDEBAR_POPULATED);
     });
   }
 }
 
 /**
  * Functions handling the custom request view.
  */
@@ -1901,16 +1990,17 @@ NetworkDetailsView.prototype = {
           yield view._setTimingsInformation(src.eventTimings);
           break;
         case 5: // "Preview"
           yield view._setHtmlPreview(src.responseContent);
           break;
       }
       populated[tab] = true;
       window.emit(EVENTS.TAB_UPDATED);
+      NetMonitorView.RequestsMenu.ensureSelectedItemIsVisible();
     });
   },
 
   /**
    * Sets the network request summary shown in this view.
    *
    * @param object aData
    *        The data source (this should be the attachment of a request item).
--- a/browser/devtools/netmonitor/netmonitor.css
+++ b/browser/devtools/netmonitor/netmonitor.css
@@ -55,12 +55,12 @@
   #network-table[domain-overflows] .requests-menu-domain {
     display: none;
   }
 
   #network-table[type-overflows] .requests-menu-domain {
     -moz-box-flex: 1;
   }
 
-  #network-table[domain-overflows] .requests-menu-file {
+  #network-table[domain-overflows] .requests-menu-icon-and-file {
     -moz-box-flex: 1;
   }
 }
--- a/browser/devtools/netmonitor/netmonitor.xul
+++ b/browser/devtools/netmonitor/netmonitor.xul
@@ -63,18 +63,18 @@
                 </button>
                 <button id="requests-menu-method-button"
                         class="requests-menu-header-button requests-menu-method"
                         data-key="method"
                         label="&netmonitorUI.toolbar.method;"
                         flex="1">
                 </button>
               </hbox>
-              <hbox id="requests-menu-file-header-box"
-                    class="requests-menu-header requests-menu-file"
+              <hbox id="requests-menu-icon-and-file-header-box"
+                    class="requests-menu-header requests-menu-icon-and-file"
                     align="center">
                 <button id="requests-menu-file-button"
                         class="requests-menu-header-button requests-menu-file"
                         data-key="file"
                         label="&netmonitorUI.toolbar.file;"
                         flex="1">
                 </button>
               </hbox>
@@ -147,18 +147,23 @@
             <hbox id="requests-menu-item-template" hidden="true">
               <hbox class="requests-menu-subitem requests-menu-status-and-method"
                     align="center">
                 <box class="requests-menu-status"/>
                 <label class="plain requests-menu-method"
                        crop="end"
                        flex="1"/>
               </hbox>
-              <label class="plain requests-menu-subitem requests-menu-file"
-                     crop="end"/>
+              <hbox class="requests-menu-subitem requests-menu-icon-and-file"
+                    align="center">
+                <image class="requests-menu-icon" hidden="true"/>
+                <label class="plain requests-menu-file"
+                       crop="end"
+                       flex="1"/>
+              </hbox>
               <label class="plain requests-menu-subitem requests-menu-domain"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-type"
                      crop="end"/>
               <label class="plain requests-menu-subitem requests-menu-size"
                      crop="end"/>
               <hbox class="requests-menu-subitem requests-menu-waterfall"
                     align="center"
--- a/browser/devtools/netmonitor/test/browser.ini
+++ b/browser/devtools/netmonitor/test/browser.ini
@@ -42,16 +42,18 @@ support-files =
 [browser_net_copy_url.js]
 [browser_net_cyrillic-01.js]
 [browser_net_cyrillic-02.js]
 [browser_net_filter-01.js]
 [browser_net_filter-02.js]
 [browser_net_filter-03.js]
 [browser_net_footer-summary.js]
 [browser_net_html-preview.js]
+[browser_net_icon-preview.js]
+[browser_net_image-tooltip.js]
 [browser_net_json-long.js]
 [browser_net_json-malformed.js]
 [browser_net_json_custom_mime.js]
 [browser_net_json_text_mime.js]
 [browser_net_jsonp.js]
 [browser_net_large-response.js]
 [browser_net_open_request_in_tab.js]
 [browser_net_page-nav.js]
--- a/browser/devtools/netmonitor/test/browser_net_complex-params.js
+++ b/browser/devtools/netmonitor/test/browser_net_complex-params.js
@@ -133,16 +133,14 @@ function test() {
         aQueryStringParamValue,
         "The first query string param value was incorrect.");
 
       return NetMonitorView.editor("#request-post-data-textarea").then((aEditor) => {
         is(aEditor.getText(), aRequestPayload,
           "The text shown in the source editor is incorrect.");
         is(aEditor.getMode(), Editor.modes[aEditorMode],
           "The mode active in the source editor is incorrect.");
-
-        teardown(aMonitor).then(finish);
       });
     }
 
     aDebuggee.performRequests();
   });
 }
--- a/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js
+++ b/browser/devtools/netmonitor/test/browser_net_copy_image_as_data_uri.js
@@ -9,23 +9,21 @@ function test() {
   initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
     let { NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
-    let imageDataUri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
-
     waitForNetworkEvents(aMonitor, 6).then(() => {
       let requestItem = RequestsMenu.getItemAtIndex(5);
       RequestsMenu.selectedItem = requestItem;
 
-      waitForClipboard(imageDataUri, function setup() {
+      waitForClipboard(TEST_IMAGE_DATA_URI, function setup() {
         RequestsMenu.copyImageAsDataUri();
       }, function onSuccess() {
         ok(true, "Clipboard contains the currently selected image as data uri.");
         cleanUp();
       }, function onFailure() {
         ok(false, "Copying the currently selected image as data uri was unsuccessful.");
         cleanUp();
       });
--- a/browser/devtools/netmonitor/test/browser_net_html-preview.js
+++ b/browser/devtools/netmonitor/test/browser_net_html-preview.js
@@ -4,17 +4,17 @@
 /**
  * Tests if html responses show and properly populate a "Preview" tab.
  */
 
 function test() {
   initNetMonitor(CONTENT_TYPE_URL).then(([aTab, aDebuggee, aMonitor]) => {
     info("Starting test... ");
 
-    let { $, document, NetMonitorView } = aMonitor.panelWin;
+    let { $, document, EVENTS, NetMonitorView } = aMonitor.panelWin;
     let { RequestsMenu } = NetMonitorView;
 
     RequestsMenu.lazyUpdate = false;
 
     waitForNetworkEvents(aMonitor, 6).then(() => {
       EventUtils.sendMouseEvent({ type: "mousedown" },
         document.getElementById("details-pane-toggle"));
 
@@ -30,18 +30,17 @@ function test() {
 
       is($("#event-details-pane").selectedIndex, 5,
         "The fifth tab in the details pane should be selected.");
       is($("#preview-tab").hidden, false,
         "The preview tab should be visible now.");
       is($("#preview-tabpanel").hidden, false,
         "The preview tabpanel should be visible now.");
 
-      let RESPONSE_HTML_PREVIEW_DISPLAYED = aMonitor.panelWin.EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED;
-      waitFor(aMonitor.panelWin, RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
+      waitFor(aMonitor.panelWin, EVENTS.RESPONSE_HTML_PREVIEW_DISPLAYED).then(() => {
         let iframe = $("#response-preview");
         ok(iframe,
           "There should be a response preview iframe available.");
         ok(iframe.contentDocument,
           "The iframe's content document should be available.");
         is(iframe.contentDocument.querySelector("blink").textContent, "Not Found",
           "The iframe's content document should be loaded and correct.");
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_icon-preview.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if image responses show a thumbnail in the requests menu.
+ */
+
+function test() {
+  initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { $, $all, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    promise.all([
+      waitForNetworkEvents(aMonitor, 6),
+      waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+    ]).then(() => {
+      info("Checking the image thumbnail when all items are shown.");
+      checkImageThumbnail();
+
+      RequestsMenu.sortBy("size");
+      info("Checking the image thumbnail when all items are sorted.");
+      checkImageThumbnail();
+
+      RequestsMenu.filterOn("images");
+      info("Checking the image thumbnail when only images are shown.");
+      checkImageThumbnail();
+
+      info("Reloading the debuggee and performing all requests again...");
+      reloadAndPerformRequests();
+
+      return promise.all([
+        waitForNetworkEvents(aMonitor, 7), // 6 + 1
+        waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+      ]);
+    }).then(() => {
+      info("Checking the image thumbnail after a reload.");
+      checkImageThumbnail();
+
+      teardown(aMonitor).then(finish);
+    });
+
+    function reloadAndPerformRequests() {
+      NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED).then(() => {
+        aDebuggee.performRequests();
+      });
+    }
+
+    function checkImageThumbnail() {
+      is($all(".requests-menu-icon[type=thumbnail]").length, 1,
+        "There should be only one image request with a thumbnail displayed.");
+      is($(".requests-menu-icon[type=thumbnail]").src, TEST_IMAGE_DATA_URI,
+        "The image requests-menu-icon thumbnail is displayed correctly.");
+      is($(".requests-menu-icon[type=thumbnail]").hidden, false,
+        "The image requests-menu-icon thumbnail should not be hidden.");
+    }
+
+    aDebuggee.performRequests();
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/netmonitor/test/browser_net_image-tooltip.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if image responses show a popup in the requests menu when hovered.
+ */
+
+function test() {
+  initNetMonitor(CONTENT_TYPE_WITHOUT_CACHE_URL).then(([aTab, aDebuggee, aMonitor]) => {
+    info("Starting test... ");
+
+    let { $, EVENTS, ACTIVITY_TYPE, NetMonitorView, NetMonitorController } = aMonitor.panelWin;
+    let { RequestsMenu } = NetMonitorView;
+
+    promise.all([
+      waitForNetworkEvents(aMonitor, 6),
+      waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+    ]).then(() => {
+      info("Checking the image thumbnail after a few requests were made...");
+      let requestItem = RequestsMenu.items[5];
+      let requestTooltip = requestItem.attachment.tooltip;
+      ok(requestTooltip, "There should be a tooltip instance for the image request.");
+
+      let anchor = $(".requests-menu-file", requestItem.target);
+      return showTooltipOn(requestTooltip, anchor);
+    }).then(aTooltip => {
+      ok(true,
+        "An tooltip was successfully opened for the image request.");
+      is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
+        "The tooltip's image content is displayed correctly.");
+
+      info("Reloading the debuggee and performing all requests again...");
+      reloadAndPerformRequests();
+
+      return promise.all([
+        waitForNetworkEvents(aMonitor, 7), // 6 + 1
+        waitFor(aMonitor.panelWin, EVENTS.RESPONSE_IMAGE_THUMBNAIL_DISPLAYED)
+      ]);
+    }).then(() => {
+      info("Checking the image thumbnail after a reload.");
+      let requestItem = RequestsMenu.items[6];
+      let requestTooltip = requestItem.attachment.tooltip;
+      ok(requestTooltip, "There should be a tooltip instance for the image request.");
+
+      let anchor = $(".requests-menu-file", requestItem.target);
+      return showTooltipOn(requestTooltip, anchor);
+    }).then(aTooltip => {
+      ok(true,
+        "An tooltip was successfully opened for the image request.");
+      is(aTooltip.content.querySelector("image").src, TEST_IMAGE_DATA_URI,
+        "The tooltip's image content is displayed correctly.");
+
+      teardown(aMonitor).then(finish);
+    });
+
+    function reloadAndPerformRequests() {
+      NetMonitorController.triggerActivity(ACTIVITY_TYPE.RELOAD.WITH_CACHE_ENABLED).then(() => {
+        aDebuggee.performRequests();
+      });
+    }
+
+    function showTooltipOn(aTooltip, aTarget) {
+      let deferred = promise.defer();
+
+      aTooltip.panel.addEventListener("popupshown", function onEvent() {
+        aTooltip.panel.removeEventListener("popupshown", onEvent, true);
+        deferred.resolve(aTooltip);
+      }, true);
+
+      aTooltip._showOnHover(aTarget);
+      return deferred.promise;
+    }
+
+    aDebuggee.performRequests();
+  });
+}
--- a/browser/devtools/netmonitor/test/head.js
+++ b/browser/devtools/netmonitor/test/head.js
@@ -35,16 +35,17 @@ const CUSTOM_GET_URL = EXAMPLE_URL + "ht
 const STATISTICS_URL = EXAMPLE_URL + "html_statistics-test-page.html";
 
 const SIMPLE_SJS = EXAMPLE_URL + "sjs_simple-test-server.sjs";
 const CONTENT_TYPE_SJS = EXAMPLE_URL + "sjs_content-type-test-server.sjs";
 const STATUS_CODES_SJS = EXAMPLE_URL + "sjs_status-codes-test-server.sjs";
 const SORTING_SJS = EXAMPLE_URL + "sjs_sorting-test-server.sjs";
 
 const TEST_IMAGE = EXAMPLE_URL + "test-image.png";
+const TEST_IMAGE_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
 
 // All tests are asynchronous.
 waitForExplicitFinish();
 
 // Enable logging for all the relevant tests.
 const gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 
--- a/browser/devtools/shared/widgets/Tooltip.js
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -394,22 +394,22 @@ Tooltip.prototype = {
       setNamedTimeout(this.uid, this._showDelay, () => {
         this._showOnHover(event.target);
       });
     }
   },
 
   _showOnHover: function(target) {
     let res = this._targetNodeCb(target, this);
+    let show = arg => this.show(arg instanceof Ci.nsIDOMNode ? arg : target);
+
     if (res && res.then) {
-      res.then(() => {
-        this.show(target);
-      });
+      res.then(show);
     } else if (res) {
-      this.show(target);
+      show(res);
     }
   },
 
   _onBaseNodeMouseLeave: function() {
     clearNamedTimeout(this.uid);
     this._lastHovered = null;
     this.hide();
   },
--- a/browser/devtools/shared/widgets/ViewHelpers.jsm
+++ b/browser/devtools/shared/widgets/ViewHelpers.jsm
@@ -765,16 +765,23 @@ this.WidgetMethods = {
    * @param number aIndex
    *        The index of the item to bring into view.
    */
   ensureIndexIsVisible: function(aIndex) {
     this.ensureItemIsVisible(this.getItemAtIndex(aIndex));
   },
 
   /**
+   * Sugar for ensuring the selected item is visible in this container.
+   */
+  ensureSelectedItemIsVisible: function() {
+    this.ensureItemIsVisible(this.selectedItem);
+  },
+
+  /**
    * If supported by the widget, the label string temporarily added to this
    * container when there are no child items present.
    */
   set emptyText(aValue) {
     this._emptyText = aValue;
 
     // Apply the emptyText attribute right now if there are no child items.
     if (!this._itemsByElement.size) {
@@ -891,16 +898,19 @@ this.WidgetMethods = {
     this._insertItemAt.apply(this, i < j ? [j, aFirst] : [i, aSecond]);
 
     // 5. Restore the previous selection, if necessary.
     if (selectedIndex == i) {
       this._widget.selectedItem = aFirst._target;
     } else if (selectedIndex == j) {
       this._widget.selectedItem = aSecond._target;
     }
+
+    // 6. Let the outside world know that these two items were swapped.
+    ViewHelpers.dispatchEvent(aFirst.target, "swap", [aSecond, aFirst]);
   },
 
   /**
    * Visually swaps two items in this container at specific indices.
    *
    * @param number aFirst
    *        The index of the first item to be swapped.
    * @param number aSecond
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -609,17 +609,16 @@ you can use these alternative items. Oth
 
 <!ENTITY findOnCmd.label     "Find in This Page…">
 <!ENTITY findOnCmd.accesskey "F">
 <!ENTITY findOnCmd.commandkey "f">
 <!ENTITY findAgainCmd.label  "Find Again">
 <!ENTITY findAgainCmd.accesskey "g">
 <!ENTITY findAgainCmd.commandkey "g">
 <!ENTITY findAgainCmd.commandkey2 "VK_F3">
-<!ENTITY findSelectionCmd.commandkey "e">
 
 <!ENTITY spellAddDictionaries.label "Add Dictionaries…">
 <!ENTITY spellAddDictionaries.accesskey "A">
 
 <!ENTITY editBookmark.done.label                     "Done">
 <!ENTITY editBookmark.cancel.label                   "Cancel">
 <!ENTITY editBookmark.removeBookmark.accessKey       "R">
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1171,16 +1171,17 @@ toolbar .toolbarbutton-1:not([type="menu
     -moz-image-region: rect(0px, 160px, 32px, 128px);
   }
 
   #zoom-controls@inAnyPanel@ > #zoom-in-button,
   toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
     -moz-image-region: rect(0px, 192px, 32px, 160px);
   }
 
+  #PanelUI-fxa-status > .toolbarbutton-icon,
   #PanelUI-quit > .toolbarbutton-icon,
   #PanelUI-customize > .toolbarbutton-icon,
   #PanelUI-help > .toolbarbutton-icon {
     width: 16px;
   }
 }
 
 toolbar .toolbarbutton-1:not([type="menu-button"]),
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -78,16 +78,20 @@
   padding: 2px 12px;
   background-color: rgb(251,251,251);
   color: rgb(71,71,71);
   box-shadow: 0 1px rgba(255, 255, 255, 0.5),
               inset 0 1px rgba(255, 255, 255, 0.5);
   -moz-appearance: none;
 }
 
+.customizationmode-button[disabled="true"] {
+  opacity: .5;
+}
+
 #customization-titlebar-visibility-button {
   list-style-image: url("chrome://browser/skin/customizableui/customize-titleBar-toggle.png");
   -moz-image-region: rect(0, 24px, 24px, 0);
   padding: 2px 7px;
   -moz-margin-end: 10px;
 }
 
 #customization-titlebar-visibility-button > .button-box > .button-text {
@@ -99,17 +103,17 @@
   -moz-image-region: rect(0, 48px, 24px, 24px);
   background-color: rgb(218, 218, 218);
   border-color: rgb(168, 168, 168);
   text-shadow: 0 1px rgb(236, 236, 236);
   box-shadow: 0 1px rgba(255, 255, 255, 0.5),
               inset 0 1px rgb(196, 196, 196);
 }
 
-#customization-undo-reset {
+#customization-undo-reset-button {
   -moz-margin-end: 10px;
 }
 
 #main-window[customize-entered] #customization-panel-container {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-separatorHorizontal.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-separatorVertical.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -121,21 +121,41 @@
   height: 10px;
 }
 
 .requests-menu-method {
   text-align: center;
   font-weight: 600;
 }
 
-.requests-menu-file {
+.requests-menu-icon-and-file {
   width: 20vw;
   min-width: 4em;
 }
 
+.requests-menu-icon {
+  background: #fff;
+  width: calc(1em + 4px);
+  height: calc(1em + 4px);
+  margin: -4px 0px;
+  -moz-margin-end: 4px;
+}
+
+.theme-dark .requests-menu-icon {
+  outline: 1px solid @table_itemDarkStartBorder@;
+}
+
+.theme-light .requests-menu-icon {
+  outline: 1px solid @table_itemLightStartBorder@;
+}
+
+.requests-menu-file {
+  text-align: start;
+}
+
 .requests-menu-domain {
   width: 14vw;
   min-width: 10em;
 }
 
 .requests-menu-type {
   text-align: center;
   width: 4em;
@@ -283,18 +303,16 @@ box.requests-menu-status {
 
 /* Network requests table: waterfall items */
 
 .requests-menu-subitem.requests-menu-waterfall {
   -moz-padding-start: 0px;
   -moz-padding-end: 4px;
   background-repeat: repeat-y; /* Background created on a <canvas> in js. */
   background-position: -1px center;
-  margin-top: -1px; /* Compensate borders. */
-  margin-bottom: -1px;
 }
 
 .requests-menu-subitem.requests-menu-waterfall:-moz-locale-dir(rtl) {
   background-position: right center;
 }
 
 .requests-menu-timings:-moz-locale-dir(ltr) {
   transform-origin: left center;
@@ -766,17 +784,20 @@ box.requests-menu-status {
     margin: 0 !important;
     /* To prevent all the margin hacks to hide the sidebar. */
   }
 
   .requests-menu-status-and-method {
     width: 16vw;
   }
 
-  .requests-menu-file,
+  .requests-menu-icon-and-file {
+    width: 30vw;
+  }
+
   .requests-menu-domain {
     width: 30vw;
   }
 
   .requests-menu-type {
     width: 8vw;
   }
 
@@ -792,15 +813,15 @@ box.requests-menu-status {
 @media (min-width: 701px) {
   #network-table[type-overflows] .requests-menu-domain {
     border-width: 0 !important;
     box-shadow: none !important;
     /* The "Type" header is not visible anymore, and thus the
        right border and box-shadow of "Domain" column should be hidden. */
   }
 
-  #network-table[domain-overflows] .requests-menu-file {
+  #network-table[domain-overflows] .requests-menu-icon-and-file {
     border-width: 0 !important;
     box-shadow: none !important;
     /* The "Domain" header is not visible anymore, and thus the
        right border and box-shadow of "File" column should be hidden. */
   }
 }
--- a/build/docs/test_manifests.rst
+++ b/build/docs/test_manifests.rst
@@ -171,17 +171,23 @@ expression syntax until it is documented
 
 File Installation
 -----------------
 
 Files referenced by manifests are automatically installed into the object
 directory into paths defined in
 :py:func:`mozbuild.frontend.emitter.TreeMetadataEmitter._process_test_manifest`.
 
-Referenced files in the manifest not in the same directory tree as the manifest
-file are **not** installed.
+Relative paths resolving to parent directory (e.g.
+``support-files = ../foo.txt`` have special behavior.
+
+For ``support-files``, the file will be installed to the default destination
+for that manifest. Only the file's base name is used to construct the final
+path: directories are irrelevant.
+
+For all other entry types, the file installation is skipped.
 
 .. _reftest_manifests:
 
 Reftest Manifests
 =================
 
 See `MDN <https://developer.mozilla.org/en-US/docs/Creating_reftest-based_unit_tests>`_.
--- a/mobile/android/base/ActivityHandlerHelper.java
+++ b/mobile/android/base/ActivityHandlerHelper.java
@@ -1,283 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.prompts.PromptService;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.ActivityResultHandlerMap;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
-
-import org.json.JSONException;
-import org.json.JSONObject;
 
 import android.app.Activity;
-import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.os.Environment;
-import android.provider.MediaStore;
-import android.util.Log;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
-
-public class ActivityHandlerHelper implements GeckoEventListener {
-    private static final String LOGTAG = "GeckoActivityHandlerHelper";
-
-    private final ConcurrentLinkedQueue<String> mFilePickerResult;
-
-    private final ActivityResultHandlerMap mActivityResultHandlerMap;
-    private final FilePickerResultHandlerSync mFilePickerResultHandlerSync;
-    private final CameraImageResultHandler mCameraImageResultHandler;
-    private final CameraVideoResultHandler mCameraVideoResultHandler;
-
-    public interface FileResultHandler {
-        public void gotFile(String filename);
-    }
 
-    @SuppressWarnings("serial")
-    public ActivityHandlerHelper() {
-        mFilePickerResult = new ConcurrentLinkedQueue<String>() {
-            @Override public boolean offer(String e) {
-                if (super.offer(e)) {
-                    // poke the Gecko thread in case it's waiting for new events
-                    GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
-                    return true;
-                }
-                return false;
-            }
-        };
-        mActivityResultHandlerMap = new ActivityResultHandlerMap();
-        mFilePickerResultHandlerSync = new FilePickerResultHandlerSync(mFilePickerResult);
-        mCameraImageResultHandler = new CameraImageResultHandler(mFilePickerResult);
-        mCameraVideoResultHandler = new CameraVideoResultHandler(mFilePickerResult);
-        GeckoAppShell.getEventDispatcher().registerEventListener("FilePicker:Show", this);
-    }
+public class ActivityHandlerHelper {
+    private static final String LOGTAG = "GeckoActivityHandlerHelper";
+    private static final ActivityResultHandlerMap mActivityResultHandlerMap = new ActivityResultHandlerMap();
 
-    @Override
-    public void handleMessage(String event, final JSONObject message) {
-        if (event.equals("FilePicker:Show")) {
-            String mimeType = "*/*";
-            String mode = message.optString("mode");
-
-            if ("mimeType".equals(mode))
-                mimeType = message.optString("mimeType");
-            else if ("extension".equals(mode))
-                mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
-
-            Log.i(LOGTAG, "Mime: " + mimeType);
-
-            showFilePickerAsync(GeckoAppShell.getGeckoInterface().getActivity(), mimeType, new FileResultHandler() {
-                public void gotFile(String filename) {
-                    try {
-                        message.put("file", filename);
-                    } catch (JSONException ex) {
-                        Log.i(LOGTAG, "Can't add filename to message " + filename);
-                    }
-                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
-                        "FilePicker:Result", message.toString()));
-                }
-            });
-        }
-    }
-
-    public int makeRequestCode(ActivityResultHandler aHandler) {
+    private static int makeRequestCode(ActivityResultHandler aHandler) {
         return mActivityResultHandlerMap.put(aHandler);
     }
 
-    public void startIntentForActivity (Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
+    public static void startIntent(Intent intent, ActivityResultHandler activityResultHandler) {
+        startIntentForActivity(GeckoAppShell.getGeckoInterface().getActivity(), intent, activityResultHandler);
+    }
+
+    public static void startIntentForActivity(Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
         activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
     }
 
-    private int addIntentActivitiesToList(Context context, Intent intent, ArrayList<Prompt.PromptListItem> items, ArrayList<Intent> aIntents) {
-        PackageManager pm = context.getPackageManager();
-        List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
 
-        if (lri == null) {
-            return 0;
-        }
-
-        for (ResolveInfo ri : lri) {
-            Intent rintent = new Intent(intent);
-            rintent.setComponent(new ComponentName(
-                    ri.activityInfo.applicationInfo.packageName,
-                    ri.activityInfo.name));
-
-            Prompt.PromptListItem item = new Prompt.PromptListItem(ri.loadLabel(pm).toString());
-            item.icon = ri.loadIcon(pm);
-            items.add(item);
-            aIntents.add(rintent);
-        }
-
-        return lri.size();
-    }
-
-    private int addFilePickingActivities(Context context, ArrayList<Prompt.PromptListItem> aItems, String aType, ArrayList<Intent> aIntents) {
-        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.setType(aType);
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
-
-        return addIntentActivitiesToList(context, intent, aItems, aIntents);
-    }
-
-    private Prompt.PromptListItem[] getItemsAndIntentsForFilePicker(Context context, String aMimeType, ArrayList<Intent> aIntents) {
-        ArrayList<Prompt.PromptListItem> items = new ArrayList<Prompt.PromptListItem>();
-
-        if (aMimeType.equals("audio/*")) {
-            if (addFilePickingActivities(context, items, "audio/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else if (aMimeType.equals("image/*")) {
-            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-            intent.putExtra(MediaStore.EXTRA_OUTPUT,
-                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  CameraImageResultHandler.generateImageName())));
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            if (addFilePickingActivities(context, items, "image/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else if (aMimeType.equals("video/*")) {
-            Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            if (addFilePickingActivities(context, items, "video/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else {
-            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-            intent.putExtra(MediaStore.EXTRA_OUTPUT,
-                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  CameraImageResultHandler.generateImageName())));
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            addFilePickingActivities(context, items, "*/*", aIntents);
-        }
-
-        return items.toArray(new Prompt.PromptListItem[] {});
-    }
-
-    private String getFilePickerTitle(Context context, String aMimeType) {
-        if (aMimeType.equals("audio/*")) {
-            return context.getString(R.string.filepicker_audio_title);
-        } else if (aMimeType.equals("image/*")) {
-            return context.getString(R.string.filepicker_image_title);
-        } else if (aMimeType.equals("video/*")) {
-            return context.getString(R.string.filepicker_video_title);
-        } else {
-            return context.getString(R.string.filepicker_title);
-        }
-    }
-
-    private interface IntentHandler {
-        public void gotIntent(Intent intent);
-    }
-
-    /* Gets an intent that can open a particular mimetype. Will show a prompt with a list
-     * of Activities that can handle the mietype. Asynchronously calls the handler when
-     * one of the intents is selected. If the caller passes in null for the handler, will still
-     * prompt for the activity, but will throw away the result.
-     */
-    private void getFilePickerIntentAsync(final Context context, String aMimeType, final IntentHandler handler) {
-        final ArrayList<Intent> intents = new ArrayList<Intent>();
-        final Prompt.PromptListItem[] items =
-            getItemsAndIntentsForFilePicker(context, aMimeType, intents);
-
-        if (intents.size() == 0) {
-            Log.i(LOGTAG, "no activities for the file picker!");
-            handler.gotIntent(null);
-            return;
-        }
-
-        if (intents.size() == 1) {
-            handler.gotIntent(intents.get(0));
-            return;
-        }
-
-        final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
-            public void onPromptFinished(String promptServiceResult) {
-                if (handler == null) {
-                    return;
-                }
-
-                int itemId = -1;
-                try {
-                    itemId = new JSONObject(promptServiceResult).getInt("button");
-                } catch (JSONException e) {
-                    Log.e(LOGTAG, "result from promptservice was invalid: ", e);
-                }
-
-                if (itemId == -1) {
-                    handler.gotIntent(null);
-                } else {
-                    handler.gotIntent(intents.get(itemId));
-                }
-            }
-        });
-
-        final String title = getFilePickerTitle(context, aMimeType);
-        // Runnable has to be called to show an intent-like
-        // context menu UI using the PromptService.
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override public void run() {
-                prompt.show(title, "", items, false);
-            }
-        });
-    }
-
-    /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
-     * sends the file returned to the passed in handler. If a null handler is passed in, will still
-     * pick and launch the file picker, but will throw away the result.
-     */
-    public void showFilePickerAsync(final Activity parentActivity, String aMimeType, final FileResultHandler handler) {
-        getFilePickerIntentAsync(parentActivity, aMimeType, new IntentHandler() {
-            public void gotIntent(Intent intent) {
-                if (handler == null) {
-                    return;
-                }
-
-                if (intent == null) {
-                    handler.gotFile("");
-                    return;
-                }
-
-                if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intent.getAction())) {
-                    CameraImageResultHandler cam = new CameraImageResultHandler(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(cam));
-                } else if (MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction())) {
-                    CameraVideoResultHandler vid = new CameraVideoResultHandler(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(vid));
-                } else if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
-                    FilePickerResultHandlerSync file = new FilePickerResultHandlerSync(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(file));
-                } else {
-                    Log.e(LOGTAG, "We should not get an intent with another action!");
-                    handler.gotFile("");
-                    return;
-                }
-            }
-        });
-    }
-
-    boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+    public static boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
         ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
         if (handler != null) {
             handler.onActivityResult(resultCode, data);
             return true;
         }
         return false;
     }
 }
deleted file mode 100644
--- a/mobile/android/base/CameraImageResultHandler.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.ActivityResultHandler;
-
-import android.app.Activity;
-import android.content.Intent;
-import android.os.Environment;
-import android.text.format.Time;
-import android.util.Log;
-
-import java.io.File;
-import java.util.Queue;
-
-class CameraImageResultHandler implements ActivityResultHandler {
-    private static final String LOGTAG = "GeckoCameraImageResultHandler";
-
-    private final Queue<String> mFilePickerResult;
-    private final ActivityHandlerHelper.FileResultHandler mHandler;
-
-    CameraImageResultHandler(Queue<String> resultQueue) {
-        mFilePickerResult = resultQueue;
-        mHandler = null;
-    }
-
-    /* Use this constructor to asynchronously listen for results */
-    public CameraImageResultHandler(ActivityHandlerHelper.FileResultHandler handler) {
-        mHandler = handler;
-        mFilePickerResult = null;
-    }
-
-    @Override
-    public void onActivityResult(int resultCode, Intent data) {
-        if (resultCode != Activity.RESULT_OK) {
-            if (mFilePickerResult != null) {
-                mFilePickerResult.offer("");
-            }
-            return;
-        }
-
-        File file = new File(Environment.getExternalStorageDirectory(), sImageName);
-        sImageName = "";
-
-        if (mFilePickerResult != null) {
-            mFilePickerResult.offer(file.getAbsolutePath());
-        }
-
-        if (mHandler != null) {
-            mHandler.gotFile(file.getAbsolutePath());
-        }
-    }
-
-    // this code is really hacky and doesn't belong anywhere so I'm putting it here for now
-    // until I can come up with a better solution.
-
-    private static String sImageName = "";
-
-    static String generateImageName() {
-        Time now = new Time();
-        now.setToNow();
-        sImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
-        return sImageName;
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/CameraVideoResultHandler.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.util.ActivityResultHandler;
-
-import android.app.Activity;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.content.Intent;
-import android.database.Cursor;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.util.Log;
-
-import java.util.Queue;
-
-class CameraVideoResultHandler implements ActivityResultHandler {
-    private static final String LOGTAG = "GeckoCameraVideoResultHandler";
-
-    private final Queue<String> mFilePickerResult;
-    private final ActivityHandlerHelper.FileResultHandler mHandler;
-
-    CameraVideoResultHandler(Queue<String> resultQueue) {
-        mFilePickerResult = resultQueue;
-        mHandler = null;
-    }
-
-    /* Use this constructor to asynchronously listen for results */
-    public CameraVideoResultHandler(ActivityHandlerHelper.FileResultHandler handler) {
-        mFilePickerResult = null;
-        mHandler = handler;
-    }
-
-    private void sendResult(String res) {
-        if (mFilePickerResult != null)
-            mFilePickerResult.offer(res);
-
-        if (mHandler != null)
-            mHandler.gotFile(res);
-    }
-
-    @Override
-    public void onActivityResult(int resultCode, final Intent data) {
-        // Intent.getData() can return null. Avoid a crash. See bug 904551.
-        if (data == null || data.getData() == null || resultCode != Activity.RESULT_OK) {
-            sendResult("");
-            return;
-        }
-
-        final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
-        final LoaderManager lm = fa.getSupportLoaderManager();
-        lm.initLoader(data.hashCode(), null, new LoaderCallbacks<Cursor>() {
-            @Override
-            public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-                return new CursorLoader(fa,
-                                        data.getData(),
-                                        new String[] { MediaStore.Video.Media.DATA },
-                                        null,  // selection
-                                        null,  // selectionArgs
-                                        null); // sortOrder
-            }
-
-            @Override
-            public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-                if (cursor.moveToFirst()) {
-                    sendResult(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)));
-                } else {
-                    sendResult("");
-                }
-            }
-
-            @Override
-            public void onLoaderReset(Loader<Cursor> loader) { }
-        });
-    }
-}
copy from mobile/android/base/ActivityHandlerHelper.java
copy to mobile/android/base/FilePicker.java
--- a/mobile/android/base/ActivityHandlerHelper.java
+++ b/mobile/android/base/FilePicker.java
@@ -1,283 +1,222 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.prompts.PromptService;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.util.ActivityResultHandlerMap;
+import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Environment;
+import android.os.Parcelable;
 import android.provider.MediaStore;
 import android.util.Log;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
-
-public class ActivityHandlerHelper implements GeckoEventListener {
-    private static final String LOGTAG = "GeckoActivityHandlerHelper";
 
-    private final ConcurrentLinkedQueue<String> mFilePickerResult;
+public class FilePicker implements GeckoEventListener {
+    private static final String LOGTAG = "GeckoFilePicker";
+    private static FilePicker sFilePicker;
+    private final Context mContext;
 
-    private final ActivityResultHandlerMap mActivityResultHandlerMap;
-    private final FilePickerResultHandlerSync mFilePickerResultHandlerSync;
-    private final CameraImageResultHandler mCameraImageResultHandler;
-    private final CameraVideoResultHandler mCameraVideoResultHandler;
-
-    public interface FileResultHandler {
+    public interface ResultHandler {
         public void gotFile(String filename);
     }
 
-    @SuppressWarnings("serial")
-    public ActivityHandlerHelper() {
-        mFilePickerResult = new ConcurrentLinkedQueue<String>() {
-            @Override public boolean offer(String e) {
-                if (super.offer(e)) {
-                    // poke the Gecko thread in case it's waiting for new events
-                    GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
-                    return true;
-                }
-                return false;
-            }
-        };
-        mActivityResultHandlerMap = new ActivityResultHandlerMap();
-        mFilePickerResultHandlerSync = new FilePickerResultHandlerSync(mFilePickerResult);
-        mCameraImageResultHandler = new CameraImageResultHandler(mFilePickerResult);
-        mCameraVideoResultHandler = new CameraVideoResultHandler(mFilePickerResult);
+    public static void init(Context context) {
+        if (sFilePicker == null) {
+            sFilePicker = new FilePicker(context);
+        }
+    }
+
+    protected FilePicker(Context context) {
+        mContext = context;
         GeckoAppShell.getEventDispatcher().registerEventListener("FilePicker:Show", this);
     }
 
     @Override
     public void handleMessage(String event, final JSONObject message) {
         if (event.equals("FilePicker:Show")) {
             String mimeType = "*/*";
             String mode = message.optString("mode");
 
             if ("mimeType".equals(mode))
                 mimeType = message.optString("mimeType");
             else if ("extension".equals(mode))
                 mimeType = GeckoAppShell.getMimeTypeFromExtensions(message.optString("extensions"));
 
-            Log.i(LOGTAG, "Mime: " + mimeType);
-
-            showFilePickerAsync(GeckoAppShell.getGeckoInterface().getActivity(), mimeType, new FileResultHandler() {
+            showFilePickerAsync(mimeType, new ResultHandler() {
                 public void gotFile(String filename) {
                     try {
                         message.put("file", filename);
                     } catch (JSONException ex) {
                         Log.i(LOGTAG, "Can't add filename to message " + filename);
                     }
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
                         "FilePicker:Result", message.toString()));
                 }
             });
         }
     }
 
-    public int makeRequestCode(ActivityResultHandler aHandler) {
-        return mActivityResultHandlerMap.put(aHandler);
+    private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
+        PackageManager pm = mContext.getPackageManager();
+        List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
+        for (ResolveInfo ri : lri) {
+            ComponentName cn = new ComponentName(ri.activityInfo.applicationInfo.packageName, ri.activityInfo.name);
+            if (filters != null && !filters.containsKey(cn.toString())) {
+                Intent rintent = new Intent(intent);
+                rintent.setComponent(cn);
+                intents.put(cn.toString(), rintent);
+            }
+        }
     }
 
-    public void startIntentForActivity (Activity activity, Intent intent, ActivityResultHandler activityResultHandler) {
-        activity.startActivityForResult(intent, mActivityResultHandlerMap.put(activityResultHandler));
+    private Intent getIntent(String mimeType) {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType(mimeType);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        return intent;
     }
 
-    private int addIntentActivitiesToList(Context context, Intent intent, ArrayList<Prompt.PromptListItem> items, ArrayList<Intent> aIntents) {
-        PackageManager pm = context.getPackageManager();
-        List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
-
-        if (lri == null) {
-            return 0;
-        }
-
-        for (ResolveInfo ri : lri) {
-            Intent rintent = new Intent(intent);
-            rintent.setComponent(new ComponentName(
-                    ri.activityInfo.applicationInfo.packageName,
-                    ri.activityInfo.name));
-
-            Prompt.PromptListItem item = new Prompt.PromptListItem(ri.loadLabel(pm).toString());
-            item.icon = ri.loadIcon(pm);
-            items.add(item);
-            aIntents.add(rintent);
-        }
+    private List<Intent> getIntentsForFilePicker(final String mimeType,
+                                                       final FilePickerResultHandler fileHandler) {
+        // The base intent to use for the file picker. Even if this is an implicit intent, Android will
+        // still show a list of Activitiees that match this action/type.
+        Intent baseIntent;
+        // A HashMap of Activities the base intent will show in the chooser. This is used
+        // to filter activities from other intents so that we don't show duplicates.
+        HashMap<String, Intent> baseIntents = new HashMap<String, Intent>();
+        // A list of other activities to shwo in the picker (and the intents to launch them).
+        HashMap<String, Intent> intents = new HashMap<String, Intent> ();
 
-        return lri.size();
-    }
-
-    private int addFilePickingActivities(Context context, ArrayList<Prompt.PromptListItem> aItems, String aType, ArrayList<Intent> aIntents) {
-        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.setType(aType);
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        if ("audio/*".equals(mimeType)) {
+            // For audio the only intent is the mimetype
+            baseIntent = getIntent(mimeType);
+            addActivities(baseIntent, baseIntents, null);
+        } else if ("image/*".equals(mimeType)) {
+            // For images the base is a capture intent
+            baseIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+            baseIntent.putExtra(MediaStore.EXTRA_OUTPUT,
+                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
+                                                  fileHandler.generateImageName())));
+            addActivities(baseIntent, baseIntents, null);
 
-        return addIntentActivitiesToList(context, intent, aItems, aIntents);
-    }
-
-    private Prompt.PromptListItem[] getItemsAndIntentsForFilePicker(Context context, String aMimeType, ArrayList<Intent> aIntents) {
-        ArrayList<Prompt.PromptListItem> items = new ArrayList<Prompt.PromptListItem>();
+            // We also add the mimetype intent
+            addActivities(getIntent(mimeType), intents, baseIntents);
+        } else if ("video/*".equals(mimeType)) {
+            // For videos the base is a capture intent
+            baseIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+            addActivities(baseIntent, baseIntents, null);
 
-        if (aMimeType.equals("audio/*")) {
-            if (addFilePickingActivities(context, items, "audio/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else if (aMimeType.equals("image/*")) {
+            // We also add the mimetype intent
+            addActivities(getIntent(mimeType), intents, baseIntents);
+        } else {
+            baseIntent = getIntent("*/*");
+            addActivities(baseIntent, baseIntents, null);
+
             Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
             intent.putExtra(MediaStore.EXTRA_OUTPUT,
                             Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  CameraImageResultHandler.generateImageName())));
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            if (addFilePickingActivities(context, items, "image/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else if (aMimeType.equals("video/*")) {
-            Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-            addIntentActivitiesToList(context, intent, items, aIntents);
+                                                  fileHandler.generateImageName())));
+            addActivities(intent, intents, baseIntents);
+            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+            addActivities(intent, intents, baseIntents);
+        }
 
-            if (addFilePickingActivities(context, items, "video/*", aIntents) <= 0) {
-                addFilePickingActivities(context, items, "*/*", aIntents);
-            }
-        } else {
-            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
-            intent.putExtra(MediaStore.EXTRA_OUTPUT,
-                            Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
-                                                  CameraImageResultHandler.generateImageName())));
-            addIntentActivitiesToList(context, intent, items, aIntents);
+        // If we didn't find any activities, we fall back to the */* mimetype intent
+        if (baseIntents.size() == 0 && intents.size() == 0) {
+            intents.clear();
 
-            intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-            addIntentActivitiesToList(context, intent, items, aIntents);
-
-            addFilePickingActivities(context, items, "*/*", aIntents);
+            baseIntent = getIntent("*/*");
+            addActivities(baseIntent, baseIntents, null);
         }
 
-        return items.toArray(new Prompt.PromptListItem[] {});
+        ArrayList<Intent> vals = new ArrayList<Intent>(intents.values());
+        vals.add(0, baseIntent);
+        return vals;
     }
 
-    private String getFilePickerTitle(Context context, String aMimeType) {
-        if (aMimeType.equals("audio/*")) {
-            return context.getString(R.string.filepicker_audio_title);
-        } else if (aMimeType.equals("image/*")) {
-            return context.getString(R.string.filepicker_image_title);
-        } else if (aMimeType.equals("video/*")) {
-            return context.getString(R.string.filepicker_video_title);
+    private String getFilePickerTitle(String mimeType) {
+        if (mimeType.equals("audio/*")) {
+            return mContext.getString(R.string.filepicker_audio_title);
+        } else if (mimeType.equals("image/*")) {
+            return mContext.getString(R.string.filepicker_image_title);
+        } else if (mimeType.equals("video/*")) {
+            return mContext.getString(R.string.filepicker_video_title);
         } else {
-            return context.getString(R.string.filepicker_title);
+            return mContext.getString(R.string.filepicker_title);
         }
     }
 
     private interface IntentHandler {
         public void gotIntent(Intent intent);
     }
 
     /* Gets an intent that can open a particular mimetype. Will show a prompt with a list
      * of Activities that can handle the mietype. Asynchronously calls the handler when
      * one of the intents is selected. If the caller passes in null for the handler, will still
      * prompt for the activity, but will throw away the result.
      */
-    private void getFilePickerIntentAsync(final Context context, String aMimeType, final IntentHandler handler) {
-        final ArrayList<Intent> intents = new ArrayList<Intent>();
-        final Prompt.PromptListItem[] items =
-            getItemsAndIntentsForFilePicker(context, aMimeType, intents);
+    private void getFilePickerIntentAsync(final String mimeType,
+                                          final FilePickerResultHandler fileHandler,
+                                          final IntentHandler handler) {
+        List<Intent> intents = getIntentsForFilePicker(mimeType, fileHandler);
 
         if (intents.size() == 0) {
             Log.i(LOGTAG, "no activities for the file picker!");
             handler.gotIntent(null);
             return;
         }
 
-        if (intents.size() == 1) {
-            handler.gotIntent(intents.get(0));
+        Intent base = intents.remove(0);
+
+        if (intents.size() == 0) {
+            handler.gotIntent(base);
             return;
         }
 
-        final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
-            public void onPromptFinished(String promptServiceResult) {
-                if (handler == null) {
-                    return;
-                }
-
-                int itemId = -1;
-                try {
-                    itemId = new JSONObject(promptServiceResult).getInt("button");
-                } catch (JSONException e) {
-                    Log.e(LOGTAG, "result from promptservice was invalid: ", e);
-                }
-
-                if (itemId == -1) {
-                    handler.gotIntent(null);
-                } else {
-                    handler.gotIntent(intents.get(itemId));
-                }
-            }
-        });
-
-        final String title = getFilePickerTitle(context, aMimeType);
-        // Runnable has to be called to show an intent-like
-        // context menu UI using the PromptService.
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override public void run() {
-                prompt.show(title, "", items, false);
-            }
-        });
+        Intent chooser = Intent.createChooser(base, getFilePickerTitle(mimeType));
+        chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(new Parcelable[]{}));
+        handler.gotIntent(chooser);
     }
 
     /* Allows the user to pick an activity to load files from using a list prompt. Then opens the activity and
      * sends the file returned to the passed in handler. If a null handler is passed in, will still
      * pick and launch the file picker, but will throw away the result.
      */
-    public void showFilePickerAsync(final Activity parentActivity, String aMimeType, final FileResultHandler handler) {
-        getFilePickerIntentAsync(parentActivity, aMimeType, new IntentHandler() {
+    protected void showFilePickerAsync(String mimeType, final ResultHandler handler) {
+        final FilePickerResultHandler fileHandler = new FilePickerResultHandler(handler);
+        getFilePickerIntentAsync(mimeType, fileHandler, new IntentHandler() {
+            @Override
             public void gotIntent(Intent intent) {
                 if (handler == null) {
                     return;
                 }
 
                 if (intent == null) {
                     handler.gotFile("");
                     return;
                 }
 
-                if (MediaStore.ACTION_IMAGE_CAPTURE.equals(intent.getAction())) {
-                    CameraImageResultHandler cam = new CameraImageResultHandler(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(cam));
-                } else if (MediaStore.ACTION_VIDEO_CAPTURE.equals(intent.getAction())) {
-                    CameraVideoResultHandler vid = new CameraVideoResultHandler(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(vid));
-                } else if (Intent.ACTION_GET_CONTENT.equals(intent.getAction())) {
-                    FilePickerResultHandlerSync file = new FilePickerResultHandlerSync(handler);
-                    parentActivity.startActivityForResult(intent, mActivityResultHandlerMap.put(file));
-                } else {
-                    Log.e(LOGTAG, "We should not get an intent with another action!");
-                    handler.gotFile("");
-                    return;
-                }
+                ActivityHandlerHelper.startIntent(intent, fileHandler);
             }
         });
     }
-
-    boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
-        ActivityResultHandler handler = mActivityResultHandlerMap.getAndRemove(requestCode);
-        if (handler != null) {
-            handler.onActivityResult(resultCode, data);
-            return true;
-        }
-        return false;
-    }
 }
--- a/mobile/android/base/FilePickerResultHandler.java
+++ b/mobile/android/base/FilePickerResultHandler.java
@@ -7,82 +7,206 @@ package org.mozilla.gecko;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.util.ActivityResultHandler;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.MediaStore;
 import android.provider.OpenableColumns;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.text.format.Time;
 import android.util.Log;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.io.IOException;
 import java.util.Queue;
 
-abstract class FilePickerResultHandler implements ActivityResultHandler {
+class FilePickerResultHandler implements ActivityResultHandler {
     private static final String LOGTAG = "GeckoFilePickerResultHandler";
 
     protected final Queue<String> mFilePickerResult;
-    protected final ActivityHandlerHelper.FileResultHandler mHandler;
+    protected final FilePicker.ResultHandler mHandler;
+
+    // this code is really hacky and doesn't belong anywhere so I'm putting it here for now
+    // until I can come up with a better solution.
+    private String mImageName = "";
 
-    protected FilePickerResultHandler(Queue<String> resultQueue, ActivityHandlerHelper.FileResultHandler handler) {
+    public FilePickerResultHandler(Queue<String> resultQueue) {
         mFilePickerResult = resultQueue;
+        mHandler = null;
+    }
+
+    /* Use this constructor to asynchronously listen for results */
+    public FilePickerResultHandler(FilePicker.ResultHandler handler) {
+        mFilePickerResult = null;
         mHandler = handler;
     }
 
-    protected String handleActivityResult(int resultCode, Intent data) {
-        if (data == null || resultCode != Activity.RESULT_OK)
-            return "";
-        Uri uri = data.getData();
-        if (uri == null)
-            return "";
+    private void sendResult(String res) {
+        if (mFilePickerResult != null)
+            mFilePickerResult.offer(res);
+
+        if (mHandler != null)
+            mHandler.gotFile(res);
+    }
+
+    @Override
+    public void onActivityResult(int resultCode, Intent intent) {
+        if (resultCode != Activity.RESULT_OK) {
+            sendResult("");
+            return;
+        }
+
+        // Camera results won't return an Intent. Use the file name we passed to the original intent.
+        if (intent == null) {
+            if (mImageName != null) {
+                File file = new File(Environment.getExternalStorageDirectory(), mImageName);
+                sendResult(file.getAbsolutePath());
+            } else {
+                sendResult("");
+            }
+            return;
+        }
+
+        Uri uri = intent.getData();
+        if (uri == null) {
+            sendResult("");
+            return;
+        }
+
+        // Some file pickers may return a file uri
         if ("file".equals(uri.getScheme())) {
             String path = uri.getPath();
-            return path == null ? "" : path;
+            sendResult(path == null ? "" : path);
+            return;
         }
+
+        final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
+        final LoaderManager lm = fa.getSupportLoaderManager();
+        // Finally, Video pickers and some file pickers may return a content provider.
         try {
-            ContentResolver cr = GeckoAppShell.getContext().getContentResolver();
-            Cursor cursor = cr.query(uri, new String[] { OpenableColumns.DISPLAY_NAME },
-                                     null, null, null);
-            String name = null;
-            if (cursor != null) {
-                try {
-                    if (cursor.moveToNext()) {
-                        name = cursor.getString(0);
-                    }
-                } finally {
-                    cursor.close();
-                }
+            // Try a query to make sure the expected columns exist
+            final ContentResolver cr = fa.getContentResolver();
+            Cursor cursor = cr.query(uri, new String[] { "MediaStore.Video.Media.DATA" }, null, null, null);
+            cursor.close();
+
+            lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri));
+            return;
+        } catch(Exception ex) { }
+
+        lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri));
+        return;
+    }
+
+    public String generateImageName() {
+        Time now = new Time();
+        now.setToNow();
+        mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg";
+        return mImageName;
+    }
+
+    private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> {
+        final private Uri mUri;
+        public VideoLoaderCallbacks(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
+            return new CursorLoader(fa,
+                                    mUri,
+                                    new String[] { "MediaStore.Video.Media.DATA" },
+                                    null,  // selection
+                                    null,  // selectionArgs
+                                    null); // sortOrder
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+            if (cursor.moveToFirst()) {
+                String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));
+                sendResult(res);
             }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) { }
+    }
 
-            // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
-            String fileName = "tmp_";
-            String fileExt = null;
-            int period;
-            if (name == null || (period = name.lastIndexOf('.')) == -1) {
-                String mimeType = cr.getType(uri);
-                fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
+    private class FileLoaderCallbacks implements LoaderCallbacks<Cursor> {
+        final private Uri mUri;
+        public FileLoaderCallbacks(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
+            return new CursorLoader(fa,
+                                    mUri,
+                                    new String[] { OpenableColumns.DISPLAY_NAME },
+                                    null,  // selection
+                                    null,  // selectionArgs
+                                    null); // sortOrder
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+            if (cursor.moveToFirst()) {
+                String name = cursor.getString(0);
+                // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens
+                String fileName = "tmp_";
+                String fileExt = null;
+                int period;
+
+                final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity();
+                final ContentResolver cr = fa.getContentResolver();
+
+                // Generate an extension if we don't already have one
+                if (name == null || (period = name.lastIndexOf('.')) == -1) {
+                    String mimeType = cr.getType(mUri);
+                    fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType);
+                } else {
+                    fileExt = name.substring(period);
+                    fileName += name.substring(0, period);
+                }
+
+                // Now write the data to the temp file
+                try {
+                    File file = File.createTempFile(fileName, fileExt, GeckoLoader.getGREDir(GeckoAppShell.getContext()));
+                    FileOutputStream fos = new FileOutputStream(file);
+                    InputStream is = cr.openInputStream(mUri);
+                    byte[] buf = new byte[4096];
+                    int len = is.read(buf);
+                    while (len != -1) {
+                        fos.write(buf, 0, len);
+                        len = is.read(buf);
+                    }
+                    fos.close();
+
+                    String path = file.getAbsolutePath();
+                    sendResult((path == null) ? "" : path);
+                } catch(IOException ex) {
+                    Log.i(LOGTAG, "Error writing file", ex);
+                }
             } else {
-                fileExt = name.substring(period);
-                fileName += name.substring(0, period);
+                sendResult("");
             }
-            Log.i(LOGTAG, "Filename: " + fileName + " . " + fileExt);
-            File file = File.createTempFile(fileName, fileExt, GeckoLoader.getGREDir(GeckoAppShell.getContext()));
-            FileOutputStream fos = new FileOutputStream(file);
-            InputStream is = cr.openInputStream(uri);
-            byte[] buf = new byte[4096];
-            int len = is.read(buf);
-            while (len != -1) {
-                fos.write(buf, 0, len);
-                len = is.read(buf);
-            }
-            fos.close();
-            String path = file.getAbsolutePath();
-            return path == null ? "" : path;
-        } catch (Exception e) {
-            Log.e(LOGTAG, "showing file picker", e);
         }
-        return "";
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) { }
     }
+
 }
+
deleted file mode 100644
--- a/mobile/android/base/FilePickerResultHandlerSync.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.Intent;
-import android.util.Log;
-
-import java.util.Queue;
-
-class FilePickerResultHandlerSync extends FilePickerResultHandler {
-    private static final String LOGTAG = "GeckoFilePickerResultHandlerSync";
-
-    FilePickerResultHandlerSync(Queue<String> resultQueue) {
-        super(resultQueue, null);
-    }
-
-    /* Use this constructor to asynchronously listen for results */
-    public FilePickerResultHandlerSync(ActivityHandlerHelper.FileResultHandler handler) {
-        super(null, handler);
-    }
-
-    @Override
-    public void onActivityResult(int resultCode, Intent data) {
-        if (mFilePickerResult != null)
-            mFilePickerResult.offer(handleActivityResult(resultCode, data));
-
-        if (mHandler != null)
-            mHandler.gotFile(handleActivityResult(resultCode, data));
-    }
-}
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -974,24 +974,24 @@ public abstract class GeckoApp
             }
             if (image != null) {
                 String path = Media.insertImage(getContentResolver(),image, null, null);
                 final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
                 intent.addCategory(Intent.CATEGORY_DEFAULT);
                 intent.setData(Uri.parse(path));
 
                 // Removes the image from storage once the chooser activity ends.
-                GeckoAppShell.sActivityHelper.startIntentForActivity(this,
-                                                                    Intent.createChooser(intent, sAppContext.getString(R.string.set_image_chooser_title)),
-                                                                    new ActivityResultHandler() {
-                                                                        @Override
-                                                                        public void onActivityResult (int resultCode, Intent data) {
-                                                                            getContentResolver().delete(intent.getData(), null, null);
-                                                                        }
-                                                                    });
+                ActivityHandlerHelper.startIntentForActivity(this,
+                                                            Intent.createChooser(intent, sAppContext.getString(R.string.set_image_chooser_title)),
+                                                            new ActivityResultHandler() {
+                                                                @Override
+                                                                public void onActivityResult (int resultCode, Intent data) {
+                                                                    getContentResolver().delete(intent.getData(), null, null);
+                                                                }
+                                                            });
             } else {
                 Toast.makeText(sAppContext, R.string.set_image_fail, Toast.LENGTH_SHORT).show();
             }
         } catch(OutOfMemoryError ome) {
             Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
         } catch(IOException ioe) {
             Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
         } finally {
@@ -2316,17 +2316,17 @@ public abstract class GeckoApp
             return;
         }
 
         moveTaskToBack(true);
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (!GeckoAppShell.sActivityHelper.handleActivityResult(requestCode, resultCode, data)) {
+        if (!ActivityHandlerHelper.handleActivityResult(requestCode, resultCode, data)) {
             super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
     public AbsoluteLayout getPluginContainer() { return mPluginContainer; }
 
     // Accelerometer.
     @Override
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -189,17 +189,16 @@ public class GeckoAppShell
     private static Sensor gLinearAccelerometerSensor = null;
     private static Sensor gGyroscopeSensor = null;
     private static Sensor gOrientationSensor = null;
     private static Sensor gProximitySensor = null;
     private static Sensor gLightSensor = null;
 
     private static volatile boolean mLocationHighAccuracy;
 
-    public static ActivityHandlerHelper sActivityHelper = new ActivityHandlerHelper();
     static NotificationClient sNotificationClient;
 
     /* The Android-side API: API methods that Android calls */
 
     // Initialization methods
     public static native void nativeInit();
 
     // helper methods
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -109,16 +109,17 @@ public class GeckoApplication extends Ap
 
         mInBackground = false;
     }
 
     @Override
     public void onCreate() {
         HardwareUtils.init(getApplicationContext());
         Clipboard.init(getApplicationContext());
+        FilePicker.init(getApplicationContext());
         GeckoLoader.loadMozGlue();
         super.onCreate();
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
--- a/mobile/android/base/home/HomeBanner.java
+++ b/mobile/android/base/home/HomeBanner.java
@@ -53,16 +53,18 @@ public class HomeBanner extends LinearLa
 
         // The drawable should have 50% opacity.
         closeButton.getDrawable().setAlpha(127);
 
         closeButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                 HomeBanner.this.setVisibility(View.GONE);
+                // Send the current message id back to JS.
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag()));
             }
         });
 
         setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 // Send the current message id back to JS.
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag()));
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -112,17 +112,17 @@ abstract class HomeFragment extends Frag
         // between the activity and its fragments.
 
         ContextMenuInfo menuInfo = item.getMenuInfo();
         if (menuInfo == null || !(menuInfo instanceof HomeContextMenuInfo)) {
             return false;
         }
 
         final HomeContextMenuInfo info = (HomeContextMenuInfo) menuInfo;
-        final Context context = getActivity().getApplicationContext();
+        final Context context = getActivity();
 
         final int itemId = item.getItemId();
         if (itemId == R.id.home_share) {
             if (info.url == null) {
                 Log.e(LOGTAG, "Can't share because URL is null");
                 return false;
             } else {
                 GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
@@ -155,17 +155,17 @@ abstract class HomeFragment extends Frag
             final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
             Tabs.getInstance().loadUrl(url, flags);
             Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
             return true;
         }
 
         if (itemId == R.id.home_edit_bookmark) {
             // UI Dialog associates to the activity context, not the applications'.
-            new EditBookmarkDialog(getActivity()).show(info.url);
+            new EditBookmarkDialog(context).show(info.url);
             return true;
         }
 
         if (itemId == R.id.home_open_in_reader) {
             final String url = ReaderModeUtils.getAboutReaderForUrl(info.url);
             Tabs.getInstance().loadUrl(url, Tabs.LOADURL_NONE);
             return true;
         }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -98,18 +98,16 @@ gbjar.sources += [
     'animation/HeightChangeAnimation.java',
     'animation/PropertyAnimator.java',
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
     'ANRReporter.java',
     'AppNotificationClient.java',
     'BaseGeckoInterface.java',
     'BrowserApp.java',
-    'CameraImageResultHandler.java',
-    'CameraVideoResultHandler.java',
     'ContactService.java',
     'ContextGetter.java',
     'CustomEditText.java',
     'DataReportingNotification.java',
     'db/BrowserContract.java',
     'db/BrowserDatabaseHelper.java',
     'db/BrowserDB.java',
     'db/BrowserProvider.java',
@@ -131,18 +129,18 @@ gbjar.sources += [
     'favicons/cache/FaviconsForURL.java',
     'favicons/decoders/FaviconDecoder.java',
     'favicons/decoders/ICODecoder.java',
     'favicons/decoders/IconDirectoryEntry.java',
     'favicons/decoders/LoadFaviconResult.java',
     'favicons/Favicons.java',
     'favicons/LoadFaviconTask.java',
     'favicons/OnFaviconLoadedListener.java',
+    'FilePicker.java',
     'FilePickerResultHandler.java',
-    'FilePickerResultHandlerSync.java',
     'FindInPageBar.java',
     'FormAssistPopup.java',
     'GeckoAccessibility.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
     'GeckoAppShell.java',
--- a/mobile/android/base/prompts/ColorPickerInput.java
+++ b/mobile/android/base/prompts/ColorPickerInput.java
@@ -40,17 +40,17 @@ public class ColorPickerInput extends Pr
 
         BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
         cp.setColor(mInitialColor);
 
         return mView;
     }
 
     @Override
-    public String getValue() {
+    public Object getValue() {
         BasicColorPicker cp = (BasicColorPicker) mView.findViewById(R.id.colorpicker);
         int color = cp.getColor();
         return "#" + Integer.toHexString(color).substring(2);
     }
 
     @Override
     public boolean getScrollable() {
         return true;
--- a/mobile/android/base/prompts/IconGridInput.java
+++ b/mobile/android/base/prompts/IconGridInput.java
@@ -98,18 +98,18 @@ public class IconGridInput extends Promp
     }
 
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         mSelected = position;
     }
 
     @Override
-    public String getValue() {
-        return Integer.toString(mSelected);
+    public Object getValue() {
+        return new Integer(mSelected);
     }
 
     @Override
     public boolean getScrollable() {
         return true;
     }
 
     private class IconGridAdapter extends ArrayAdapter<IconGridItem> {
--- a/mobile/android/base/prompts/Prompt.java
+++ b/mobile/android/base/prompts/Prompt.java
@@ -155,28 +155,35 @@ public class Prompt implements OnClickLi
     }
 
     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.
+     *  object that's passed in. If this is a multi-select dialog, sets a
+     *  selected attribute to an array of booleans.
      */
     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]);
+                    if (mSelected[i]) {
+                        selected.put(i);
+                    }
                 }
-                result.put("button", selected);
+                result.put("list", selected);
             } else {
+                // Mirror the selected array from multi choice for consistency.
+                JSONArray selected = new JSONArray();
+                selected.put(which);
+                result.put("list", selected);
+                // Make the button be the index of the select item.
                 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.
      */
@@ -207,22 +214,22 @@ public class Prompt implements OnClickLi
     }
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
         ThreadUtils.assertOnUiThread();
         JSONObject ret = new JSONObject();
         try {
             ListView list = mDialog.getListView();
+            addButtonResult(ret, which);
+            addInputValues(ret);
+
             if (list != null || mSelected != null) {
                 addListResult(ret, which);
-            } else {
-                addButtonResult(ret, which);
             }
-            addInputValues(ret);
         } catch(Exception ex) {
             Log.i(LOGTAG, "Error building return: " + ex);
         }
 
         if (dialog != null) {
             dialog.dismiss();
         }
 
--- a/mobile/android/base/prompts/PromptInput.java
+++ b/mobile/android/base/prompts/PromptInput.java
@@ -78,20 +78,20 @@ public class PromptInput {
                     }
                 });
                 input.requestFocus();
             }
 
             mView = (View)input;
             return mView;
         }
-
-        public String getValue() {
+        @Override
+        public Object getValue() {
             EditText edit = (EditText)mView;
-            return edit.getText().toString();
+            return edit.getText();
         }
     }
 
     public static class NumberInput extends EditInput {
         public static final String INPUT_TYPE = "number";
         public NumberInput(JSONObject obj) {
             super(obj);
         }
@@ -113,20 +113,20 @@ public class PromptInput {
 
         public View getView(Context context) throws UnsupportedOperationException {
             EditText input = (EditText) super.getView(context);
             input.setInputType(InputType.TYPE_CLASS_TEXT |
                                InputType.TYPE_TEXT_VARIATION_PASSWORD |
                                InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
             return input;
         }
-
-        public String getValue() {
+        @Override
+        public Object getValue() {
             EditText edit = (EditText)mView;
-            return edit.getText().toString();
+            return edit.getText();
         }
     }
 
     public static class CheckboxInput extends PromptInput {
         public static final String INPUT_TYPE = "checkbox";
         private boolean mChecked;
 
         public CheckboxInput(JSONObject obj) {
@@ -137,20 +137,20 @@ public class PromptInput {
         public View getView(Context context) throws UnsupportedOperationException {
             CheckBox checkbox = new CheckBox(context);
             checkbox.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
             checkbox.setText(mLabel);
             checkbox.setChecked(mChecked);
             mView = (View)checkbox;
             return mView;
         }
-
-        public String getValue() {
+        @Override
+        public Object getValue() {
             CheckBox checkbox = (CheckBox)mView;
-            return checkbox.isChecked() ? "true" : "false";
+            return checkbox.isChecked() ? Boolean.TRUE : Boolean.FALSE;
         }
     }
 
     public static class DateTimeInput extends PromptInput {
         public static final String[] INPUT_TYPES = new String[] {
             "date",
             "week",
             "time",
@@ -215,18 +215,18 @@ public class PromptInput {
                 mView = (View)input;
             }
             return mView;
         }
 
         private static String formatDateString(String dateFormat, Calendar calendar) {
             return new SimpleDateFormat(dateFormat).format(calendar.getTime());
         }
-
-        public String getValue() {
+        @Override
+        public Object getValue() {
             if (Build.VERSION.SDK_INT < 11 && mType.equals("date")) {
                 // We can't use the custom DateTimePicker with a sdk older than 11.
                 // Fallback on the native DatePicker.
                 DatePicker dp = (DatePicker)mView;
                 GregorianCalendar calendar =
                     new GregorianCalendar(dp.getYear(),dp.getMonth(),dp.getDayOfMonth());
                 return formatDateString("yyyy-MM-dd",calendar);
             } else if (mType.equals("time")) {
@@ -295,37 +295,37 @@ public class PromptInput {
                 container.addView(textView);
 
                 container.addView(spinner);
                 return container;
             }
 
             return spinner;
         }
-
-        public String getValue() {
-            return Integer.toString(spinner.getSelectedItemPosition());
+        @Override
+        public Object getValue() {
+            return new Integer(spinner.getSelectedItemPosition());
         }
     }
 
     public static class LabelInput extends PromptInput {
         public static final String INPUT_TYPE = "label";
         public LabelInput(JSONObject obj) {
             super(obj);
         }
 
         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 String getValue() {
+        @Override
+        public Object getValue() {
             return "";
         }
     }
 
     public PromptInput(JSONObject obj) {
         mLabel = obj.optString("label");
         mType = obj.optString("type");
         String id = obj.optString("id");
@@ -364,17 +364,17 @@ public class PromptInput {
     public View getView(Context context) throws UnsupportedOperationException {
         return null;
     }
 
     public String getId() {
         return mId;
     }
 
-    public String getValue() {
+    public Object getValue() {
         return "";
     }
 
     public boolean getScrollable() {
         return false;
     }
 
     public boolean canApplyInputStyle() {
--- a/mobile/android/base/tests/components/AboutHomeComponent.java
+++ b/mobile/android/base/tests/components/AboutHomeComponent.java
@@ -12,16 +12,17 @@ import org.mozilla.gecko.tests.helpers.*
 import org.mozilla.gecko.tests.UITestContext;
 
 import com.jayway.android.robotium.solo.Condition;
 import com.jayway.android.robotium.solo.Solo;
 
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
 import android.view.View;
+import android.widget.TextView;
 
 /**
  * A class representing any interactions that take place on the Awesomescreen.
  */
 public class AboutHomeComponent extends BaseComponent {
     private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
 
     // The different types of panels that can be present on about:home
@@ -57,16 +58,20 @@ public class AboutHomeComponent extends 
     public AboutHomeComponent(final UITestContext testContext) {
         super(testContext);
     }
 
     private ViewPager getHomePagerView() {
         return (ViewPager) mSolo.getView(R.id.home_pager);
     }
 
+    private View getHomeBannerView() {
+        return mSolo.getView(R.id.home_banner);
+    }
+
     public AboutHomeComponent assertCurrentPanel(final PanelType expectedPanel) {
         assertVisible();
 
         final int expectedPanelIndex = getPanelIndexForDevice(expectedPanel.ordinal());
         assertEquals("The current HomePager panel is " + expectedPanel,
                      expectedPanelIndex, getHomePagerView().getCurrentItem());
         return this;
     }
@@ -78,16 +83,53 @@ public class AboutHomeComponent extends 
     }
 
     public AboutHomeComponent assertVisible() {
         assertEquals("The HomePager is visible",
                      View.VISIBLE, getHomePagerView().getVisibility());
         return this;
     }
 
+    public AboutHomeComponent assertBannerNotVisible() {
+        assertFalse("The HomeBanner is not visible",
+                    getHomeBannerView().getVisibility() == View.VISIBLE);
+        return this;
+    }
+
+    public AboutHomeComponent assertBannerVisible() {
+        assertEquals("The HomeBanner is visible",
+                     View.VISIBLE, getHomeBannerView().getVisibility());
+        return this;
+    }
+
+    public AboutHomeComponent assertBannerText(String text) {
+        assertBannerVisible();
+
+        final TextView textView = (TextView) getHomeBannerView().findViewById(R.id.text);
+        assertEquals("The correct HomeBanner text is shown",
+                     text, textView.getText().toString());
+        return this;
+    }
+
+    public AboutHomeComponent clickOnBanner() {
+        assertBannerVisible();
+
+        mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner.");
+        mSolo.clickOnView(getHomeBannerView());
+        return this;
+    }
+
+    public AboutHomeComponent dismissBanner() {
+        assertBannerVisible();
+
+        mTestContext.dumpLog(LOGTAG, "Clicking on HomeBanner close button.");
+        mSolo.clickOnView(getHomeBannerView().findViewById(R.id.close));
+        return this;
+    }
+
     public AboutHomeComponent swipeToPanelOnRight() {
         mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the right.");
         swipeToPanel(Solo.RIGHT);
         return this;
     }
 
     public AboutHomeComponent swipeToPanelOnLeft() {
         mTestContext.dumpLog(LOGTAG, "Swiping to the panel on the left.");
--- a/mobile/android/base/tests/helpers/GeckoHelper.java
+++ b/mobile/android/base/tests/helpers/GeckoHelper.java
@@ -22,18 +22,30 @@ public final class GeckoHelper {
     private GeckoHelper() { /* To disallow instantiation. */ }
 
     protected static void init(final UITestContext context) {
         sActivity = context.getActivity();
         sActions = context.getActions();
     }
 
     public static void blockForReady() {
-        final EventExpecter geckoReady = sActions.expectGeckoEvent("Gecko:Ready");
+        blockForEvent("Gecko:Ready");
+    }
 
-        final boolean isReady = GeckoThread.checkLaunchState(LaunchState.GeckoRunning);
-        if (!isReady) {
-            geckoReady.blockForEvent();
+    /**
+     * Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the
+     * first page load.
+     */
+    public static void blockForDelayedStartup() {
+        blockForEvent("Gecko:DelayedStartup");
+    }
+
+    private static void blockForEvent(final String eventName) {
+        final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName);
+
+        final boolean isRunning = GeckoThread.checkLaunchState(LaunchState.GeckoRunning);
+        if (!isRunning) {
+            eventExpecter.blockForEvent();
         }
 
-        geckoReady.unregisterListener();
+        eventExpecter.unregisterListener();
     }
 }
--- a/mobile/android/base/tests/helpers/NavigationHelper.java
+++ b/mobile/android/base/tests/helpers/NavigationHelper.java
@@ -44,21 +44,21 @@ final public class NavigationHelper {
     }
 
     /**
      * Returns a new URL with the docshell HTTP server host prefix.
      */
     private static String adjustUrl(final String url) {
         assertNotNull("url is not null", url);
 
-        if (!url.startsWith("about:")) {
-            return sContext.getAbsoluteHostnameUrl(url);
+        if (url.startsWith("about:") || url.startsWith("chrome:")) {
+            return url;
         }
 
-        return url;
+        return sContext.getAbsoluteHostnameUrl(url);
     }
 
     public static void goBack() {
         if (DeviceHelper.isTablet()) {
             sToolbar.pressBackButton(); // Waits for page load & asserts isNotEditing.
             return;
         }
 
--- a/mobile/android/base/tests/robocop_text_page.html
+++ b/mobile/android/base/tests/robocop_text_page.html
@@ -1,16 +1,17 @@
 <html>
 <head>
 <title> Robocop Text Page </title>
 <meta name="viewport" content="initial-scale=1.0"/>
 <meta charset="utf-8">
 </head>
 <body>
 <div style='float: left; width: 100%; height: 500px; margin: 0; padding: 0; border: none; background-color: rgb(250,0,0)'> </div>
+<div style='float: left; width: 10%; height: 500px; margin: 0; padding: 0; border: none; background-color: rgb(250,0,0)'> </div>
 <p>Text taken from Wikipedia.org</p>
 <p> <b>Will be searching for this string:</b> Robocop </p>
 <p>Mozilla is a free software community best known for producing the Firefox web browser. The Mozilla community uses, develops, spreads and supports Mozilla products and works to advance the goals of the Open Web described in the Mozilla Manifesto.[1] The community is supported institutionally by the Mozilla Foundation and its tax-paying subsidiary, the Mozilla Corporation.[2] </p>
 <div style='float: left; width: 100%; height: 500px; margin: 0; padding: 0; border: none'> </div>
 <p> <b>Will be searching for this string:</b> Robocop </p>
 <p>In addition to the Firefox browser, Mozilla also produces Firefox Mobile, the Firefox OS mobile operating system, the bug tracking system Bugzilla and a number of other projects.</p>
 <div style='float: left; width: 200%; height: 500px; margin: 0; padding: 0; border: none'> </div>
 <p> <b>Will be searching for this string:</b> Robocop </p>
--- a/mobile/android/base/tests/roboextender/robocop_home_banner.html
+++ b/mobile/android/base/tests/roboextender/robocop_home_banner.html
@@ -10,26 +10,36 @@ Components.utils.import("resource://gre/
 
 const TEXT = "The quick brown fox jumps over the lazy dog.";
 
 function start() {
   var test = location.hash.substring(1);
   window[test]();
 }
 
+var messageId;
+
 function addMessage() {
-  Home.banner.add({
+  messageId = Home.banner.add({
     text: TEXT,
     onclick: function() {
       sendMessageToJava({ type: "TestHomeBanner:MessageClicked" });
     },
     onshown: function() {
       sendMessageToJava({ type: "TestHomeBanner:MessageShown" });
+    },
+    ondismiss: function() {
+      sendMessageToJava({ type: "TestHomeBanner:MessageDismissed" });
     }
   });
   sendMessageToJava({ type: "TestHomeBanner:MessageAdded" });
 }
 
+function removeMessage() {
+  Home.banner.remove(messageId);
+  sendMessageToJava({ type: "TestHomeBanner:MessageRemoved" });
+}
+
     </script>
   </head>
   <body onload="start();">
   </body>
 </html>
--- a/mobile/android/base/tests/testAboutHomePageNavigation.java
+++ b/mobile/android/base/tests/testAboutHomePageNavigation.java
@@ -8,17 +8,17 @@ import org.mozilla.gecko.tests.helpers.*
 /**
  * Tests functionality related to navigating between the various about:home panels.
  */
 public class testAboutHomePageNavigation extends UITest {
     // TODO: Define this test dynamically by creating dynamic representations of the Page
     // enum for both phone and tablet, then swiping through the panels. This will also
     // benefit having a HomePager with custom panels.
     public void testAboutHomePageNavigation() {
-        GeckoHelper.blockForReady();
+        GeckoHelper.blockForDelayedStartup();
 
         mAboutHome.assertVisible()
                   .assertCurrentPanel(PanelType.TOP_SITES);
 
         mAboutHome.swipeToPanelOnRight();
         mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
 
         mAboutHome.swipeToPanelOnRight();
--- a/mobile/android/base/tests/testFindInPage.java
+++ b/mobile/android/base/tests/testFindInPage.java
@@ -13,34 +13,38 @@ public class testFindInPage extends Pixe
     protected int getTestType() {
         return TEST_MOCHITEST;
     }
 
     public void testFindInPage() {
         blockForGeckoReady();
         String url = getAbsoluteUrl("/robocop/robocop_text_page.html");
         loadAndPaint(url);
+
+        // Select the upper left corner of the screen
         height = mDriver.getGeckoHeight()/8;
-        width = mDriver.getGeckoWidth()/2;
+        width = mDriver.getGeckoWidth()/8;
 
+        /* Disabled by bug 958111.
         // Search that does not find the term and therefor should not pan the page
         Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
         findText("Robocoop", 3); // This will be close enough to existing text to test that search finds just what it should
         PaintedSurface painted = waitForPaint(paintExpecter);
         paintExpecter.unregisterListener();
         try {
             mAsserter.ispixel(painted.getPixelAt(width,height), 255, 0, 0, "Pixel at " + String.valueOf(width) + "," + String.valueOf(height));
         } finally {
             painted.close();
         }
+        */
 
         // Search that finds matches and therefor pans the page
-        paintExpecter = mActions.expectPaint();
+        Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
         findText("Robocop", 3);
-        painted = waitForPaint(paintExpecter);
+        PaintedSurface painted = waitForPaint(paintExpecter);
         paintExpecter.unregisterListener();
         try {
             mAsserter.isnotpixel(painted.getPixelAt(width,height), 255, 0, 0, "Pixel at " + String.valueOf(width) + "," + String.valueOf(height));
         } finally {
             painted.close();
         }
     }
 
--- a/mobile/android/base/tests/testHomeBanner.java
+++ b/mobile/android/base/tests/testHomeBanner.java
@@ -1,37 +1,116 @@
 package org.mozilla.gecko.tests;
 
-import org.mozilla.gecko.*;
+import static org.mozilla.gecko.tests.helpers.AssertionHelper.*;
 
-public class testHomeBanner extends BaseTest {
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.tests.helpers.*;
+
+import android.view.View;
+
+public class testHomeBanner extends UITest {
 
     private static final String TEST_URL = "chrome://roboextender/content/robocop_home_banner.html";
     private static final String TEXT = "The quick brown fox jumps over the lazy dog.";
 
-    @Override
-    protected int getTestType() {
-        return TEST_MOCHITEST;
+    public void testHomeBanner() {
+        GeckoHelper.blockForReady();
+
+        // Make sure the banner is not visible to start.
+        mAboutHome.assertVisible()
+                  .assertBannerNotVisible();
+
+        // These test methods depend on being run in this order.
+        addBannerTest();
+        removeBannerTest();
+
+        // Make sure to test dismissing the banner after everything else, since dismissing
+        // the banner will prevent it from showing up again.
+        dismissBannerTest();
+    }
+
+    /**
+     * Adds a banner message, verifies that it appears when it should, and verifies that
+     * onshown/onclick handlers are called in JS.
+     *
+     * Note: This test does not remove the message after it is done.
+     */
+    private void addBannerTest() {
+        addBannerMessage();
+
+        // Load about:home again, and make sure the onshown handler is called.
+        Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageShown");
+        NavigationHelper.enterAndLoadUrl("about:home");
+        eventExpecter.blockForEvent();
+
+        // Verify that the banner is visible with the correct text.
+        mAboutHome.assertBannerText(TEXT);
+
+        // Test to make sure the onclick handler is called.
+        eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageClicked");
+        mAboutHome.clickOnBanner();
+        eventExpecter.blockForEvent();
+
+        // Verify that the banner isn't visible after navigating away from about:home.
+        NavigationHelper.enterAndLoadUrl("about:firefox");
+
+        // AboutHomeComponent calls mSolo.getView, which will fail an assertion if the
+        // view is not present, so we need to use findViewById in this case.
+        final View banner = getActivity().findViewById(R.id.home_banner);
+        assertTrue("The HomeBanner is not visible", banner == null || banner.getVisibility() != View.VISIBLE);
     }
 
-    public void testHomeBanner() {
-        blockForGeckoReady();
+
+    /**
+     * Removes a banner message, and verifies that it no longer appears on about:home.
+     *
+     * Note: This test expects for a message to have been added before it runs.
+     */
+    private void removeBannerTest() {
+        removeBannerMessage();
+
+        // Verify that the banner no longer appears.
+        NavigationHelper.enterAndLoadUrl("about:home");
+        mAboutHome.assertVisible()
+                  .assertBannerNotVisible();
+    }
 
-        Actions.EventExpecter eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageAdded");
+    /**
+     * Adds a banner message, verifies that its ondismiss handler is called in JS,
+     * and verifies that the banner is no longer shown after it is dismissed.
+     *
+     * Note: This test does not remove the message after it is done.
+     */
+    private void dismissBannerTest() {
+        // Add back the banner message to test the dismiss functionality.
+        addBannerMessage();
 
-        // Add a message to the home banner
-        inputAndLoadUrl(TEST_URL + "#addMessage");
+        NavigationHelper.enterAndLoadUrl("about:home");
+        mAboutHome.assertVisible();
+
+        // Test to make sure the ondismiss handler is called when the close button is clicked.
+        final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageDismissed");
+        mAboutHome.dismissBanner();
         eventExpecter.blockForEvent();
 
-        // Load about:home, and test to make sure the onshown handler is called
-        eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageShown");
-        inputAndLoadUrl("about:home");
-        eventExpecter.blockForEvent();
+        mAboutHome.assertBannerNotVisible();
+    }
 
-        // Verify that the correct message text showed up in the banner
-        mAsserter.ok(waitForText(TEXT), "banner text", "correct text appeared in the home banner");
+    /**
+     * Loads the roboextender page to add a message to the banner.
+     */
+    private void addBannerMessage() {
+        final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageAdded");
+        NavigationHelper.enterAndLoadUrl(TEST_URL + "#addMessage");
+        eventExpecter.blockForEvent();
+    }
 
-        // Test to make sure the onclick handler is called
-        eventExpecter = mActions.expectGeckoEvent("TestHomeBanner:MessageClicked");
-        mSolo.clickOnText(TEXT);
+    /**
+     * Loads the roboextender page to remove the message from the banner.
+     */
+    private void removeBannerMessage() {
+        final Actions.EventExpecter eventExpecter = getActions().expectGeckoEvent("TestHomeBanner:MessageRemoved");
+        NavigationHelper.enterAndLoadUrl(TEST_URL + "#removeMessage");
         eventExpecter.blockForEvent();
     }
 }
--- a/mobile/android/base/webapp/EventListener.java
+++ b/mobile/android/base/webapp/EventListener.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.webapp;
 
+import org.mozilla.gecko.ActivityHandlerHelper;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.util.GeckoEventListener;
@@ -218,17 +219,17 @@ public class EventListener implements Ge
             // TODO: propagate the error back to the mozApps.install caller.
             return;
         }
 
         Intent intent = new Intent(Intent.ACTION_VIEW);
         intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
 
         // Now call the package installer.
-        GeckoAppShell.sActivityHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
+        ActivityHandlerHelper.startIntentForActivity(context, intent, new ActivityResultHandler() {
             @Override
             public void onActivityResult(int resultCode, Intent data) {
                 // The InstallListener will catch the case where the user pressed install.
                 // Now deal with if the user pressed cancel.
                 if (resultCode == Activity.RESULT_CANCELED) {
                     try {
                         context.unregisterReceiver(receiver);
                         receiver.cleanup();
--- a/mobile/android/chrome/content/FindHelper.js
+++ b/mobile/android/chrome/content/FindHelper.js
@@ -72,12 +72,13 @@ var FindHelper = {
           // this should never happen
           Cu.reportError("Warning: selected tab changed during find!");
           // fall through and restore viewport on the initial tab anyway
         }
         this._targetTab.setViewport(JSON.parse(this._initialViewport));
         this._targetTab.sendViewportUpdate();
       }
     } else {
+      ZoomHelper.zoomToRect(aData.rect, -1, false, true);
       this._viewportChanged = true;
     }
   }
 };
--- a/mobile/android/chrome/content/SelectHelper.js
+++ b/mobile/android/chrome/content/SelectHelper.js
@@ -32,42 +32,35 @@ var SelectHelper = {
       p.addButton({
         label: Strings.browser.GetStringFromName("selectHelper.closeMultipleSelectDialog")
       }).setMultiChoiceItems(list);
     } else {
       p.setSingleChoiceItems(list);
     }
 
     p.show((function(data) {
-      let selected = data.button;
-      if (selected == -1)
-          return;
+      let selected = data.list;
 
       if (aElement instanceof Ci.nsIDOMXULMenuListElement) {
-        if (aElement.selectedIndex != selected) {
-          aElement.selectedIndex = selected;
+        if (aElement.selectedIndex != selected[0]) {
+          aElement.selectedIndex = selected[0];
           this.fireOnCommand(aElement);
         }
       } else if (aElement instanceof HTMLSelectElement) {
         let changed = false;
-        if (!Array.isArray(selected)) {
-          let temp = [];
-          for (let i = 0; i <= list.length; i++) {
-            temp[i] = (i == selected);
-          }
-          selected = temp;
-        }
-
         let i = 0;
         this.forOptions(aElement, function(aNode) {
-          if (aNode.selected != selected[i]) {
+          if (aNode.selected && selected.indexOf(i) == -1) {
             changed = true;
-            aNode.selected = selected[i];
+            aNode.selected = false;
+          } else if (!aNode.selected && selected.indexOf(i) != -1) {
+            changed = true;
+            aNode.selected = true;
           }
-          i++
+          i++;
         });
 
         if (changed)
           this.fireOnChange(aElement);
       }
     }).bind(this));
   },
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/ZoomHelper.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var ZoomHelper = {
+  zoomInAndSnapToRange: function(aRange) {
+    // aRange is always non-null here, since a check happened previously.
+    let viewport = BrowserApp.selectedTab.getViewport();
+    let fudge = 15; // Add a bit of fudge.
+    let boundingElement = aRange.offsetNode;
+    while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
+      boundingElement = boundingElement.parentNode;
+    }
+
+    let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
+    let drRect = aRange.getClientRect();
+    let scrollTop =
+      BrowserApp.selectedBrowser.contentDocument.documentElement.scrollTop ||
+      BrowserApp.selectedBrowser.contentDocument.body.scrollTop;
+
+    // We subtract half the height of the viewport so that we can (ideally)
+    // center the area of interest on the screen.
+    let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
+
+    // Factor in the border and padding
+    let boundingStyle = window.getComputedStyle(boundingElement);
+    let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
+                         parseInt(boundingStyle.borderLeftWidth);
+
+    BrowserApp.selectedTab._mReflozPositioned = true;
+
+    rect.type = "Browser:ZoomToRect";
+    rect.x = Math.max(viewport.cssPageLeft, rect.x  - fudge + leftAdjustment);
+    rect.y = Math.max(topPos, viewport.cssPageTop);
+    rect.w = viewport.cssWidth;
+    rect.h = viewport.cssHeight;
+    rect.animate = false;
+
+    sendMessageToJava(rect);
+    BrowserApp.selectedTab._mReflozPoint = null;
+  },
+
+  zoomOut: function() {
+    BrowserEventHandler.resetMaxLineBoxWidth();
+    sendMessageToJava({ type: "Browser:ZoomToPageWidth" });
+  },
+
+  isRectZoomedIn: function(aRect, aViewport) {
+    // This function checks to see if the area of the rect visible in the
+    // viewport (i.e. the "overlapArea" variable below) is approximately
+    // the max area of the rect we can show. It also checks that the rect
+    // is actually on-screen by testing the left and right edges of the rect.
+    // In effect, this tells us whether or not zooming in to this rect
+    // will significantly change what the user is seeing.
+    const minDifference = -20;
+    const maxDifference = 20;
+    const maxZoomAllowed = 4; // keep this in sync with mobile/android/base/ui/PanZoomController.MAX_ZOOM
+
+    let vRect = new Rect(aViewport.cssX, aViewport.cssY, aViewport.cssWidth, aViewport.cssHeight);
+    let overlap = vRect.intersect(aRect);
+    let overlapArea = overlap.width * overlap.height;
+    let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
+    let showing = overlapArea / (aRect.width * availHeight);
+    let dw = (aRect.width - vRect.width);
+    let dx = (aRect.x - vRect.x);
+
+    if (fuzzyEquals(aViewport.zoom, maxZoomAllowed) && overlap.width / aRect.width > 0.9) {
+      // we're already at the max zoom and the block is not spilling off the side of the screen so that even
+      // if the block isn't taking up most of the viewport we can't pan/zoom in any more. return true so that we zoom out
+      return true;
+    }
+
+    return (showing > 0.9 &&
+            dx > minDifference && dx < maxDifference &&
+            dw > minDifference && dw < maxDifference);
+  },
+
+  /* Zoom to an element, optionally keeping a particular part of it
+   * in view if it is really tall.
+   */
+  zoomToElement: function(aElement, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
+    let rect = ElementTouchHelper.getBoundingContentRect(aElement);
+    ZoomHelper.zoomToRect(rect, aClickY, aCanZoomOut, aCanScrollHorizontally);
+  },
+
+  zoomToRect: function(aRect, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
+    const margin = 15;
+
+    if(!aRect.h || !aRect.w) {
+      aRect.h = aRect.height;
+      aRect.w = aRect.width;
+    }
+
+    let viewport = BrowserApp.selectedTab.getViewport();
+    let bRect = new Rect(aCanScrollHorizontally ? Math.max(viewport.cssPageLeft, aRect.x - margin) : viewport.cssX,
+                         aRect.y,
+                         aCanScrollHorizontally ? aRect.w + 2 * margin : viewport.cssWidth,
+                         aRect.h);
+    // constrict the rect to the screen's right edge
+    bRect.width = Math.min(bRect.width, viewport.cssPageRight - bRect.x);
+
+    // if the rect is already taking up most of the visible area and is stretching the
+    // width of the page, then we want to zoom out instead.
+    if (BrowserEventHandler.mReflozPref) {
+      let zoomFactor = BrowserApp.selectedTab.getZoomToMinFontSize(aElement);
+
+      bRect.width = zoomFactor <= 1.0 ? bRect.width : gScreenWidth / zoomFactor;
+      bRect.height = zoomFactor <= 1.0 ? bRect.height : bRect.height / zoomFactor;
+      if (zoomFactor == 1.0 || ZoomHelper.isRectZoomedIn(bRect, viewport)) {
+        if (aCanZoomOut) {
+          ZoomHelper.zoomOut();
+        }
+        return;
+      }
+    } else if (ZoomHelper.isRectZoomedIn(bRect, viewport)) {
+      if (aCanZoomOut) {
+        ZoomHelper.zoomOut();
+      }
+      return;
+    }
+
+    let rect = {};
+
+    rect.type = "Browser:ZoomToRect";
+    rect.x = bRect.x;
+    rect.y = bRect.y;
+    rect.w = bRect.width;
+    rect.h = Math.min(bRect.width * viewport.cssHeight / viewport.cssWidth, bRect.height);
+
+    if (aClickY >= 0) {
+      // if the block we're zooming to is really tall, and we want to keep a particular
+      // part of it in view, then adjust the y-coordinate of the target rect accordingly.
+      // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
+      // margins but not vertical ones.
+      let cssTapY = viewport.cssY + aClickY;
+      if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
+        rect.y = cssTapY - (rect.h / 2);
+      }
+    }
+
+    if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
+      BrowserEventHandler.resetMaxLineBoxWidth();
+    }
+
+    sendMessageToJava(rect);
+  },
+};
\ No newline at end of file
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -86,16 +86,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 [
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
   ["AboutReader", "chrome://browser/content/aboutReader.js"],
   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
   ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
   ["Linkifier", "chrome://browser/content/Linkify.js"],
+  ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
   ["CastingApps", "chrome://browser/content/CastingApps.js"],
 ].forEach(function (aScript) {
   let [name, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
@@ -183,17 +184,17 @@ function doChangeMaxLineBoxWidth(aWidth)
     range = BrowserApp.selectedTab._mReflozPoint.range;
   }
 
   try {
     docViewer.pausePainting();
     docViewer.changeMaxLineBoxWidth(aWidth);
 
     if (range) {
-      BrowserEventHandler._zoomInAndSnapToRange(range);
+      ZoomHelper.zoomInAndSnapToRange(range);
     } else {
       // In this case, we actually didn't zoom into a specific range. It
       // probably happened from a page load reflow-on-zoom event, so we
       // need to make sure painting is re-enabled.
       BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
     }
   } finally {
     docViewer.resumePainting();
@@ -1371,18 +1372,18 @@ var BrowserApp = {
       return;
 
     let focused = this.getFocusedInput(aBrowser);
 
     if (focused) {
       let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
       if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
         shouldZoom = false;
-       // _zoomToElement will handle not sending any message if this input is already mostly filling the screen
-      BrowserEventHandler._zoomToElement(focused, -1, false,
+      // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
+      ZoomHelper.zoomToElement(focused, -1, false,
           aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
     let browser = this.selectedBrowser;
 
     switch (aTopic) {
@@ -2924,26 +2925,26 @@ Tab.prototype = {
    *    painting.
    * 2. During the next call to setViewport(), which is in the Tab prototype,
    *    we detect that a call to changeMaxLineBoxWidth should be performed. If
    *    we're zooming out, then the max line box width should be reset at this
    *    time. Otherwise, we call performReflowOnZoom.
    *   2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
    *       doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
    * 3. doChangeMaxLineBoxWidth changes the line box width (which also
-   *    schedules a reflow event), and then calls _zoomInAndSnapToRange.
-   * 4. _zoomInAndSnapToRange performs the positioning of reflow-on-zoom and
-   *    then re-enables painting.
+   *    schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
+   * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
+   *    and then re-enables painting.
    *
    * Some of the events happen synchronously, while others happen asynchronously.
    * The following is a rough sketch of the progression of events:
    *
    * double tap event seen -> onDoubleTap() -> ... asynchronous ...
    *   -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
-   *   -> doChangeMaxLineBoxWidth() -> _zoomInAndSnapToRange()
+   *   -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
    *   -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
    *   -> resumePainting()
    */
   performReflowOnZoom: function(aViewport) {
     let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
 
     let viewportWidth = gScreenWidth / zoom;
     let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
@@ -4573,51 +4574,16 @@ var BrowserEventHandler = {
         break;
 
       default:
         dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
         break;
     }
   },
 
-  _zoomOut: function() {
-    BrowserEventHandler.resetMaxLineBoxWidth();
-    sendMessageToJava({ type: "Browser:ZoomToPageWidth" });
-  },
-
-  _isRectZoomedIn: function(aRect, aViewport) {
-    // This function checks to see if the area of the rect visible in the
-    // viewport (i.e. the "overlapArea" variable below) is approximately
-    // the max area of the rect we can show. It also checks that the rect
-    // is actually on-screen by testing the left and right edges of the rect.
-    // In effect, this tells us whether or not zooming in to this rect
-    // will significantly change what the user is seeing.
-    const minDifference = -20;
-    const maxDifference = 20;
-    const maxZoomAllowed = 4; // keep this in sync with mobile/android/base/ui/PanZoomController.MAX_ZOOM
-
-    let vRect = new Rect(aViewport.cssX, aViewport.cssY, aViewport.cssWidth, aViewport.cssHeight);
-    let overlap = vRect.intersect(aRect);
-    let overlapArea = overlap.width * overlap.height;
-    let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
-    let showing = overlapArea / (aRect.width * availHeight);
-    let dw = (aRect.width - vRect.width);
-    let dx = (aRect.x - vRect.x);
-
-    if (fuzzyEquals(aViewport.zoom, maxZoomAllowed) && overlap.width / aRect.width > 0.9) {
-      // we're already at the max zoom and the block is not spilling off the side of the screen so that even
-      // if the block isn't taking up most of the viewport we can't pan/zoom in any more. return true so that we zoom out
-      return true;
-    }
-
-    return (showing > 0.9 &&
-            dx > minDifference && dx < maxDifference &&
-            dw > minDifference && dw < maxDifference);
-  },
-
   onDoubleTap: function(aData) {
     let data = JSON.parse(aData);
     let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
 
     // We only want to do this if reflow-on-zoom is enabled, we don't already
     // have a reflow-on-zoom event pending, and the element upon which the user
     // double-tapped isn't of a type we want to avoid reflow-on-zoom.
     if (BrowserEventHandler.mReflozPref &&
@@ -4638,27 +4604,27 @@ var BrowserEventHandler = {
       let docShell = webNav.QueryInterface(Ci.nsIDocShell);
       let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
       docViewer.pausePainting();
 
       BrowserApp.selectedTab.probablyNeedRefloz = true;
     }
 
     if (!element) {
-      this._zoomOut();
+      ZoomHelper.zoomOut();
       return;
     }
 
     while (element && !this._shouldZoomToElement(element))
       element = element.parentNode;
 
     if (!element) {
-      this._zoomOut();
+      ZoomHelper.zoomOut();
     } else {
-      this._zoomToElement(element, data.y);
+      ZoomHelper.zoomToElement(element, data.y);
     }
   },
 
   /**
    * Determine if reflow-on-zoom functionality should be suppressed, given a
    * particular element. Double-tapping on the following elements suppresses
    * reflow-on-zoom:
    *
@@ -4674,112 +4640,16 @@ var BrowserEventHandler = {
         aElement instanceof Ci.nsIDOMHTMLMediaElement ||
         aElement instanceof Ci.nsIDOMHTMLPreElement) {
       return true;
     }
 
     return false;
   },
 
-  /* Zoom to an element, optionally keeping a particular part of it
-   * in view if it is really tall.
-   */
-  _zoomToElement: function(aElement, aClickY = -1, aCanZoomOut = true, aCanScrollHorizontally = true) {
-    const margin = 15;
-    let rect = ElementTouchHelper.getBoundingContentRect(aElement);
-
-    let viewport = BrowserApp.selectedTab.getViewport();
-    let bRect = new Rect(aCanScrollHorizontally ? Math.max(viewport.cssPageLeft, rect.x - margin) : viewport.cssX,
-                         rect.y,
-                         aCanScrollHorizontally ? rect.w + 2 * margin : viewport.cssWidth,
-                         rect.h);
-    // constrict the rect to the screen's right edge
-    bRect.width = Math.min(bRect.width, viewport.cssPageRight - bRect.x);
-
-    // if the rect is already taking up most of the visible area and is stretching the
-    // width of the page, then we want to zoom out instead.
-    if (BrowserEventHandler.mReflozPref) {
-      let zoomFactor = BrowserApp.selectedTab.getZoomToMinFontSize(aElement);
-
-      bRect.width = zoomFactor <= 1.0 ? bRect.width : gScreenWidth / zoomFactor;
-      bRect.height = zoomFactor <= 1.0 ? bRect.height : bRect.height / zoomFactor;
-      if (zoomFactor == 1.0 || this._isRectZoomedIn(bRect, viewport)) {
-        if (aCanZoomOut) {
-          this._zoomOut();
-        }
-        return;
-      }
-    } else if (this._isRectZoomedIn(bRect, viewport)) {
-      if (aCanZoomOut) {
-        this._zoomOut();
-      }
-      return;
-    }
-
-    rect.type = "Browser:ZoomToRect";
-    rect.x = bRect.x;
-    rect.y = bRect.y;
-    rect.w = bRect.width;
-    rect.h = Math.min(bRect.width * viewport.cssHeight / viewport.cssWidth, bRect.height);
-
-    if (aClickY >= 0) {
-      // if the block we're zooming to is really tall, and we want to keep a particular
-      // part of it in view, then adjust the y-coordinate of the target rect accordingly.
-      // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
-      // margins but not vertical ones.
-      let cssTapY = viewport.cssY + aClickY;
-      if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
-        rect.y = cssTapY - (rect.h / 2);
-      }
-    }
-
-    if (rect.w > viewport.cssWidth || rect.h > viewport.cssHeight) {
-      BrowserEventHandler.resetMaxLineBoxWidth();
-    }
-
-    sendMessageToJava(rect);
-  },
-
-  _zoomInAndSnapToRange: function(aRange) {
-    // aRange is always non-null here, since a check happened previously.
-    let viewport = BrowserApp.selectedTab.getViewport();
-    let fudge = 15; // Add a bit of fudge.
-    let boundingElement = aRange.offsetNode;
-    while (!boundingElement.getBoundingClientRect && boundingElement.parentNode) {
-      boundingElement = boundingElement.parentNode;
-    }
-
-    let rect = ElementTouchHelper.getBoundingContentRect(boundingElement);
-    let drRect = aRange.getClientRect();
-    let scrollTop =
-      BrowserApp.selectedBrowser.contentDocument.documentElement.scrollTop ||
-      BrowserApp.selectedBrowser.contentDocument.body.scrollTop;
-
-    // We subtract half the height of the viewport so that we can (ideally)
-    // center the area of interest on the screen.
-    let topPos = scrollTop + drRect.top - (viewport.cssHeight / 2.0);
-
-    // Factor in the border and padding
-    let boundingStyle = window.getComputedStyle(boundingElement);
-    let leftAdjustment = parseInt(boundingStyle.paddingLeft) +
-                         parseInt(boundingStyle.borderLeftWidth);
-
-    BrowserApp.selectedTab._mReflozPositioned = true;
-
-    rect.type = "Browser:ZoomToRect";
-    rect.x = Math.max(viewport.cssPageLeft, rect.x  - fudge + leftAdjustment);
-    rect.y = Math.max(topPos, viewport.cssPageTop);
-    rect.w = viewport.cssWidth;
-    rect.h = viewport.cssHeight;
-    rect.animate = false;
-
-    sendMessageToJava(rect);
-    BrowserApp.selectedTab._mReflozPoint = null;
-  },
-
   onPinchFinish: function(aData) {
     let data = {};
     try {
       data = JSON.parse(aData);
     } catch(ex) {
       console.log(ex);
       return;
     }
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -47,16 +47,17 @@ chrome.jar:
   content/PluginHelper.js              (content/PluginHelper.js)
   content/OfflineApps.js               (content/OfflineApps.js)
   content/MasterPassword.js            (content/MasterPassword.js)
   content/FindHelper.js                (content/FindHelper.js)
   content/PermissionsHelper.js         (content/PermissionsHelper.js)
   content/FeedHandler.js               (content/FeedHandler.js)
   content/Feedback.js                  (content/Feedback.js)
   content/Linkify.js                   (content/Linkify.js)
+  content/ZoomHelper.js                (content/ZoomHelper.js)
   content/CastingApps.js               (content/CastingApps.js)
 #ifdef MOZ_SERVICES_HEALTHREPORT
   content/aboutHealthReport.xhtml      (content/aboutHealthReport.xhtml)
 * content/aboutHealthReport.js         (content/aboutHealthReport.js)
 #endif
 
 % content branding %content/branding/
 
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -38,16 +38,19 @@ function BannerMessage(options) {
   if ("icon" in options && options.icon != null)
     this.iconURI = resolveGeckoURI(options.icon);
 
   if ("onshown" in options && typeof options.onshown === "function")
     this.onshown = options.onshown;
 
   if ("onclick" in options && typeof options.onclick === "function")
     this.onclick = options.onclick;
+
+  if ("ondismiss" in options && typeof options.ondismiss === "function")
+    this.ondismiss = options.ondismiss;
 }
 
 let HomeBanner = (function () {
   // Holds the messages that will rotate through the banner.
   let _messages = {};
 
   // A queue used to keep track of which message to show next.
   let _queue = [];
@@ -72,26 +75,36 @@ let HomeBanner = (function () {
   };
 
   let _handleClick = function(id) {
     let message = _messages[id];
     if (message.onclick)
       message.onclick();
   };
 
+  let _handleDismiss = function(id) {
+    let message = _messages[id];
+    if (message.ondismiss)
+      message.ondismiss();
+  };
+
   return Object.freeze({
     observe: function(subject, topic, data) {
       switch(topic) {
         case "HomeBanner:Get":
           _handleGet();
           break;
 
         case "HomeBanner:Click":
           _handleClick(data);
           break;
+
+        case "HomeBanner:Dismiss":
+          _handleDismiss(data);
+          break; 
       }
     },
 
     /**
      * Adds a new banner message to the rotation.
      *
      * @return id Unique identifer for the message.
      */
@@ -102,16 +115,17 @@ let HomeBanner = (function () {
       // Add the new message to the end of the queue.
       _queue.push(message.id);
 
       // If this is the first message we're adding, add
       // observers to listen for requests from the Java UI.
       if (Object.keys(_messages).length == 1) {
         Services.obs.addObserver(this, "HomeBanner:Get", false);
         Services.obs.addObserver(this, "HomeBanner:Click", false);
+        Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
 
         // Send a message to Java, in case there's an active HomeBanner
         // waiting for a response.
         _handleGet();
       }
 
       return message.id;
     },
@@ -127,16 +141,17 @@ let HomeBanner = (function () {
       // Remove the message from the queue.
       let index = _queue.indexOf(id);
       _queue.splice(index, 1);
 
       // If there are no more messages, remove the observers.
       if (Object.keys(_messages).length == 0) {
         Services.obs.removeObserver(this, "HomeBanner:Get");
         Services.obs.removeObserver(this, "HomeBanner:Click");
+        Services.obs.removeObserver(this, "HomeBanner:Dismiss");
       }
     }
   });
 })();
 
 function Panel(options) {
   if ("id" in options)
     this.id = options.id;
--- a/python/mozbuild/mozbuild/backend/recursivemake.py
+++ b/python/mozbuild/mozbuild/backend/recursivemake.py
@@ -1037,22 +1037,24 @@ class RecursiveMakeBackend(CommonBackend
     def _process_host_simple_program(self, program, backend_file):
         backend_file.write('HOST_SIMPLE_PROGRAMS += %s\n' % program)
 
     def _process_test_manifest(self, obj, backend_file):
         # Much of the logic in this function could be moved to CommonBackend.
         self.backend_input_files.add(mozpath.join(obj.topsrcdir,
             obj.manifest_relpath))
 
-        # Duplicate manifests may define the same file. That's OK.
-        for source, dest in obj.installs.items():
+        # Don't allow files to be defined multiple times unless it is allowed.
+        # We currently allow duplicates for non-test files or test files if
+        # the manifest is listed as a duplicate.
+        for source, (dest, is_test) in obj.installs.items():
             try:
                 self._install_manifests['tests'].add_symlink(source, dest)
             except ValueError:
-                if not obj.dupe_manifest:
+                if not obj.dupe_manifest and is_test:
                     raise
 
         for base, pattern, dest in obj.pattern_installs:
             try:
                 self._install_manifests['tests'].add_pattern_symlink(base,
                     pattern, dest)
             except ValueError:
                 if not obj.dupe_manifest:
--- a/python/mozbuild/mozbuild/frontend/data.py
+++ b/python/mozbuild/mozbuild/frontend/data.py
@@ -386,17 +386,19 @@ class LibraryDefinition(SandboxDerived):
 class TestManifest(SandboxDerived):
     """Represents a manifest file containing information about tests."""
 
     __slots__ = (
         # The type of test manifest this is.
         'flavor',
 
         # Maps source filename to destination filename. The destination
-        # path is relative from the tests root directory.
+        # path is relative from the tests root directory. Values are 2-tuples
+        # of (destpath, is_test_file) where the 2nd item is True if this
+        # item represents a test file (versus a support file).
         'installs',
 
         # A list of pattern matching installs to perform. Entries are
         # (base, pattern, dest).
         'pattern_installs',
 
         # Where all files for this manifest flavor are installed in the unified
         # test package directory.
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -485,38 +485,57 @@ class TreeMetadataEmitter(LoggingMixin):
                         # We only support globbing on support-files because
                         # the harness doesn't support * for head and tail.
                         if '*' in pattern and thing == 'support-files':
                             obj.pattern_installs.append(
                                 (manifest_dir, pattern, out_dir))
                         else:
                             full = mozpath.normpath(mozpath.join(manifest_dir,
                                 pattern))
-                            # Only install paths in our directory. This
-                            # rule is somewhat arbitrary and could be lifted.
+
+                            dest_path = mozpath.join(out_dir, pattern)
+
+                            # If the path resolves to a different directory
+                            # tree, we take special behavior depending on the
+                            # entry type.
                             if not full.startswith(manifest_dir):
-                                continue
+                                # If it's a support file, we install the file
+                                # into the current destination directory.
+                                # This implementation makes installing things
+                                # with custom prefixes impossible. If this is
+                                # needed, we can add support for that via a
+                                # special syntax later.
+                                if thing == 'support-files':
+                                    dest_path = mozpath.join(out_dir,
+                                        os.path.basename(pattern))
+                                # If it's not a support file, we ignore it.
+                                # This preserves old behavior so things like
+                                # head files doesn't get installed multiple
+                                # times.
+                                else:
+                                    continue
 
-                            obj.installs[full] = mozpath.join(out_dir, pattern)
+                            obj.installs[full] = (mozpath.normpath(dest_path),
+                                False)
 
             for test in filtered:
                 obj.tests.append(test)
 
                 obj.installs[mozpath.normpath(test['path'])] = \
-                    mozpath.join(out_dir, test['relpath'])
+                    (mozpath.join(out_dir, test['relpath']), True)
 
                 process_support_files(test)
 
             if not filtered:
                 # If there are no tests, look for support-files under DEFAULT.
                 process_support_files(defaults)
 
             # We also copy the manifest into the output directory.
             out_path = mozpath.join(out_dir, mozpath.basename(manifest_path))
-            obj.installs[path] = out_path
+            obj.installs[path] = (out_path, False)
 
             # Some manifests reference files that are auto generated as
             # part of the build or shouldn't be installed for some
             # reason. Here, we prune those files from the install set.
             # FUTURE we should be able to detect autogenerated files from
             # other build metadata. Once we do that, we can get rid of this.
             for f in defaults.get('generated-files', '').split():
                 # We re-raise otherwise the stack trace isn't informative.
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest1.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = support-file.txt
+
+[test_foo.js]
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/mochitest2.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = support-file.txt
+
+[test_bar.js]
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/backend/data/test-manifests-duplicate-support-files/moz.build
@@ -0,0 +1,7 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+MOCHITEST_MANIFESTS += [
+    'mochitest1.ini',
+    'mochitest2.ini',
+]
new file mode 100644
--- a/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
+++ b/python/mozbuild/mozbuild/test/backend/test_recursivemake.py
@@ -611,16 +611,25 @@ class TestRecursiveMakeBackend(BackendTe
 
         with open(os.path.join(env.topobjdir, 'backend.mk'), 'rb') as fh:
             lines = fh.readlines()
 
         lines = [line.rstrip() for line in lines]
 
         self.assertIn('JAR_MANIFEST := %s/jar.mn' % env.topsrcdir, lines)
 
+    def test_test_manifests_duplicate_support_files(self):
+        """Ensure duplicate support-files in test manifests work."""
+        env = self._consume('test-manifests-duplicate-support-files',
+            RecursiveMakeBackend)
+
+        p = os.path.join(env.topobjdir, '_build_manifests', 'install', 'tests')
+        m = InstallManifest(p)
+        self.assertIn('testing/mochitest/tests/support-file.txt', m)
+
     def test_android_eclipse(self):
         env = self._consume('android_eclipse', RecursiveMakeBackend)
 
         with open(mozpath.join(env.topobjdir, 'backend.mk'), 'rb') as fh:
             lines = fh.readlines()
 
         lines = [line.rstrip() for line in lines]
 
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/child/mochitest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+support-files = ../support-file.txt
+
+[test_foo.js]
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/frontend/data/test-manifest-parent-support-files-dir/moz.build
@@ -0,0 +1,4 @@
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+MOCHITEST_MANIFESTS += ['child/mochitest.ini']
new file mode 100644
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -335,65 +335,65 @@ class TestEmitterBasic(unittest.TestCase
                 if isinstance(o, TestManifest)]
 
         self.assertEqual(len(objs), 6)
 
         metadata = {
             'a11y.ini': {
                 'flavor': 'a11y',
                 'installs': {
-                    'a11y.ini',
-                    'test_a11y.js',
+                    'a11y.ini': False,
+                    'test_a11y.js': True,
                 },
                 'pattern-installs': 1,
             },
             'browser.ini': {
                 'flavor': 'browser-chrome',
                 'installs': {
-                    'browser.ini',
-                    'test_browser.js',
-                    'support1',
-                    'support2',
+                    'browser.ini': False,
+                    'test_browser.js': True,
+                    'support1': False,
+                    'support2': False,
                 },
             },
             'metro.ini': {
                 'flavor': 'metro-chrome',
                 'installs': {
-                    'metro.ini',
-                    'test_metro.js',
+                    'metro.ini': False,
+                    'test_metro.js': True,
                 },
             },
             'mochitest.ini': {
                 'flavor': 'mochitest',
                 'installs': {
-                    'mochitest.ini',
-                    'test_mochitest.js',
+                    'mochitest.ini': False,
+                    'test_mochitest.js': True,
                 },
                 'external': {
                     'external1',
                     'external2',
                 },
             },
             'chrome.ini': {
                 'flavor': 'chrome',
                 'installs': {
-                    'chrome.ini',
-                    'test_chrome.js',
+                    'chrome.ini': False,
+                    'test_chrome.js': True,
                 },
             },
             'xpcshell.ini': {
                 'flavor': 'xpcshell',
                 'dupe': True,
                 'installs': {
-                    'xpcshell.ini',
-                    'test_xpcshell.js',
-                    'head1',
-                    'head2',
-                    'tail1',
-                    'tail2',
+                    'xpcshell.ini': False,
+                    'test_xpcshell.js': True,
+                    'head1': False,
+                    'head2': False,
+                    'tail1': False,
+                    'tail2': False,
                 },
             },
         }
 
         for o in objs:
             m = metadata[mozpath.basename(o.manifest_relpath)]
 
             self.assertTrue(o.path.startswith(o.directory))
@@ -402,19 +402,20 @@ class TestEmitterBasic(unittest.TestCase
 
             external_normalized = set(mozpath.basename(p) for p in
                     o.external_installs)
             self.assertEqual(external_normalized, m.get('external', set()))
 
             self.assertEqual(len(o.installs), len(m['installs']))
             for path in o.installs.keys():
                 self.assertTrue(path.startswith(o.directory))
-                path = path[len(o.directory)+1:]
+                relpath = path[len(o.directory)+1:]
 
-                self.assertIn(path, m['installs'])
+                self.assertIn(relpath, m['installs'])
+                self.assertEqual(o.installs[path][1], m['installs'][relpath])
 
             if 'pattern-installs' in m:
                 self.assertEqual(len(o.pattern_installs), m['pattern-installs'])
 
     def test_test_manifest_unmatched_generated(self):
         reader = self.reader('test-manifest-unmatched-generated')
 
         with self.assertRaisesRegexp(SandboxValidationError,
@@ -433,16 +434,32 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEqual(len(objs), 1)
 
         o = objs[0]
 
         self.assertEqual(o.flavor, 'mochitest')
         basenames = set(mozpath.basename(k) for k in o.installs.keys())
         self.assertEqual(basenames, {'mochitest.ini', 'test_active.html'})
 
+    def test_test_manifest_parent_support_files_dir(self):
+        """support-files referencing a file in a parent directory works."""
+        reader = self.reader('test-manifest-parent-support-files-dir')
+
+        objs = [o for o in self.read_topsrcdir(reader)
+                if isinstance(o, TestManifest)]
+
+        self.assertEqual(len(objs), 1)
+
+        o = objs[0]
+
+        expected = mozpath.join(o.srcdir, 'support-file.txt')
+        self.assertIn(expected, o.installs)
+        self.assertEqual(o.installs[expected],
+            ('testing/mochitest/tests/child/support-file.txt', False))
+
     def test_ipdl_sources(self):
         reader = self.reader('ipdl_sources')
         objs = self.read_topsrcdir(reader)
 
         ipdls = []
         for o in objs:
             if isinstance(o, IPDLFile):
                 ipdls.append('%s/%s' % (o.relativedir, o.basename))
--- a/services/common/tests/unit/test_utils_convert_string.js
+++ b/services/common/tests/unit/test_utils_convert_string.js
@@ -69,62 +69,54 @@ add_test(function test_bad_argument() {
     do_check_true(failed);
   }
 
   run_next_test();
 });
 
 add_task(function test_stringAsHex() {
   do_check_eq(TEST_HEX, CommonUtils.stringAsHex(TEST_STR));
-  run_next_test();
 });
 
 add_task(function test_hexAsString() {
   do_check_eq(TEST_STR, CommonUtils.hexAsString(TEST_HEX));
-  run_next_test();
 });
 
 add_task(function test_hexToBytes() {
   let bytes = CommonUtils.hexToBytes(TEST_HEX);
   do_check_eq(TEST_BYTES.length, bytes.length);
   // Ensure that the decimal values of each byte are correct
   do_check_true(arraysEqual(TEST_BYTES,
       CommonUtils.stringToByteArray(bytes)));
-  run_next_test();
 });
 
 add_task(function test_bytesToHex() {
   // Create a list of our character bytes from the reference int values
   let bytes = CommonUtils.byteArrayToString(TEST_BYTES);
   do_check_eq(TEST_HEX, CommonUtils.bytesAsHex(bytes));
-  run_next_test();
 });
 
 add_task(function test_stringToBytes() {
   do_check_true(arraysEqual(TEST_BYTES,
       CommonUtils.stringToByteArray(CommonUtils.stringToBytes(TEST_STR))));
-  run_next_test();
 });
 
 add_task(function test_stringRoundTrip() {
   do_check_eq(TEST_STR,
     CommonUtils.hexAsString(CommonUtils.stringAsHex(TEST_STR)));
-  run_next_test();
 });
 
 add_task(function test_hexRoundTrip() {
   do_check_eq(TEST_HEX,
     CommonUtils.stringAsHex(CommonUtils.hexAsString(TEST_HEX)));
-  run_next_test();
 });
 
 add_task(function test_byteArrayRoundTrip() {
   do_check_true(arraysEqual(TEST_BYTES,
     CommonUtils.stringToByteArray(CommonUtils.byteArrayToString(TEST_BYTES))));
-  run_next_test();
 });
 
 // turn formatted test vectors into normal hex strings
 function h(hexStr) {
   return hexStr.replace(/\s+/g, "");
 }
 
 function arraysEqual(a1, a2) {
--- a/services/crypto/tests/unit/test_utils_pbkdf2.js
+++ b/services/crypto/tests/unit/test_utils_pbkdf2.js
@@ -86,18 +86,16 @@ add_task(function test_pbkdf2_hmac_sha1(
      DK: h("56 fa 6a a7 55 48 09 9d"+
            "cc 37 d7 f0 34 25 e0 c3"), // (16 octets)
     },
   ];
 
   for (let v of vectors) {
     do_check_eq(v.DK, b2h(pbkdf2(v.P, v.S, v.c, v.dkLen)));
   }
-
-  run_next_test();
 });
 
 // I can't find any normative ietf test vectors for pbkdf2 hmac-sha256.
 // The following vectors are derived with the same inputs as above (the sha1
 // test).  Results verified by users here:
 // https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors
 add_task(function test_pbkdf2_hmac_sha256() {
   let pbkdf2 = CryptoUtils.pbkdf2Generate;
@@ -151,16 +149,14 @@ add_task(function test_pbkdf2_hmac_sha25
            "3c 69 62 26 65 0a 86 87"), // (16 octets)
     },
   ];
 
   for (let v of vectors) {
     do_check_eq(v.DK,
         b2h(pbkdf2(v.P, v.S, v.c, v.dkLen, Ci.nsICryptoHMAC.SHA256, 32)));
   }
-
-  run_next_test();
 });
 
 // turn formatted test vectors into normal hex strings
 function h(hexStr) {
   return hexStr.replace(/\s+/g, "");
 }
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -480,26 +480,22 @@ add_task(function test_getAssertion() {
 add_task(function test_resend_email_not_signed_in() {
   let fxa = new MockFxAccounts();
 
   try {
     yield fxa.resendVerificationEmail();
   } catch(err) {
     do_check_eq(err.message,
       "Cannot resend verification email; no signed-in user");
-    do_test_finished();
-    run_next_test();
     return;
   }
   do_throw("Should not be able to resend email when nobody is signed in");
 });
 
-add_task(function test_resend_email() {
-  do_test_pending();
-
+add_test(function test_resend_email() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
   do_check_eq(fxa.internal.generationCount, 0);
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
     log.debug("Alice signing in");
@@ -523,17 +519,16 @@ add_task(function test_resend_email() {
         // Timer was not restarted
         do_check_eq(fxa.internal.generationCount, 1);
 
         // Timer is still ticking
         do_check_true(fxa.internal.currentTimer > 0);
 
         // Ok abort polling before we go on to the next test
         fxa.internal.abortExistingFlow();
-        do_test_finished();
         run_next_test();
       });
     });
   });
 });
 
 /*
  * End of tests.
--- a/services/fxaccounts/tests/xpcshell/test_credentials.js
+++ b/services/fxaccounts/tests/xpcshell/test_credentials.js
@@ -59,18 +59,16 @@ add_task(function test_onepw_setup_crede
 
   do_check_eq(b2h(authPW), "4b8dec7f48e7852658163601ff766124c312f9392af6c3d4e1a247eb439be342");
 
   // derive unwrap key
   let unwrapKeyInfo = Credentials.keyWord('unwrapBkey');
   let unwrapKey = hkdf(quickStretchedPW, hkdfSalt, unwrapKeyInfo, hkdfLen);
 
   do_check_eq(b2h(unwrapKey), "8ff58975be391338e4ec5d7138b5ed7b65c7d1bfd1f3a4f93e05aa47d5b72be9");
-
-  run_next_test();
 });
 
 add_task(function test_client_stretch_kdf() {
   let pbkdf2 = CryptoUtils.pbkdf2Generate;
   let hkdf = CryptoUtils.hkdf;
   let expected = vectors["client stretch-KDF"];
 
   let emailUTF8 = h2s(expected.email);
@@ -98,18 +96,16 @@ add_task(function test_client_stretch_kd
   do_check_eq(passwordUTF8, results.passwordUTF8,
       "passwordUTF8 is wrong");
 
   do_check_eq(expected.quickStretchedPW, b2h(results.quickStretchedPW),
       "quickStretchedPW is wrong");
 
   do_check_eq(expected.authPW, b2h(results.authPW),
       "authPW is wrong");
-
-  run_next_test();
 });
 
 // End of tests
 // Utility functions follow
 
 function run_test() {
   run_next_test();
 }
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -1370,29 +1370,39 @@ function add_task(func) {
   _gTests.push([true, func]);
 }
 
 /**
  * Runs the next test function from the list of async tests.
  */
 let _gRunningTest = null;
 let _gTestIndex = 0; // The index of the currently running test.
+let _gTaskRunning = false;
 function run_next_test()
 {
+  if (_gTaskRunning) {
+    throw new Error("run_next_test() called from an add_task() test function. " +
+                    "run_next_test() should not be called from inside add_task() " +
+                    "under any circumstances!");
+  }
+
   function _run_next_test()
   {
     if (_gTestIndex < _gTests.length) {
       let _isTask;
       [_isTask, _gRunningTest] = _gTests[_gTestIndex++];
       print("TEST-INFO | " + _TEST_FILE + " | Starting " + _gRunningTest.name);
       do_test_pending(_gRunningTest.name);
 
       if (_isTask) {
-        _Task.spawn(_gRunningTest)
-             .then(run_next_test, do_report_unexpected_exception);
+        _gTaskRunning = true;
+        _Task.spawn(_gRunningTest).then(
+          () => { _gTaskRunning = false; run_next_test(); },
+          (ex) => { _gTaskRunning = false; do_report_unexpected_exception(ex); }
+        );
       } else {
         // Exceptions do not kill asynchronous tests, so they'll time out.
         try {
           _gRunningTest();
         } catch (e) {
           do_throw(e);
         }
       }
--- a/testing/xpcshell/selftest.py
+++ b/testing/xpcshell/selftest.py
@@ -113,16 +113,26 @@ function run_test() { run_next_test(); }
 
 add_task(function test() {
   let result = yield Promise.resolve(false);
 
   do_check_true(result);
 });
 '''
 
+ADD_TASK_RUN_NEXT_TEST = '''
+function run_test() { run_next_test(); }
+
+add_task(function () {
+  Assert.ok(true);
+
+  run_next_test();
+});
+'''
+
 ADD_TEST_THROW_STRING = '''
 function run_test() {do_throw("Passing a string to do_throw")};
 '''
 
 ADD_TEST_THROW_OBJECT = '''
 let error = {
   message: "Error object",
   fileName: "failure.js",
@@ -511,16 +521,29 @@ tail =
             ADD_TASK_FAILURE_INSIDE)
         self.writeManifest(["test_add_task_failure_inside.js"])
 
         self.assertTestResult(False)
         self.assertEquals(1, self.x.testCount)
         self.assertEquals(0, self.x.passCount)
         self.assertEquals(1, self.x.failCount)
 
+    def testAddTaskRunNextTest(self):
+        """
+        Calling run_next_test() from inside add_task() results in failure.
+        """
+        self.writeFile("test_add_task_run_next_test.js",
+            ADD_TASK_RUN_NEXT_TEST)
+        self.writeManifest(["test_add_task_run_next_test.js"])
+
+        self.assertTestResult(False)
+        self.assertEquals(1, self.x.testCount)
+        self.assertEquals(0, self.x.passCount)
+        self.assertEquals(1, self.x.failCount)
+
     def testMissingHeadFile(self):
         """
         Ensure that missing head file results in fatal error.
         """
         self.writeFile("test_basic.js", SIMPLE_PASSING_TEST)
         self.writeManifest([("test_basic.js", "head = missing.js")])
 
         raised = False
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/CrashManager.jsm", this);
+let bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 
 Cu.import("resource://testing-common/CrashManagerTest.jsm", this);
 
 const DUMMY_DATE = new Date(1391043519000);
 
@@ -36,18 +36,16 @@ add_task(function* test_constructor_inva
 });
 
 add_task(function* test_get_manager() {
   let m = yield getManager();
   Assert.ok(m, "CrashManager obtained.");
 
   yield m.createDummyDump(true);
   yield m.createDummyDump(false);
-
-  run_next_test();
 });
 
 // Unsubmitted dump files on disk are detected properly.
 add_task(function* test_pending_dumps() {
   let m = yield getManager();
   let now = Date.now();
   let ids = [];
   const COUNT = 5;
--- a/toolkit/components/viewsource/content/viewSource.xul
+++ b/toolkit/components/viewsource/content/viewSource.xul
@@ -46,20 +46,16 @@
   <command id="cmd_close" oncommand="window.close();"/>
   <commandset id="editMenuCommands"/>
   <command id="cmd_find"
            oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
   <command id="cmd_findAgain"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(false);"/>
   <command id="cmd_findPrevious"
            oncommand="document.getElementById('FindToolbar').onFindAgainCommand(true);"/>
-#ifdef XP_MACOSX
-  <command id="cmd_findSelection"
-           oncommand="document.getElementById('FindToolbar').onFindSelectionCommand();"/>
-#endif
   <command id="cmd_reload" oncommand="ViewSourceReload();"/>
   <command id="cmd_goToLine" oncommand="ViewSourceGoToLine();" disabled="true"/>
   <command id="cmd_highlightSyntax" oncommand="highlightSyntax();"/>
   <command id="cmd_wrapLongLines" oncommand="wrapLongLines()"/>
   <command id="cmd_textZoomReduce" oncommand="ZoomManager.reduce();"/>
   <command id="cmd_textZoomEnlarge" oncommand="ZoomManager.enlarge();"/>
   <command id="cmd_textZoomReset" oncommand="ZoomManager.reset();"/>
 
@@ -85,19 +81,16 @@
 
     <key id="key_reload" key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel"/>
     <key key="&reloadCmd.commandkey;" command="cmd_reload" modifiers="accel,shift"/>
     <key keycode="VK_F5" command="cmd_reload"/>
     <key keycode="VK_F5" command="cmd_reload" modifiers="accel"/>
     <key id="key_find" key="&findOnCmd.commandkey;" command="cmd_find" modifiers="accel"/>
     <key id="key_findAgain" key="&findAgainCmd.commandkey;" command="cmd_findAgain" modifiers="accel"/>
     <key id="key_findPrevious" key="&findAgainCmd.commandkey;" command="cmd_findPrevious" modifiers="accel,shift"/>
-#ifdef XP_MACOSX
-    <key id="key_findSelection" key="&findSelectionCmd.commandkey;" command="cmd_findSelection" modifiers="accel"/>
-#endif
     <key keycode="&findAgainCmd.commandkey2;" command="cmd_findAgain"/>
     <key keycode="&findAgainCmd.commandkey2;"  command="cmd_findPrevious" modifiers="shift"/>
 
     <key keycode="VK_BACK" command="Browser:Back"/>
     <key keycode="VK_BACK" command="Browser:Forward" modifiers="shift"/>
 #ifndef XP_MACOSX
     <key id="goBackKb" keycode="VK_LEFT" command="Browser:Back" modifiers="alt"/>
     <key id="goForwardKb" keycode="VK_RIGHT" command="Browser:Forward" modifiers="alt"/>
--- a/toolkit/content/tests/chrome/Makefile.in
+++ b/toolkit/content/tests/chrome/Makefile.in
@@ -1,17 +1,12 @@
 # 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/.
 
-MOCHITEST_CHROME_FILES := \
-  ../widgets/popup_shared.js \
-  ../widgets/tree_shared.js \
-  $(SHULL)
-
 # test_panel_focus.xul won't work if the Full Keyboard Access preference is set to
 # textboxes and lists only, so skip this test on Mac
 ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 MOCHITEST_CHROME_FILES += test_panel_focus.xul \
                window_panel_focus.xul \
                test_chromemargin.xul \
                window_chromemargin.xul \
                bug451540_window.xul \
--- a/toolkit/content/tests/chrome/bug409624_window.xul
+++ b/toolkit/content/tests/chrome/bug409624_window.xul
@@ -53,17 +53,16 @@
       gFindBar.open();
       let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
       if (!matchCaseCheckbox.hidden && matchCaseCheckbox.checked)
         matchCaseCheckbox.click();
       ok(!matchCaseCheckbox.checked, "case-insensitivity correctly set");
 
       // Simulate typical input
       textbox.focus();
-      gFindBar.clear();
       sendChar("m");
       ok(gFindBar.canClear, "canClear property true after input");
       let preSelection = gBrowser.contentWindow.getSelection();
       ok(!preSelection.isCollapsed, "Found item and selected range");
       gFindBar.clear();
       is(textbox.value, '', "findbar input value cleared after clear() call");
       let postSelection = gBrowser.contentWindow.getSelection();
       ok(postSelection.isCollapsed, "item found deselected after clear() call");
--- a/toolkit/content/tests/chrome/findbar_window.xul
+++ b/toolkit/content/tests/chrome/findbar_window.xul
@@ -25,19 +25,16 @@
 
     const SAMPLE_URL = "http://www.mozilla.org/";
     const SAMPLE_TEXT = "Some text in a text field.";
     const SEARCH_TEXT = "Text Test";
 
     var gFindBar = null;
     var gBrowser;
 
-    var gClipboard = Cc["@mozilla.org/widget/clipboard;1"].getService("nsIClipboard");
-    var gHasFindClipboard = gClipboard.supportsFindClipboard();
-
     var gStatusText;
     var gXULBrowserWindow = {
       QueryInterface: function(aIID) {
         if (aIID.Equals(Ci.nsIXULBrowserWindow) ||
             aIID.Equals(Ci.nsISupports))
          return this;
 
         throw Cr.NS_NOINTERFACE;
@@ -92,33 +89,28 @@
       ok(gFindBar.hidden, "findbar should be hidden after testNormalFindWithComposition");
       testAutoCaseSensitivityUI();
       testQuickFindText();
       gFindBar.close();
       ok(gFindBar.hidden, "Failed to close findbar after testQuickFindText");
       testFindbarSelection();
       testDrop();
       testQuickFindLink();
-      if (gHasFindClipboard)
-        testStatusText();
+      testStatusText();
       testQuickFindClose();
     }
 
     function testFindbarSelection() {
       function checkFindbarState(aTestName, aExpSelection) {
         document.getElementById("cmd_find").doCommand();
         ok(!gFindBar.hidden, "testFindbarSelection: failed to open findbar: " + aTestName);
         ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField,
            "testFindbarSelection: find field is not focused: " + aTestName);
-        if (gHasFindClipboard) {
-          ok(gFindBar._findField.value == aExpSelection,
-             "Incorrect selection in testFindbarSelection: "  + aTestName +
-             ". Selection: " + gFindBar._findField.value);
-        }
-        testClipboardSearchString(aExpSelection);
+        ok(gFindBar._findField.value == aExpSelection,
+           "Incorrect selection in testFindbarSelection: "  + aTestName + ". Selection: " + gFindBar._findField.value);
 
         // Clear the value, close the findbar
         gFindBar._findField.value = "";
         gFindBar.close();
       }
 
       // test normal selected text
       var cH2 = gBrowser.contentDocument.getElementById("h2");
@@ -188,24 +180,22 @@
       var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
       if (!matchCaseCheckbox.hidden & matchCaseCheckbox.checked)
         matchCaseCheckbox.click();
 
       var searchStr = "text tes";
       enterStringIntoFindField(searchStr);
       ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() == searchStr,
          "testNormalFind: failed to find '" + searchStr + "'");
-      testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
 
       if (!matchCaseCheckbox.hidden) {
         matchCaseCheckbox.click();
         enterStringIntoFindField("t");
         ok(gBrowser.contentWindow.getSelection() != searchStr,
            "testNormalFind: Case-sensitivy is broken '" + searchStr + "'");
-        testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
         matchCaseCheckbox.click();
       }
     }
 
     function testNormalFindWithComposition() {
       document.getElementById("cmd_find").doCommand();
 
       ok(!gFindBar.hidden, "testNormalFindWithComposition: findbar should be open");
@@ -233,33 +223,31 @@
               { "length": searchStr.length, "attr": COMPOSITION_ATTR_RAWINPUT }
             ]
           },
           "caret": { "start": searchStr.length, "length": 0 }
         });
 
       ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() != searchStr,
          "testNormalFindWithComposition: text shouldn't be found during composition");
-      testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
 
       synthesizeText(
         { "composition":
           { "string": searchStr,
             "clauses":
             [
               { "length": 0, "attr": 0 }
             ]
           },
           "caret": { "start": searchStr.length, "length": 0 }
         });
       synthesizeComposition({ type: "compositionend", data: searchStr });
 
       ok(gBrowser.contentWindow.getSelection().toString().toLowerCase() == searchStr,
          "testNormalFindWithComposition: text should be found after committing composition");
-      testClipboardSearchString(gBrowser.contentWindow.getSelection().toString());
 
       if (clicked) {
         matchCaseCheckbox.click();
       }
     }
 
     function testAutoCaseSensitivityUI() {
       var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
@@ -309,17 +297,16 @@
       ok(!gFindBar.hidden, "testQuickFindLink: failed to open findbar");
       ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField,
          "testQuickFindLink: find field is not focused");
 
       var searchStr = "Link Test";
       enterStringIntoFindField(searchStr);
       ok(gBrowser.contentWindow.getSelection() == searchStr,
           "testQuickFindLink: failed to find sample link");
-      testClipboardSearchString(searchStr);
     }
 
     function testQuickFindText() {
       clearFocus();
 
       var event = document.createEvent("KeyEvents");
       event.initKeyEvent("keypress", true, true, null, false, false,
                          false, false, 0, "/".charCodeAt(0));
@@ -327,28 +314,16 @@
 
       ok(!gFindBar.hidden, "testQuickFindText: failed to open findbar");
       ok(document.commandDispatcher.focusedElement == gFindBar._findField.inputField,
          "testQuickFindText: find field is not focused");
 
       enterStringIntoFindField(SEARCH_TEXT);
       ok(gBrowser.contentWindow.getSelection() == SEARCH_TEXT,
          "testQuickFindText: failed to find '" + SEARCH_TEXT + "'");
-      testClipboardSearchString(SEARCH_TEXT);
-    }
-
-    function testClipboardSearchString(aExpected) {
-      if (!gHasFindClipboard)
-        return;
-
-      if (!aExpected)
-        aExpected = "";
-      var searchStr = gFindBar.browser.finder.clipboardSearchString;
-      ok(searchStr == aExpected, "testClipboardSearchString: search string not " +
-         "set to '" + aExpected + "', instead found '" + searchStr + "'");
     }
   ]]></script>
 
   <commandset>
     <command id="cmd_find" oncommand="document.getElementById('FindToolbar').onFindCommand();"/>
   </commandset>
   <browser type="content-primary" flex="1" id="content" src="about:blank"/>
   <findbar id="FindToolbar" browserid="content"/>
--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -105,23 +105,16 @@
 
       <handler event="blur"><![CDATA[
         let findbar = this.findbar;
         // Note: This code used to remove the selection
         // if it matched an editable.
         findbar.browser.finder.enableSelection();
       ]]></handler>
 
-#ifdef XP_MACOSX
-      <handler event="focus"><![CDATA[
-        let findbar = this.findbar;
-        findbar._onFindFieldFocus();
-      ]]></handler>
-#endif
-
       <handler event="compositionstart"><![CDATA[
         // Don't close the find toolbar while IME is composing.
         let findbar = this.findbar;
         findbar._isIMEComposing = true;
         if (findbar._quickFindTimeout) {
           clearTimeout(findbar._quickFindTimeout);
           findbar._quickFindTimeout = null;
         }
@@ -1047,27 +1040,18 @@
             prefsvc.setIntPref("accessibility.typeaheadfind.flashBar",
                                --this._flashFindBar);
           }
 
           if (this.prefillWithSelection)
             userWantsPrefill =
               prefsvc.getBoolPref("accessibility.typeaheadfind.prefillwithselection");
 
-          let initialString = null;
-          if (this.prefillWithSelection && userWantsPrefill)
-            initialString = this._getInitialSelection();
-#ifdef XP_MACOSX
-          if (!initialString) {
-            let clipboardSearchString = this.browser.finder.clipboardSearchString;
-            if (clipboardSearchString)
-              initialString = clipboardSearchString;
-          }
-#endif
-
+          let initialString = (this.prefillWithSelection && userWantsPrefill) ?
+                              this._getInitialSelection() : null;
           if (initialString)
             this._findField.value = initialString;
 
           this._enableFindButtons(!!this._findField.value);
 
           this._findField.select();
           this._findField.focus();
         ]]></body>
@@ -1113,42 +1097,16 @@
           if (this._findField.value != this._browser.finder.searchString)
             this._find(this._findField.value);
           else
             this._findAgain(aFindPrevious);
 
         ]]></body>
       </method>
 
-#ifdef XP_MACOSX
-      <!--
-        - Fetches the currently selected text and sets that as the text to search
-        - next. This is a MacOS specific feature.
-      -->
-      <method name="onFindSelectionCommand">
-        <body><![CDATA[
-          let searchString = this.browser.finder.setSearchStringToSelection();
-          if (searchString)
-            this._findField.value = searchString;
-        ]]></body>
-      </method>
-
-      <method name="_onFindFieldFocus">
-        <body><![CDATA[
-          let clipboardSearchString = this._browser.finder.clipboardSearchString;
-          if (clipboardSearchString && this._findField.value != clipboardSearchString) {
-            this._findField.value = clipboardSearchString;
-            // Changing the search string makes the previous status invalid, so
-            // we better clear it here.
-            this._updateStatusUI();
-          }
-        ]]></body>
-      </method>
-#endif
-
       <!--
         - This handles all the result changes for both
         - type-ahead-find and highlighting.
         - @param aResult
         -   One of the nsITypeAheadFind.FIND_* constants
         -   indicating the result of a search operation.
         - @param aFindBackwards
         -   If the search was done from the bottom to
@@ -1156,18 +1114,16 @@
         -   when reaching "the end of the page".
         - @param aLinkURL
         -   When a link matched then its URK. Always null
         -   when not in FIND_LINKS mode.
         -->
       <method name="onFindResult">
         <parameter name="aData"/>
         <body><![CDATA[
-          if (this._findField.value != this.browser.finder.searchString)
-            this._findField.value = this.browser.finder.searchString;
           this._updateStatusUI(aData.result, aData.findBackwards);
           this._updateStatusUIBar(aData.linkURL);
 
           if (aData.result == this.nsITypeAheadFind.FIND_NOTFOUND)
             this._findFailedString = aData.searchString;
           else
             this._findFailedString = null;
 
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -431,17 +431,20 @@ EventLoop.prototype = {
  *        An optional (for content debugging only) reference to the content
  *        window.
  */
 function ThreadActor(aHooks, aGlobal)
 {
   this._state = "detached";
   this._frameActors = [];
   this._hooks = aHooks;
-  this.global = aGlobal;
+  this.global = this.globalSafe = aGlobal;
+  if (aGlobal && aGlobal.wrappedJSObject) {
+    this.global = aGlobal.wrappedJSObject;
+  }
   // A map of actorID -> actor for breakpoints created and managed by the server.
   this._hiddenBreakpoints = new Map();
 
   this.findGlobals = this.globalManager.findGlobals.bind(this);
   this.onNewGlobal = this.globalManager.onNewGlobal.bind(this);
   this.onNewSource = this.onNewSource.bind(this);
   this._allEventsListener = this._allEventsListener.bind(this);
 
@@ -1758,17 +1761,17 @@ ThreadActor.prototype = {
     this.dbg.onExceptionUnwind = undefined;
     if (aFrame) {
       aFrame.onStep = undefined;
       aFrame.onPop = undefined;
     }
     // Clear DOM event breakpoints.
     // XPCShell tests don't use actual DOM windows for globals and cause
     // removeListenerForAllEvents to throw.
-    if (this.global && !this.global.toString().contains("Sandbox")) {
+    if (this.globalSafe && !this.globalSafe.toString().contains("Sandbox")) {
       let els = Cc["@mozilla.org/eventlistenerservice;1"]
                 .getService(Ci.nsIEventListenerService);
       els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
       for (let [,bp] of this._hiddenBreakpoints) {
         bp.onDelete();
       }
       this._hiddenBreakpoints.clear();
     }
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -650,17 +650,17 @@ BrowserTabActor.prototype = {
    * up the content window for debugging.
    */
   _pushContext: function BTA_pushContext() {
     dbg_assert(!this._contextPool, "Can't push multiple contexts");
 
     this._contextPool = new ActorPool(this.conn);
     this.conn.addActorPool(this._contextPool);
 
-    this.threadActor = new ThreadActor(this, this.window.wrappedJSObject);
+    this.threadActor = new ThreadActor(this, this.window);
     this._contextPool.addActor(this.threadActor);
   },
 
   /**
    * Exits the current thread actor and removes the context-lifetime actor pool.
    * The content window is no longer being debugged after this call.
    */
   _popContext: function BTA_popContext() {
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -52,16 +52,17 @@ function createRootActor(aConnection)
   root.applicationType = "xpcshell-tests";
   return root;
 }
 
 function TestTabActor(aConnection, aGlobal)
 {
   this.conn = aConnection;
   this._global = aGlobal;
+  this._global.wrappedJSObject = aGlobal;
   this._threadActor = new ThreadActor(this, this._global);
   this.conn.addActor(this._threadActor);
   this._attached = false;
   this._extraActors = {};
 }
 
 TestTabActor.prototype = {
   constructor: TestTabActor,
--- a/toolkit/devtools/server/tests/unit/testcompatactors.js
+++ b/toolkit/devtools/server/tests/unit/testcompatactors.js
@@ -108,10 +108,11 @@ function createRootActor()
     "listTabs": actor.listTabs,
     "echo": function(aRequest) { return aRequest; },
   };
   return actor;
 }
 
 DebuggerServer.addTestGlobal = function addTestGlobal(aGlobal)
 {
+  aGlobal.wrappedJSObject = aGlobal;
   gTestGlobals.push(aGlobal);
 }
--- a/toolkit/locales/en-US/chrome/global/viewSource.dtd
+++ b/toolkit/locales/en-US/chrome/global/viewSource.dtd
@@ -66,17 +66,16 @@ you can use these alternative items. Oth
 
 <!ENTITY findOnCmd.label     "Find in This Page…">
 <!ENTITY findOnCmd.accesskey "F">
 <!ENTITY findOnCmd.commandkey "f">
 <!ENTITY findAgainCmd.label  "Find Again">
 <!ENTITY findAgainCmd.accesskey "g">
 <!ENTITY findAgainCmd.commandkey "g">
 <!ENTITY findAgainCmd.commandkey2 "VK_F3">
-<!ENTITY findSelectionCmd.commandkey "e">
 
 <!ENTITY backCmd.label "Back">
 <!ENTITY backCmd.accesskey "B">
 <!ENTITY forwardCmd.label "Forward">
 <!ENTITY forwardCmd.accesskey "F">
 <!ENTITY goBackCmd.commandKey "[">
 <!ENTITY goForwardCmd.commandKey "]">
 
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -7,26 +7,16 @@ this.EXPORTED_SYMBOLS = ["Finder"];
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Geometry.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "TextToSubURIService",
-                                         "@mozilla.org/intl/texttosuburi;1",
-                                         "nsITextToSubURI");
-XPCOMUtils.defineLazyServiceGetter(this, "Clipboard",
-                                         "@mozilla.org/widget/clipboard;1",
-                                         "nsIClipboard");
-XPCOMUtils.defineLazyServiceGetter(this, "ClipboardHelper",
-                                         "@mozilla.org/widget/clipboardhelper;1",
-                                         "nsIClipboardHelper");
-
 function Finder(docShell) {
   this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
   this._fastFind.init(docShell);
 
   this._docShell = docShell;
   this._listeners = [];
   this._previousLink = null;
   this._searchString = null;
@@ -43,86 +33,51 @@ Finder.prototype = {
   },
 
   removeResultListener: function (aListener) {
     this._listeners = this._listeners.filter(l => l != aListener);
   },
 
   _notify: function (aSearchString, aResult, aFindBackwards, aDrawOutline) {
     this._searchString = aSearchString;
-    this.clipboardSearchString = aSearchString
     this._outlineLink(aDrawOutline);
 
     let foundLink = this._fastFind.foundLink;
     let linkURL = null;
     if (foundLink) {
       let docCharset = null;
       let ownerDoc = foundLink.ownerDocument;
       if (ownerDoc)
         docCharset = ownerDoc.characterSet;
 
-      linkURL = TextToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
+      if (!this._textToSubURIService) {
+        this._textToSubURIService = Cc["@mozilla.org/intl/texttosuburi;1"]
+                                      .getService(Ci.nsITextToSubURI);
+      }
+
+      linkURL = this._textToSubURIService.unEscapeURIForUI(docCharset, foundLink.href);
     }
 
     let data = {
       result: aResult,
       findBackwards: aFindBackwards,
       linkURL: linkURL,
       rect: this._getResultRect(),
       searchString: this._searchString,
     };
 
     for (let l of this._listeners) {
       l.onFindResult(data);
     }
   },
 
   get searchString() {
-    if (!this._searchString && this._fastFind.searchString)
-      this._searchString = this._fastFind.searchString;
     return this._searchString;
   },
 
-  get clipboardSearchString() {
-    let searchString = "";
-    if (!Clipboard.supportsFindClipboard())
-      return searchString;
-
-    try {
-      let trans = Cc["@mozilla.org/widget/transferable;1"]
-                    .createInstance(Ci.nsITransferable);
-      trans.init(this._getWindow()
-                     .QueryInterface(Ci.nsIInterfaceRequestor)
-                     .getInterface(Ci.nsIWebNavigation)
-                     .QueryInterface(Ci.nsILoadContext));
-      trans.addDataFlavor("text/unicode");
-
-      Clipboard.getData(trans, Ci.nsIClipboard.kFindClipboard);
-
-      let data = {};
-      let dataLen = {};
-      trans.getTransferData("text/unicode", data, dataLen);
-      if (data.value) {
-        data = data.value.QueryInterface(Ci.nsISupportsString);
-        searchString = data.toString();
-      }
-    } catch (ex) {}
-
-    return searchString;
-  },
-
-  set clipboardSearchString(aSearchString) {
-    if (!aSearchString || !Clipboard.supportsFindClipboard())
-      return;
-
-    ClipboardHelper.copyStringToClipboard(aSearchString,
-                                          Ci.nsIClipboard.kFindClipboard,
-                                          this._getWindow().document);
-  },
-
   set caseSensitive(aSensitive) {
     this._fastFind.caseSensitive = aSensitive;
   },
 
   /**
    * Used for normal search operations, highlights the first match.
    *
    * @param aSearchString String to search for.
@@ -145,35 +100,16 @@ Finder.prototype = {
    * @param aDrawOutline Puts an outline around matched links.
    */
   findAgain: function (aFindBackwards, aLinksOnly, aDrawOutline) {
     let result = this._fastFind.findAgain(aFindBackwards, aLinksOnly);
     let searchString = this._fastFind.searchString;
     this._notify(searchString, result, aFindBackwards, aDrawOutline);
   },
 
-  /**
-   * Forcibly set the search string of the find clipboard to the currently
-   * selected text in the window, on supported platforms (i.e. OSX).
-   */
-  setSearchStringToSelection: function() {
-    // Find the selected text.
-    let selection = this._getWindow().getSelection();
-    // Don't go for empty selections.
-    if (!selection.rangeCount)
-      return null;
-    let searchString = (selection.toString() || "").trim();
-    // Empty strings are rather useless to search for.
-    if (!searchString.length)
-      return null;
-
-    this.clipboardSearchString = searchString;
-    return searchString;
-  },
-
   highlight: function (aHighlight, aWord) {
     let found = this._highlight(aHighlight, aWord, null);
     if (aHighlight) {
       let result = found ? Ci.nsITypeAheadFind.FIND_FOUND
                          : Ci.nsITypeAheadFind.FIND_NOTFOUND;
       this._notify(aWord, result, false, false);
     }
   },
--- a/widget/android/nsClipboard.cpp
+++ b/widget/android/nsClipboard.cpp
@@ -104,14 +104,8 @@ nsClipboard::HasDataMatchingFlavors(cons
 
 NS_IMETHODIMP
 nsClipboard::SupportsSelectionClipboard(bool *aIsSupported)
 {
   *aIsSupported = false;
   return NS_OK;
 }
 
-NS_IMETHODIMP
-nsClipboard::SupportsFindClipboard(bool* _retval)
-{
-  *_retval = false;
-  return NS_OK;
-}
--- a/widget/cocoa/nsClipboard.h
+++ b/widget/cocoa/nsClipboard.h
@@ -18,33 +18,27 @@ class nsClipboard : public nsBaseClipboa
 
 public:
   nsClipboard();
   virtual ~nsClipboard();
 
   // nsIClipboard  
   NS_IMETHOD HasDataMatchingFlavors(const char** aFlavorList, uint32_t aLength,
                                     int32_t aWhichClipboard, bool *_retval);
-  NS_IMETHOD SupportsFindClipboard(bool *_retval);
 
   // Helper methods, used also by nsDragService
   static NSDictionary* PasteboardDictFromTransferable(nsITransferable *aTransferable);
   static bool IsStringType(const nsCString& aMIMEType, NSString** aPasteboardType);
   static NSString* WrapHtmlForSystemPasteboard(NSString* aString);
   static nsresult TransferableFromPasteboard(nsITransferable *aTransferable, NSPasteboard *pboard);
 
 protected:
 
   // impelement the native clipboard behavior
   NS_IMETHOD SetNativeClipboardData(int32_t aWhichClipboard);
   NS_IMETHOD GetNativeClipboardData(nsITransferable * aTransferable, int32_t aWhichClipboard);
   
 private:
-  // This is always set to the native change count after any modification of the
-  // general clipboard.
-  int mChangeCountGeneral;
-  // This is always set to the native change count after any modification of the
-  // find clipboard.
-  int mChangeCountFind;
+  int mChangeCount; // this is always set to the native change count after any clipboard modifications
 
 };
 
 #endif // nsClipboard_h_
--- a/widget/cocoa/nsClipboard.mm
+++ b/widget/cocoa/nsClipboard.mm
@@ -30,18 +30,17 @@
 #ifdef PR_LOGGING
 extern PRLogModuleInfo* sCocoaLog;
 #endif
 
 extern void EnsureLogInitialized();
 
 nsClipboard::nsClipboard() : nsBaseClipboard()
 {
-  mChangeCountGeneral = 0;
-  mChangeCountFind = 0;
+  mChangeCount = 0;
 
   EnsureLogInitialized();
 }
 
 nsClipboard::~nsClipboard()
 {
 }
 
@@ -61,63 +60,47 @@ GetDataFromPasteboard(NSPasteboard* aPas
   return data;
 }
 
 NS_IMETHODIMP
 nsClipboard::SetNativeClipboardData(int32_t aWhichClipboard)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard) || !mTransferable)
+  if ((aWhichClipboard != kGlobalClipboard) || !mTransferable)
     return NS_ERROR_FAILURE;
 
   mIgnoreEmptyNotification = true;
 
   NSDictionary* pasteboardOutputDict = PasteboardDictFromTransferable(mTransferable);
   if (!pasteboardOutputDict)
     return NS_ERROR_FAILURE;
 
+  // write everything out to the general pasteboard
   unsigned int outputCount = [pasteboardOutputDict count];
   NSArray* outputKeys = [pasteboardOutputDict allKeys];
-  NSPasteboard* cocoaPasteboard;
-  if (aWhichClipboard == kFindClipboard) {
-    cocoaPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard];
-    [cocoaPasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
-  } else {
-    // Write everything else out to the general pasteboard.
-    cocoaPasteboard = [NSPasteboard generalPasteboard];
-    [cocoaPasteboard declareTypes:outputKeys owner:nil];
-  }
-
+  NSPasteboard* generalPBoard = [NSPasteboard generalPasteboard];
+  [generalPBoard declareTypes:outputKeys owner:nil];
   for (unsigned int i = 0; i < outputCount; i++) {
     NSString* currentKey = [outputKeys objectAtIndex:i];
     id currentValue = [pasteboardOutputDict valueForKey:currentKey];
-    if (aWhichClipboard == kFindClipboard) {
-      if (currentKey == NSStringPboardType)
-        [cocoaPasteboard setString:currentValue forType:currentKey];
+    if (currentKey == NSStringPboardType ||
+        currentKey == kCorePboardType_url ||
+        currentKey == kCorePboardType_urld ||
+        currentKey == kCorePboardType_urln) {
+      [generalPBoard setString:currentValue forType:currentKey];
+    } else if (currentKey == NSHTMLPboardType) {
+      [generalPBoard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
+                       forType:currentKey];
     } else {
-      if (currentKey == NSStringPboardType ||
-          currentKey == kCorePboardType_url ||
-          currentKey == kCorePboardType_urld ||
-          currentKey == kCorePboardType_urln) {
-        [cocoaPasteboard setString:currentValue forType:currentKey];
-      } else if (currentKey == NSHTMLPboardType) {
-        [cocoaPasteboard setString:(nsClipboard::WrapHtmlForSystemPasteboard(currentValue))
-                         forType:currentKey];
-      } else {
-        [cocoaPasteboard setData:currentValue forType:currentKey];
-      }
+      [generalPBoard setData:currentValue forType:currentKey];
     }
   }
 
-  if (aWhichClipboard == kFindClipboard) {
-    mChangeCountFind = [cocoaPasteboard changeCount];
-  } else {
-    mChangeCountGeneral = [cocoaPasteboard changeCount];
-  }
+  mChangeCount = [generalPBoard changeCount];
 
   mIgnoreEmptyNotification = false;
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
@@ -250,41 +233,35 @@ nsClipboard::TransferableFromPasteboard(
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 NS_IMETHODIMP
 nsClipboard::GetNativeClipboardData(nsITransferable* aTransferable, int32_t aWhichClipboard)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  if ((aWhichClipboard != kGlobalClipboard && aWhichClipboard != kFindClipboard) || !aTransferable)
+  if ((aWhichClipboard != kGlobalClipboard) || !aTransferable)
     return NS_ERROR_FAILURE;
 
-  NSPasteboard* cocoaPasteboard;
-  if (aWhichClipboard == kFindClipboard) {
-    cocoaPasteboard = [NSPasteboard pasteboardWithName:NSFindPboard];
-  } else {
-    cocoaPasteboard = [NSPasteboard generalPasteboard];
-  }
+  NSPasteboard* cocoaPasteboard = [NSPasteboard generalPasteboard];
   if (!cocoaPasteboard)
     return NS_ERROR_FAILURE;
 
   // get flavor list that includes all acceptable flavors (including ones obtained through conversion)
   nsCOMPtr<nsISupportsArray> flavorList;
   nsresult rv = aTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavorList));
   if (NS_FAILED(rv))
     return NS_ERROR_FAILURE;
 
   uint32_t flavorCount;
   flavorList->Count(&flavorCount);
 
-  int changeCount = (aWhichClipboard == kFindClipboard) ? mChangeCountFind : mChangeCountGeneral;
   // If we were the last ones to put something on the pasteboard, then just use the cached
   // transferable. Otherwise clear it because it isn't relevant any more.
-  if (changeCount == [cocoaPasteboard changeCount]) {
+  if (mChangeCount == [cocoaPasteboard changeCount]) {
     if (mTransferable) {
       for (uint32_t i = 0; i < flavorCount; i++) {
         nsCOMPtr<nsISupports> genericFlavor;
         flavorList->GetElementAt(i, getter_AddRefs(genericFlavor));
         nsCOMPtr<nsISupportsCString> currentFlavor(do_QueryInterface(genericFlavor));
         if (!currentFlavor)
           continue;
 
@@ -295,18 +272,19 @@ nsClipboard::GetNativeClipboardData(nsIT
         uint32_t dataSize = 0;
         rv = mTransferable->GetTransferData(flavorStr, getter_AddRefs(dataSupports), &dataSize);
         if (NS_SUCCEEDED(rv)) {
           aTransferable->SetTransferData(flavorStr, dataSupports, dataSize);
           return NS_OK; // maybe try to fill in more types? Is there a point?
         }
       }
     }
-  } else {
-    nsBaseClipboard::EmptyClipboard(aWhichClipboard);
+  }
+  else {
+    nsBaseClipboard::EmptyClipboard(kGlobalClipboard);
   }
 
   // at this point we can't satisfy the request from cache data so let's look
   // for things other people put on the system clipboard
 
   return nsClipboard::TransferableFromPasteboard(aTransferable, cocoaPasteboard);
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
@@ -375,24 +353,16 @@ nsClipboard::HasDataMatchingFlavors(cons
     }
   }
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
-NS_IMETHODIMP
-nsClipboard::SupportsFindClipboard(bool *_retval)
-{
-  NS_ENSURE_ARG_POINTER(_retval);
-  *_retval = true;
-  return NS_OK;
-}
-
 // This function converts anything that other applications might understand into the system format
 // and puts it into a dictionary which it returns.
 // static
 NSDictionary* 
 nsClipboard::PasteboardDictFromTransferable(nsITransferable* aTransferable)
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
 
--- a/widget/gtk/nsClipboard.cpp
+++ b/widget/gtk/nsClipboard.cpp
@@ -443,23 +443,16 @@ nsClipboard::HasDataMatchingFlavors(cons
 
 NS_IMETHODIMP
 nsClipboard::SupportsSelectionClipboard(bool *_retval)
 {
     *_retval = true; // yeah, unix supports the selection clipboard
     return NS_OK;
 }
 
-NS_IMETHODIMP
-nsClipboard::SupportsFindClipboard(bool* _retval)
-{
-  *_retval = false;
-  return NS_OK;
-}
-
 /* static */
 GdkAtom
 nsClipboard::GetSelectionAtom(int32_t aWhichClipboard)
 {
     if (aWhichClipboard == kGlobalClipboard)
         return GDK_SELECTION_CLIPBOARD;
 
     return GDK_SELECTION_PRIMARY;
--- a/widget/nsIClipboard.idl
+++ b/widget/nsIClipboard.idl
@@ -6,22 +6,21 @@
 
 
 #include "nsISupports.idl"
 #include "nsITransferable.idl"
 #include "nsIClipboardOwner.idl"
 
 interface nsIArray;
 
-[scriptable, uuid(ceaa0047-647f-4b8e-ad1c-aff9fa62aa51)]
+[scriptable, uuid(38984945-8674-4d04-b786-5c0ca9434457)]
 interface nsIClipboard : nsISupports
 {
     const long kSelectionClipboard = 0;
     const long kGlobalClipboard = 1;
-    const long kFindClipboard = 2;
     
    /**
     * Given a transferable, set the data on the native clipboard
     *
     * @param  aTransferable The transferable
     * @param  anOwner The owner of the transferable
     * @param  aWhichClipboard Specifies the clipboard to which this operation applies.
     * @result NS_Ok if no errors
@@ -69,22 +68,14 @@ interface nsIClipboard : nsISupports
    /**
     * Allows clients to determine if the implementation supports the concept of a 
     * separate clipboard for selection.
     * 
     * @outResult - true if 
     * @result NS_OK if successful.
     */
     boolean supportsSelectionClipboard ( ) ;
-
-    /**
-    * Allows clients to determine if the implementation supports the concept of a
-    * separate clipboard for find search strings.
-    *
-    * @result NS_OK if successful.
-    */
-    boolean supportsFindClipboard ( ) ;
 };
 
 
 %{ C++
 
 %}
--- a/widget/qt/nsClipboard.cpp
+++ b/widget/qt/nsClipboard.cpp
@@ -551,17 +551,8 @@ nsClipboard::SupportsSelectionClipboard(
     }
     else
     {
         *_retval = false;
     }
 
     return NS_OK;
 }
-
-NS_IMETHODIMP
-nsClipboard::SupportsFindClipboard(bool* _retval)
-{
-  NS_ENSURE_ARG_POINTER(_retval);
-
-  *_retval = false;
-  return NS_OK;
-}
--- a/widget/xpwidgets/nsBaseClipboard.cpp
+++ b/widget/xpwidgets/nsBaseClipboard.cpp
@@ -17,17 +17,16 @@ nsBaseClipboard::nsBaseClipboard()
   mIgnoreEmptyNotification = false;
   mEmptyingForSetData      = false;
 }
 
 nsBaseClipboard::~nsBaseClipboard()
 {
   EmptyClipboard(kSelectionClipboard);
   EmptyClipboard(kGlobalClipboard);
-  EmptyClipboard(kFindClipboard);
 }
 
 NS_IMPL_ISUPPORTS1(nsBaseClipboard, nsIClipboard)
 
 /**
   * Sets the transferable object
   *
   */
@@ -35,19 +34,17 @@ NS_IMETHODIMP nsBaseClipboard::SetData(n
                                         int32_t aWhichClipboard)
 {
   NS_ASSERTION ( aTransferable, "clipboard given a null transferable" );
 
   if (aTransferable == mTransferable && anOwner == mClipboardOwner)
     return NS_OK;
   bool selectClipPresent;
   SupportsSelectionClipboard(&selectClipPresent);
-  bool findClipPresent;
-  SupportsFindClipboard(&findClipPresent);
-  if ( !selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard )
+  if ( !selectClipPresent && aWhichClipboard != kGlobalClipboard )
     return NS_ERROR_FAILURE;
 
   mEmptyingForSetData = true;
   EmptyClipboard(aWhichClipboard);
   mEmptyingForSetData = false;
 
   mClipboardOwner = anOwner;
   if ( anOwner )
@@ -73,37 +70,33 @@ NS_IMETHODIMP nsBaseClipboard::SetData(n
 
 /**
   * Gets the transferable object
   *
   */
 NS_IMETHODIMP nsBaseClipboard::GetData(nsITransferable * aTransferable, int32_t aWhichClipboard)
 {
   NS_ASSERTION ( aTransferable, "clipboard given a null transferable" );
-
+  
   bool selectClipPresent;
   SupportsSelectionClipboard(&selectClipPresent);
-  bool findClipPresent;
-  SupportsFindClipboard(&findClipPresent);
-  if ( !selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard )
+  if ( !selectClipPresent && aWhichClipboard != kGlobalClipboard )
     return NS_ERROR_FAILURE;
 
   if ( aTransferable )
     return GetNativeClipboardData(aTransferable, aWhichClipboard);
 
   return NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP nsBaseClipboard::EmptyClipboard(int32_t aWhichClipboard)
 {
   bool selectClipPresent;
   SupportsSelectionClipboard(&selectClipPresent);
-  bool findClipPresent;
-  SupportsFindClipboard(&findClipPresent);
-  if ( !selectClipPresent && !findClipPresent && aWhichClipboard != kGlobalClipboard )
+  if ( !selectClipPresent && aWhichClipboard != kGlobalClipboard )
     return NS_ERROR_FAILURE;
 
   if (mIgnoreEmptyNotification)
     return NS_OK;
 
   if ( mClipboardOwner ) {
     mClipboardOwner->LosingOwnership(mTransferable);
     NS_RELEASE(mClipboardOwner);
@@ -125,15 +118,8 @@ nsBaseClipboard::HasDataMatchingFlavors(
 }
 
 NS_IMETHODIMP
 nsBaseClipboard::SupportsSelectionClipboard(bool* _retval)
 {
   *_retval = false;   // we don't support the selection clipboard by default.
   return NS_OK;
 }
-
-NS_IMETHODIMP
-nsBaseClipboard::SupportsFindClipboard(bool* _retval)
-{
-  *_retval = false;   // we don't support the find clipboard by default.
-  return NS_OK;
-}
--- a/widget/xpwidgets/nsClipboardHelper.cpp
+++ b/widget/xpwidgets/nsClipboardHelper.cpp
@@ -46,35 +46,26 @@ nsClipboardHelper::CopyStringToClipboard
   nsresult rv;
 
   // get the clipboard
   nsCOMPtr<nsIClipboard>
     clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(clipboard, NS_ERROR_FAILURE);
 
-  bool clipboardSupported;
   // don't go any further if they're asking for the selection
   // clipboard on a platform which doesn't support it (i.e., unix)
   if (nsIClipboard::kSelectionClipboard == aClipboardID) {
+    bool clipboardSupported;
     rv = clipboard->SupportsSelectionClipboard(&clipboardSupported);
     NS_ENSURE_SUCCESS(rv, rv);
     if (!clipboardSupported)
       return NS_ERROR_FAILURE;
   }
 
-  // don't go any further if they're asking for the find clipboard on a platform
-  // which doesn't support it (i.e., non-osx)
-  if (nsIClipboard::kFindClipboard == aClipboardID) {
-    rv = clipboard->SupportsFindClipboard(&clipboardSupported);
-    NS_ENSURE_SUCCESS(rv, rv);
-    if (!clipboardSupported)
-      return NS_ERROR_FAILURE;
-  }
-
   // create a transferable for putting data on the clipboard
   nsCOMPtr<nsITransferable>
     trans(do_CreateInstance("@mozilla.org/widget/transferable;1", &rv));
   NS_ENSURE_SUCCESS(rv, rv);
   NS_ENSURE_TRUE(trans, NS_ERROR_FAILURE);
 
   nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDocument);
   nsILoadContext* loadContext = doc ? doc->GetLoadContext() : nullptr;
--- a/widget/xpwidgets/nsClipboardProxy.cpp
+++ b/widget/xpwidgets/nsClipboardProxy.cpp
@@ -86,15 +86,8 @@ nsClipboardProxy::HasDataMatchingFlavors
 
 NS_IMETHODIMP
 nsClipboardProxy::SupportsSelectionClipboard(bool *aIsSupported)
 {
   *aIsSupported = false;
   return NS_OK;
 }
 
-
-NS_IMETHODIMP
-nsClipboardProxy::SupportsFindClipboard(bool *aIsSupported)
-{
-  *aIsSupported = false;
-  return NS_OK;
-}