Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 25 Feb 2014 15:20:54 -0500
changeset 170482 22650589a724b5a4b875996865ce71c288d875b9
parent 170404 f0bfbe2caf468e1fcd4b3ed20cfe80b290ba9549 (current diff)
parent 170481 4f3ea4de80fccd03966b3798ddc804c8e0dbf34e (diff)
child 170491 c349f7cdd5c1c850c30ee768797350f3200ff42d
child 170495 b6fbeba807c725bc0179c75bae2e136cde0b2e99
child 170552 d11a993d78468f867ee21093d81dadae8404d750
push id26288
push userryanvm@gmail.com
push dateTue, 25 Feb 2014 20:20:43 +0000
treeherdermozilla-central@22650589a724 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
browser/app/profile/firefox.js
browser/base/content/test/general/browser.ini
python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/Builder.launch
python/mozbuild/mozbuild/backend/templates/android_eclipse/build.xml
services/common/hawk.js
services/common/tests/unit/test_hawk.js
--- a/b2g/components/FxAccountsMgmtService.jsm
+++ b/b2g/components/FxAccountsMgmtService.jsm
@@ -34,17 +34,17 @@ this.FxAccountsMgmtService = {
   _sendChromeEvent: function(aEventName, aMsg) {
     if (!this._shell) {
       return;
     }
     log.debug("Chrome event " + JSON.stringify(aMsg));
     this._shell.sendCustomEvent(aEventName, aMsg);
   },
 
-  _onFullfill: function(aMsgId, aData) {
+  _onFulfill: function(aMsgId, aData) {
     this._sendChromeEvent("mozFxAccountsChromeEvent", {
       id: aMsgId,
       data: aData ? aData : null
     });
   },
 
   _onReject: function(aMsgId, aReason) {
     this._sendChromeEvent("mozFxAccountsChromeEvent", {
@@ -95,49 +95,49 @@ this.FxAccountsMgmtService = {
       return;
     }
 
     switch(data.method) {
       case "getAccounts":
         FxAccountsManager.getAccount().then(
           account => {
             // We only expose the email and verification status so far.
-            self._onFullfill(msg.id, account);
+            self._onFulfill(msg.id, account);
           },
           reason => {
             self._onReject(msg.id, reason);
           }
         ).then(null, Components.utils.reportError);
         break;
       case "logout":
         FxAccountsManager.signOut().then(
           () => {
-            self._onFullfill(msg.id);
+            self._onFulfill(msg.id);
           },
           reason => {
             self._onReject(msg.id, reason);
           }
         ).then(null, Components.utils.reportError);
         break;
       case "queryAccount":
         FxAccountsManager.queryAccount(data.accountId).then(
           result => {
-            self._onFullfill(msg.id, result);
+            self._onFulfill(msg.id, result);
           },
           reason => {
             self._onReject(msg.id, reason);
           }
         ).then(null, Components.utils.reportError);
         break;
       case "signIn":
       case "signUp":
       case "refreshAuthentication":
         FxAccountsManager[data.method](data.accountId, data.password).then(
           user => {
-            self._onFullfill(msg.id, user);
+            self._onFulfill(msg.id, user);
           },
           reason => {
             self._onReject(msg.id, reason);
           }
         ).then(null, Components.utils.reportError);
         break;
     }
   }
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/unit/test_fxaccounts.js
@@ -0,0 +1,184 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://testing-common/httpd.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsMgmtService",
+                                  "resource://gre/modules/FxAccountsMgmtService.jsm",
+                                  "FxAccountsMgmtService");
+
+// At end of test, restore original state
+const ORIGINAL_AUTH_URI = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
+const ORIGINAL_SHELL = FxAccountsMgmtService._shell;
+do_register_cleanup(function() {
+  Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI);
+  FxAccountsMgmtService._shell = ORIGINAL_SHELL;
+});
+
+// Make profile available so that fxaccounts can store user data
+do_get_profile();
+
+// Mock the b2g shell; make message passing possible
+let mockShell = {
+  sendCustomEvent: function(aEventName, aMsg) {
+    Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null);
+  },
+};
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function test_overall() {
+  do_check_neq(FxAccountsMgmtService, null);
+});
+
+// Check that invalid email capitalization is corrected on signIn.
+// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin
+add_test(function test_invalidEmailCase_signIn() {
+  do_test_pending();
+  let clientEmail = "greta.garbo@gmail.com";
+  let canonicalEmail = "Greta.Garbo@gmail.COM";
+  let attempts = 0;
+
+  function writeResp(response, msg) {
+    if (typeof msg === "object") {
+      msg = JSON.stringify(msg);
+    }
+    response.bodyOutputStream.write(msg, msg.length);
+  }
+
+  // Mock of the fxa accounts auth server, reproducing the behavior of
+  // /account/login when email capitalization is incorrect on signIn.
+  let server = httpd_setup({
+    "/account/login": function(request, response) {
+      response.setHeader("Content-Type", "application/json");
+      attempts += 1;
+
+      // Ensure we don't get in an endless loop
+      if (attempts > 2) {
+        response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance");
+        writeResp(response, {});
+        return;
+      }
+
+      let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+      let jsonBody = JSON.parse(body);
+      let email = jsonBody.email;
+
+      // The second time through, the accounts client will call the api with
+      // the correct email capitalization.
+      if (email == canonicalEmail) {
+        response.setStatusLine(request.httpVersion, 200, "Yay");
+        writeResp(response, {
+          uid: "your-uid",
+          sessionToken: "your-sessionToken",
+          keyFetchToken: "your-keyFetchToken",
+          verified: true,
+          authAt: 1392144866,
+        });
+        return;
+      }
+
+      // If the client has the wrong case on the email, we return a 400, with
+      // the capitalization of the email as saved in the accounts database.
+      response.setStatusLine(request.httpVersion, 400, "Incorrect email case");
+      writeResp(response, {
+        code: 400,
+        errno: 120,
+        error: "Incorrect email case",
+        email: canonicalEmail,
+      });
+      return;
+    },
+  });
+
+  // Point the FxAccountsClient's hawk rest request client to the mock server
+  Services.prefs.setCharPref("identity.fxaccounts.auth.uri", server.baseURI);
+
+  // Receive a mozFxAccountsChromeEvent message
+  function onMessage(subject, topic, data) {
+    let message = subject.wrappedJSObject;
+
+    switch (message.id) {
+      // When we signed in as "Greta.Garbo", the server should have told us
+      // that the proper capitalization is really "greta.garbo".  Call
+      // getAccounts to get the signed-in user and ensure that the
+      // capitalization is correct.
+      case "signIn":
+        FxAccountsMgmtService.handleEvent({
+          detail: {
+            id: "getAccounts",
+            data: {
+              method: "getAccounts",
+            }
+          }
+        });
+        break;
+
+      // Having initially signed in as "Greta.Garbo", getAccounts should show
+      // us that the signed-in user has the properly-capitalized email,
+      // "greta.garbo".
+      case "getAccounts":
+        Services.obs.removeObserver(onMessage, "mozFxAccountsChromeEvent");
+
+        do_check_eq(message.data.accountId, canonicalEmail);
+
+        do_test_finished();
+        server.stop(run_next_test);
+        break;
+
+      // We should not receive any other mozFxAccountsChromeEvent messages
+      default:
+        do_throw("wat!");
+        break;
+    }
+  }
+
+  Services.obs.addObserver(onMessage, "mozFxAccountsChromeEvent", false);
+
+  FxAccountsMgmtService._shell = mockShell;
+
+  // Trigger signIn using an email with incorrect capitalization
+  FxAccountsMgmtService.handleEvent({
+    detail: {
+      id: "signIn",
+      data: {
+        method: "signIn",
+        accountId: clientEmail,
+        password: "123456",
+      },
+    },
+  });
+});
+
+// End of tests
+// Utility functions follow
+
+function httpd_setup (handlers, port=-1) {
+  let server = new HttpServer();
+  for (let path in handlers) {
+    server.registerPathHandler(path, handlers[path]);
+  }
+  try {
+    server.start(port);
+  } catch (ex) {
+    dump("ERROR starting server on port " + port + ".  Already a process listening?");
+    do_throw(ex);
+  }
+
+  // Set the base URI for convenience.
+  let i = server.identity;
+  server.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort;
+
+  return server;
+}
+
+
--- a/b2g/components/test/unit/xpcshell.ini
+++ b/b2g/components/test/unit/xpcshell.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 head =
 tail =
 
 [test_bug793310.js]
 
 [test_bug832946.js]
 
+[test_fxaccounts.js]
 [test_signintowebsite.js]
 head = head_identity.js
 tail =
 
 
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -949,16 +949,19 @@ pref("toolkit.crashreporter.pluginHangSu
 
 // URL for "Learn More" for Crash Reporter
 pref("toolkit.crashreporter.infoURL",
      "https://www.mozilla.org/legal/privacy/firefox.html#crash-reporter");
 
 // base URL for web-based support pages
 pref("app.support.baseURL", "https://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/");
 
+// base url for web-based feedback pages
+pref("app.feedback.baseURL", "https://input.mozilla.org/%LOCALE%/feedback/%APP%/%VERSION%/");
+
 // Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
 pref("security.alternate_certificate_error_page", "certerror");
 
 // Whether to start the private browsing mode at application startup
 pref("browser.privatebrowsing.autostart", false);
 
 // Don't try to alter this pref, it'll be reset the next time you use the
 // bookmarking dialog
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1056,16 +1056,26 @@ let BookmarkingUI = {
     this._popupNeedsUpdate = true;
   },
 
   onPopupShowing: function BUI_onPopupShowing(event) {
     // Don't handle events for submenus.
     if (event.target != event.currentTarget)
       return;
 
+    // Ideally this code would never be reached, but if you click the outer
+    // button's border, some cpp code for the menu button's so-called XBL binding
+    // decides to open the popup even though the dropmarker is invisible.
+    if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
+      this._showSubview();
+      event.preventDefault();
+      event.stopPropagation();
+      return;
+    }
+
     let widget = CustomizableUI.getWidget("bookmarks-menu-button")
                                .forWindow(window);
     if (widget.overflowed) {
       // Don't open a popup in the overflow popup, rather just open the Library.
       event.preventDefault();
       widget.node.removeAttribute("closemenu");
       PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
       return;
@@ -1341,35 +1351,40 @@ let BookmarkingUI = {
     this._notificationTimeout = setTimeout( () => {
       this.notifier.removeAttribute("notification");
       this.notifier.removeAttribute("in-bookmarks-toolbar");
       this.button.removeAttribute("notification");
       this.notifier.style.transform = '';
     }, 1000);
   },
 
+  _showSubview: function() {
+    let view = document.getElementById("PanelUI-bookmarks");
+    view.addEventListener("ViewShowing", this);
+    view.addEventListener("ViewHiding", this);
+    let anchor = document.getElementById("bookmarks-menu-button");
+    anchor.setAttribute("closemenu", "none");
+    PanelUI.showSubView("PanelUI-bookmarks", anchor,
+                        CustomizableUI.AREA_PANEL);
+  },
+
   onCommand: function BUI_onCommand(aEvent) {
     if (aEvent.target != aEvent.currentTarget) {
       return;
     }
 
     // Handle special case when the button is in the panel.
-    let widget = CustomizableUI.getWidget("bookmarks-menu-button")
-                               .forWindow(window);
     let isBookmarked = this._itemIds.length > 0;
 
     if (this._currentAreaType == CustomizableUI.TYPE_MENU_PANEL) {
-      let view = document.getElementById("PanelUI-bookmarks");
-      view.addEventListener("ViewShowing", this);
-      view.addEventListener("ViewHiding", this);
-      widget.node.setAttribute("closemenu", "none");
-      PanelUI.showSubView("PanelUI-bookmarks", widget.node,
-                          CustomizableUI.AREA_PANEL);
+      this._showSubview();
       return;
     }
+    let widget = CustomizableUI.getWidget("bookmarks-menu-button")
+                               .forWindow(window);
     if (widget.overflowed) {
       // Allow to close the panel if the page is already bookmarked, cause
       // we are going to open the edit bookmark panel.
       if (isBookmarked)
         widget.node.removeAttribute("closemenu");
       else
         widget.node.setAttribute("closemenu", "none");
     }
--- a/browser/base/content/browser-tabview.js
+++ b/browser/base/content/browser-tabview.js
@@ -416,28 +416,26 @@ let TabView = {
   },
 
   // ----------
   // Function: _addToolbarButton
   // Adds the TabView button to the TabsToolbar.
   _addToolbarButton: function TabView__addToolbarButton() {
     let buttonId = "tabview-button";
 
-    if (document.getElementById(buttonId))
+    if (CustomizableUI.getPlacementOfWidget(buttonId))
       return;
 
-    let toolbar = document.getElementById("TabsToolbar");
-    let currentSet = toolbar.currentSet.split(",");
-    let alltabsPos = currentSet.indexOf("alltabs-button");
-    if (-1 == alltabsPos)
-      return;
-
-    let allTabsBtn = document.getElementById("alltabs-button");
-    let nextItem = allTabsBtn.nextSibling;
-    toolbar.insertItem(buttonId, nextItem);
+    let allTabsBtnPlacement = CustomizableUI.getPlacementOfWidget("alltabs-button");
+    // allTabsBtnPlacement can never be null because the button isn't removable
+    let desiredPosition = allTabsBtnPlacement.position + 1;
+    CustomizableUI.addWidgetToArea(buttonId, "TabsToolbar", desiredPosition);
+    // NB: this is for backwards compatibility, and should be removed by
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=976041
+    document.persist("TabsToolbar", "currentset");
   },
 
   // ----------
   // Function: updateGroupNumberBroadcaster
   // Updates the group number broadcaster.
   updateGroupNumberBroadcaster: function TabView_updateGroupNumberBroadcaster(number) {
     let groupsNumber = document.getElementById("tabviewGroupsNumber");
     groupsNumber.setAttribute("groups", number);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4238,26 +4238,28 @@ function onViewToolbarsPopupShowing(aEve
     return;
   }
 
   // triggerNode can be a nested child element of a toolbaritem.
   let toolbarItem = popup.triggerNode;
 
   if (toolbarItem && toolbarItem.localName == "toolbarpaletteitem") {
     toolbarItem = toolbarItem.firstChild;
-  } else {
+  } else if (toolbarItem && toolbarItem.localName != "toolbar") {
     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;
     }
+  } else {
+    toolbarItem = null;
   }
 
   // Right-clicking on an empty part of the tabstrip will exit
   // the above loop with toolbarItem being the xul:document.
   // That has no parentNode, and we should disable the items in
   // this case.
   let movable = toolbarItem && toolbarItem.parentNode &&
                 CustomizableUI.isWidgetRemovable(toolbarItem);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -707,19 +707,16 @@
                 <label id="urlbar-display" value="&urlbar.switchToTab.label;"/>
               </box>
               <hbox id="urlbar-icons">
                 <image id="page-report-button"
                        class="urlbar-icon"
                        hidden="true"
                        tooltiptext="&pageReportIcon.tooltip;"
                        onclick="gPopupBlockerObserver.onReportButtonClick(event);"/>
-                <image id="star-button"
-                       class="urlbar-icon"
-                       onclick="if (event.button === 0) BookmarkingUI.onCommand(event);"/>
               </hbox>
               <toolbarbutton id="urlbar-go-button"
                              class="chromeclass-toolbar-additional"
                              onclick="gURLBar.handleCommand(event);"
                              tooltiptext="&goEndCap.tooltip;"/>
               <toolbarbutton id="urlbar-reload-button"
                              class="chromeclass-toolbar-additional"
                              command="Browser:ReloadOrDuplicate"
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -52,16 +52,17 @@ support-files =
   file_bug906190.js
   file_bug906190.sjs
   file_bug970276_popup1.html
   file_bug970276_popup2.html
   file_bug970276_favicon1.ico
   file_bug970276_favicon2.ico
   file_dom_notifications.html
   file_fullscreen-window-open.html
+  get_user_media.html
   head.js
   healthreport_testRemoteCommands.html
   moz.png
   offlineQuotaNotification.cacheManifest
   offlineQuotaNotification.html
   page_style_sample.html
   plugin_add_dynamically.html
   plugin_alternate_content.html
@@ -259,16 +260,17 @@ skip-if = os == "mac" # bug 967013, bug 
 run-if = datareporting
 [browser_discovery.js]
 [browser_duplicateIDs.js]
 [browser_drag.js]
 skip-if = true # browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
 [browser_findbarClose.js]
 [browser_fullscreen-window-open.js]
 [browser_gestureSupport.js]
+[browser_get_user_media.js]
 [browser_getshortcutoruri.js]
 [browser_hide_removing.js]
 [browser_homeDrop.js]
 [browser_identity_UI.js]
 [browser_keywordBookmarklets.js]
 [browser_keywordSearch.js]
 [browser_keywordSearch_postData.js]
 [browser_lastAccessedTab.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_get_user_media.js
@@ -0,0 +1,742 @@
+const kObservedTopics = [
+  "getUserMedia:response:allow",
+  "getUserMedia:revoke",
+  "getUserMedia:response:deny",
+  "getUserMedia:request",
+  "recording-device-events",
+  "recording-window-ended"
+];
+
+const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
+                                   "@mozilla.org/mediaManagerService;1",
+                                   "nsIMediaManagerService");
+
+var gObservedTopics = {};
+function observer(aSubject, aTopic, aData) {
+  if (!(aTopic in gObservedTopics))
+    gObservedTopics[aTopic] = 1;
+  else
+    ++gObservedTopics[aTopic];
+}
+
+function promiseNotification(aTopic, aAction) {
+  let deferred = Promise.defer();
+
+  Services.obs.addObserver(function observer() {
+    ok(true, "got " + aTopic + " notification");
+    Services.obs.removeObserver(observer, aTopic);
+
+    if (kObservedTopics.indexOf(aTopic) != -1) {
+      if (!(aTopic in gObservedTopics))
+        gObservedTopics[aTopic] = -1;
+      else
+        --gObservedTopics[aTopic];
+    }
+
+    deferred.resolve();
+  }, aTopic, false);
+
+  if (aAction)
+    aAction();
+
+  return deferred.promise;
+}
+
+function expectNotification(aTopic) {
+  is(gObservedTopics[aTopic], 1, "expected notification " + aTopic);
+  if (aTopic in gObservedTopics)
+    --gObservedTopics[aTopic];
+}
+
+function expectNoNotifications() {
+  for (let topic in gObservedTopics) {
+    if (gObservedTopics[topic])
+      is(gObservedTopics[topic], 0, topic + " notification unexpected");
+  }
+  gObservedTopics = {}
+}
+
+function promiseMessage(aMessage, aAction) {
+  let deferred = Promise.defer();
+
+  content.addEventListener("message", function messageListener(event) {
+    content.removeEventListener("message", messageListener);
+    is(event.data, aMessage, "received " + aMessage);
+    if (event.data == aMessage)
+      deferred.resolve();
+    else
+      deferred.reject();
+  });
+
+  if (aAction)
+    aAction();
+
+  return deferred.promise;
+}
+
+function promisePopupNotification(aName) {
+  let deferred = Promise.defer();
+
+  waitForCondition(() => PopupNotifications.getNotification(aName),
+                   () => {
+    ok(!!PopupNotifications.getNotification(aName),
+       aName + " notification appeared");
+    deferred.resolve();
+  }, "timeout waiting for popup notification " + aName);
+
+  return deferred.promise;
+}
+
+function promiseNoPopupNotification(aName) {
+  let deferred = Promise.defer();
+
+  waitForCondition(() => !PopupNotifications.getNotification(aName),
+                   () => {
+    ok(!PopupNotifications.getNotification(aName),
+       aName + " notification removed");
+    deferred.resolve();
+  }, "timeout waiting for popup notification " + aName + " to disappear");
+
+  return deferred.promise;
+}
+
+const kActionAlways = 1;
+const kActionDeny = 2;
+const kActionNever = 3;
+
+function activateSecondaryAction(aAction) {
+  let notification = PopupNotifications.panel.firstChild;
+  notification.button.focus();
+  let popup = notification.menupopup;
+  popup.addEventListener("popupshown", function () {
+    popup.removeEventListener("popupshown", arguments.callee, false);
+
+    // Press 'down' as many time as needed to select the requested action.
+    while (aAction--)
+      EventUtils.synthesizeKey("VK_DOWN", {});
+
+    // Activate
+    EventUtils.synthesizeKey("VK_RETURN", {});
+  }, false);
+
+  // One down event to open the popup
+  EventUtils.synthesizeKey("VK_DOWN",
+                           { altKey: !navigator.platform.contains("Mac") });
+}
+
+registerCleanupFunction(function() {
+  gBrowser.removeCurrentTab();
+  kObservedTopics.forEach(topic => {
+    Services.obs.removeObserver(observer, topic);
+  });
+  Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
+});
+
+function getMediaCaptureState() {
+  let hasVideo = {};
+  let hasAudio = {};
+  MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio);
+  if (hasVideo.value && hasAudio.value)
+    return "CameraAndMicrophone";
+  if (hasVideo.value)
+    return "Camera";
+  if (hasAudio.value)
+    return "Microphone";
+  return "none";
+}
+
+function closeStream(aAlreadyClosed) {
+  expectNoNotifications();
+
+  info("closing the stream");
+  content.wrappedJSObject.closeStream();
+
+  if (!aAlreadyClosed)
+    yield promiseNotification("recording-device-events");
+
+  yield promiseNoPopupNotification("webRTC-sharingDevices");
+  if (!aAlreadyClosed)
+    expectNotification("recording-window-ended");
+
+  let statusButton = document.getElementById("webrtc-status-button");
+  ok(statusButton.hidden, "WebRTC status button hidden");
+}
+
+function checkDeviceSelectors(aAudio, aVideo) {
+  let micSelector = document.getElementById("webRTC-selectMicrophone");
+  if (aAudio)
+    ok(!micSelector.hidden, "microphone selector visible");
+  else
+    ok(micSelector.hidden, "microphone selector hidden");
+
+  let cameraSelector = document.getElementById("webRTC-selectCamera");
+  if (aVideo)
+    ok(!cameraSelector.hidden, "camera selector visible");
+  else
+    ok(cameraSelector.hidden, "camera selector hidden");
+}
+
+function checkSharingUI() {
+  yield promisePopupNotification("webRTC-sharingDevices");
+  let statusButton = document.getElementById("webrtc-status-button");
+  ok(!statusButton.hidden, "WebRTC status button visible");
+}
+
+function checkNotSharing() {
+  is(getMediaCaptureState(), "none", "expected nothing to be shared");
+
+  ok(!PopupNotifications.getNotification("webRTC-sharingDevices"),
+     "no webRTC-sharingDevices popup notification");
+
+  let statusButton = document.getElementById("webrtc-status-button");
+  ok(statusButton.hidden, "WebRTC status button hidden");
+}
+
+let gTests = [
+
+{
+  desc: "getUserMedia audio+video",
+  run: function checkAudioVideo() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI();
+    yield closeStream();
+  }
+},
+
+{
+  desc: "getUserMedia audio only",
+  run: function checkAudioOnly() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
+
+    yield checkSharingUI();
+    yield closeStream();
+  }
+},
+
+{
+  desc: "getUserMedia video only",
+  run: function checkVideoOnly() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(false, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(false, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "Camera", "expected camera to be shared");
+
+    yield checkSharingUI();
+    yield closeStream();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video, user disables video",
+  run: function checkDisableVideo() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    // disable the camera
+    document.getElementById("webRTC-selectCamera-menulist").value = -1;
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+
+    // reset the menuitem to have no impact on the following tests.
+    document.getElementById("webRTC-selectCamera-menulist").value = 0;
+
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "Microphone",
+       "expected microphone to be shared");
+
+    yield checkSharingUI();
+    yield closeStream();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video, user disables audio",
+  run: function checkDisableAudio() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    // disable the microphone
+    document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+
+    // reset the menuitem to have no impact on the following tests.
+    document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
+
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "Camera",
+       "expected microphone to be shared");
+
+    yield checkSharingUI();
+    yield closeStream();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video, user disables both audio and video",
+  run: function checkDisableAudioVideo() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    // disable the camera and microphone
+    document.getElementById("webRTC-selectCamera-menulist").value = -1;
+    document.getElementById("webRTC-selectMicrophone-menulist").value = -1;
+
+    yield promiseMessage("error: PERMISSION_DENIED", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+
+    // reset the menuitems to have no impact on the following tests.
+    document.getElementById("webRTC-selectCamera-menulist").value = 0;
+    document.getElementById("webRTC-selectMicrophone-menulist").value = 0;
+
+    expectNotification("getUserMedia:response:deny");
+    expectNotification("recording-window-ended");
+    checkNotSharing();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video, user clicks \"Don't Share\"",
+  run: function checkDontShare() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("error: PERMISSION_DENIED", () => {
+      activateSecondaryAction(kActionDeny);
+    });
+
+    expectNotification("getUserMedia:response:deny");
+    expectNotification("recording-window-ended");
+    checkNotSharing();
+  }
+},
+
+{
+  desc: "getUserMedia audio+video: stop sharing",
+  run: function checkStopSharing() {
+    yield promiseNotification("getUserMedia:request", () => {
+      info("requesting devices");
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+
+    yield promisePopupNotification("webRTC-shareDevices");
+    checkDeviceSelectors(true, true);
+
+    yield promiseMessage("ok", () => {
+      PopupNotifications.panel.firstChild.button.click();
+    });
+    expectNotification("getUserMedia:response:allow");
+    expectNotification("recording-device-events");
+    is(getMediaCaptureState(), "CameraAndMicrophone",
+       "expected camera and microphone to be shared");
+
+    yield checkSharingUI();
+
+    PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
+    activateSecondaryAction(kActionDeny);
+
+    yield promiseNotification("recording-device-events");
+    expectNotification("getUserMedia:revoke");
+
+    yield promiseNoPopupNotification("webRTC-sharingDevices");
+
+    if (gObservedTopics["recording-device-events"] == 1) {
+      todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+      gObservedTopics["recording-device-events"] = 0;
+    }
+
+    expectNoNotifications();
+    checkNotSharing();
+
+    // the stream is already closed, but this will do some cleanup anyway
+    yield closeStream(true);
+  }
+},
+
+{
+  desc: "getUserMedia prompt: Always/Never Share",
+  run: function checkRememberCheckbox() {
+    function checkPerm(aRequestAudio, aRequestVideo, aAllowAudio, aAllowVideo,
+                       aExpectedAudioPerm, aExpectedVideoPerm, aNever) {
+      yield promiseNotification("getUserMedia:request", () => {
+        content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
+      });
+
+      yield promisePopupNotification("webRTC-shareDevices");
+
+      let elt = id => document.getElementById(id);
+
+      let noAudio = aAllowAudio === undefined;
+      is(elt("webRTC-selectMicrophone").hidden, noAudio,
+         "microphone selector expected to be " + (noAudio ? "hidden" : "visible"));
+      if (!noAudio)
+        elt("webRTC-selectMicrophone-menulist").value = (aAllowAudio || aNever) ? 0 : -1;
+
+      let noVideo = aAllowVideo === undefined;
+      is(elt("webRTC-selectCamera").hidden, noVideo,
+         "camera selector expected to be " + (noVideo ? "hidden" : "visible"));
+      if (!noVideo)
+        elt("webRTC-selectCamera-menulist").value = (aAllowVideo || aNever) ? 0 : -1;
+
+      let expectedMessage =
+        (aAllowVideo || aAllowAudio) ? "ok" : "error: PERMISSION_DENIED";
+      yield promiseMessage(expectedMessage, () => {
+        activateSecondaryAction(aNever ? kActionNever : kActionAlways);
+      });
+      let expected = [];
+      if (expectedMessage == "ok") {
+        expectNotification("getUserMedia:response:allow");
+        expectNotification("recording-device-events");
+        if (aAllowVideo)
+          expected.push("Camera");
+        if (aAllowAudio)
+          expected.push("Microphone");
+        expected = expected.join("And");
+      }
+      else {
+        expectNotification("getUserMedia:response:deny");
+        expectNotification("recording-window-ended");
+        expected = "none";
+      }
+      is(getMediaCaptureState(), expected,
+         "expected " + expected + " to be shared");
+
+      function checkDevicePermissions(aDevice, aExpected) {
+        let Perms = Services.perms;
+        let uri = content.document.documentURIObject;
+        let devicePerms = Perms.testExactPermission(uri, aDevice);
+        if (aExpected === undefined)
+          is(devicePerms, Perms.UNKNOWN_ACTION, "no " + aDevice + " persistent permissions");
+        else {
+          is(devicePerms, aExpected ? Perms.ALLOW_ACTION : Perms.DENY_ACTION,
+             aDevice + " persistently " + (aExpected ? "allowed" : "denied"));
+        }
+        Perms.remove(uri.host, aDevice);
+      }
+      checkDevicePermissions("microphone", aExpectedAudioPerm);
+      checkDevicePermissions("camera", aExpectedVideoPerm);
+
+      if (expectedMessage == "ok")
+        yield closeStream();
+    }
+
+    // 3 cases where the user accepts the device prompt.
+    info("audio+video, user grants, expect both perms set to allow");
+    yield checkPerm(true, true, true, true, true, true);
+    info("audio only, user grants, check audio perm set to allow, video perm not set");
+    yield checkPerm(true, false, true, undefined, true, undefined);
+    info("video only, user grants, check video perm set to allow, audio perm not set");
+    yield checkPerm(false, true, undefined, true, undefined, true);
+
+    // 3 cases where the user rejects the device request.
+    // First test these cases by setting the device to 'No Audio'/'No Video'
+    info("audio+video, user denies, expect both perms set to deny");
+    yield checkPerm(true, true, false, false, false, false);
+    info("audio only, user denies, expect audio perm set to deny, video not set");
+    yield checkPerm(true, false, false, undefined, false, undefined);
+    info("video only, user denies, expect video perm set to deny, audio perm not set");
+    yield checkPerm(false, true, undefined, false, undefined, false);
+    // Now test these 3 cases again by using the 'Never Share' action.
+    info("audio+video, user denies, expect both perms set to deny");
+    yield checkPerm(true, true, false, false, false, false, true);
+    info("audio only, user denies, expect audio perm set to deny, video not set");
+    yield checkPerm(true, false, false, undefined, false, undefined, true);
+    info("video only, user denies, expect video perm set to deny, audio perm not set");
+    yield checkPerm(false, true, undefined, false, undefined, false, true);
+
+    // 2 cases where the user allows half of what's requested.
+    info("audio+video, user denies video, grants audio, " +
+         "expect video perm set to deny, audio perm set to allow.");
+    yield checkPerm(true, true, true, false, true, false);
+    info("audio+video, user denies audio, grants video, " +
+         "expect video perm set to allow, audio perm set to deny.");
+    yield checkPerm(true, true, false, true, false, true);
+  }
+},
+
+{
+  desc: "getUserMedia without prompt: use persistent permissions",
+  run: function checkUsePersistentPermissions() {
+    function usePerm(aAllowAudio, aAllowVideo, aRequestAudio, aRequestVideo,
+                     aExpectStream) {
+      let Perms = Services.perms;
+      let uri = content.document.documentURIObject;
+      if (aAllowAudio !== undefined) {
+        Perms.add(uri, "microphone", aAllowAudio ? Perms.ALLOW_ACTION
+                                                 : Perms.DENY_ACTION);
+      }
+      if (aAllowVideo !== undefined) {
+        Perms.add(uri, "camera", aAllowVideo ? Perms.ALLOW_ACTION
+                                             : Perms.DENY_ACTION);
+      }
+
+      let gum = function() {
+        content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
+      };
+
+      if (aExpectStream === undefined) {
+        // Check that we get a prompt.
+        yield promiseNotification("getUserMedia:request", gum);
+        yield promisePopupNotification("webRTC-shareDevices");
+
+        // Deny the request to cleanup...
+        yield promiseMessage("error: PERMISSION_DENIED", () => {
+          activateSecondaryAction(kActionDeny);
+        });
+        expectNotification("getUserMedia:response:deny");
+        expectNotification("recording-window-ended");
+      }
+      else {
+        let allow = (aAllowVideo && aRequestVideo) || (aAllowAudio && aRequestAudio);
+        let expectedMessage = allow ? "ok" : "error: PERMISSION_DENIED";
+        yield promiseMessage(expectedMessage, gum);
+
+        if (expectedMessage == "ok") {
+          expectNotification("recording-device-events");
+
+          // Check what's actually shared.
+          let expected = [];
+          if (aAllowVideo && aRequestVideo)
+            expected.push("Camera");
+          if (aAllowAudio && aRequestAudio)
+            expected.push("Microphone");
+          expected = expected.join("And");
+          is(getMediaCaptureState(), expected,
+             "expected " + expected + " to be shared");
+
+          yield closeStream();
+        }
+        else {
+          expectNotification("recording-window-ended");
+        }
+      }
+
+      Perms.remove(uri.host, "camera");
+      Perms.remove(uri.host, "microphone");
+    }
+
+    // Set both permissions identically
+    info("allow audio+video, request audio+video, expect ok (audio+video)");
+    yield usePerm(true, true, true, true, true);
+    info("deny audio+video, request audio+video, expect denied");
+    yield usePerm(false, false, true, true, false);
+
+    // Allow audio, deny video.
+    info("allow audio, deny video, request audio+video, expect ok (audio)");
+    yield usePerm(true, false, true, true, true);
+    info("allow audio, deny video, request audio, expect ok (audio)");
+    yield usePerm(true, false, true, false, true);
+    info("allow audio, deny video, request video, expect denied");
+    yield usePerm(true, false, false, true, false);
+
+    // Deny audio, allow video.
+    info("deny audio, allow video, request audio+video, expect ok (video)");
+    yield usePerm(false, true, true, true, true);
+    info("deny audio, allow video, request audio, expect denied");
+    yield usePerm(false, true, true, false, true);
+    info("deny audio, allow video, request video, expect ok (video)");
+    yield usePerm(false, true, false, true, false);
+
+    // Allow audio, video not set.
+    info("allow audio, request audio+video, expect prompt");
+    yield usePerm(true, undefined, true, true, undefined);
+    info("allow audio, request audio, expect ok (audio)");
+    yield usePerm(true, undefined, true, false, true);
+    info("allow audio, request video, expect prompt");
+    yield usePerm(true, undefined, false, true, undefined);
+
+    // Deny audio, video not set.
+    info("deny audio, request audio+video, expect prompt");
+    yield usePerm(false, undefined, true, true, undefined);
+    info("deny audio, request audio, expect denied");
+    yield usePerm(false, undefined, true, false, false);
+    info("deny audio, request video, expect prompt");
+    yield usePerm(false, undefined, false, true, undefined);
+
+    // Allow video, video not set.
+    info("allow video, request audio+video, expect prompt");
+    yield usePerm(undefined, true, true, true, undefined);
+    info("allow video, request audio, expect prompt");
+    yield usePerm(undefined, true, true, false, undefined);
+    info("allow video, request video, expect ok (video)");
+    yield usePerm(undefined, true, false, true, true);
+
+    // Deny video, video not set.
+    info("deny video, request audio+video, expect prompt");
+    yield usePerm(undefined, false, true, true, undefined);
+    info("deny video, request audio, expect prompt");
+    yield usePerm(undefined, false, true, false, undefined);
+    info("deny video, request video, expect denied");
+    yield usePerm(undefined, false, false, true, false);
+  }
+},
+
+{
+  desc: "Stop Sharing removes persistent permissions",
+  run: function checkStopSharingRemovesPersistentPermissions() {
+    function stopAndCheckPerm(aRequestAudio, aRequestVideo) {
+      let Perms = Services.perms;
+      let uri = content.document.documentURIObject;
+
+      // Initially set both permissions to 'allow'.
+      Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
+      Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+
+      // Start sharing what's been requested.
+      yield promiseMessage("ok", () => {
+        content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
+      });
+      expectNotification("recording-device-events");
+      yield checkSharingUI();
+
+      // Stop sharing.
+      PopupNotifications.getNotification("webRTC-sharingDevices").reshow();
+      activateSecondaryAction(kActionDeny);
+
+      yield promiseNotification("recording-device-events");
+      expectNotification("getUserMedia:revoke");
+
+      yield promiseNoPopupNotification("webRTC-sharingDevices");
+
+      if (gObservedTopics["recording-device-events"] == 1) {
+        todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
+        gObservedTopics["recording-device-events"] = 0;
+      }
+
+      // Check that permissions have been removed as expected.
+      let audioPerm = Perms.testExactPermission(uri, "microphone");
+      if (aRequestAudio)
+        is(audioPerm, Perms.UNKNOWN_ACTION, "microphone permissions removed");
+      else
+        is(audioPerm, Perms.ALLOW_ACTION, "microphone permissions untouched");
+
+      let videoPerm = Perms.testExactPermission(uri, "camera");
+      if (aRequestVideo)
+        is(videoPerm, Perms.UNKNOWN_ACTION, "camera permissions removed");
+      else
+        is(videoPerm, Perms.ALLOW_ACTION, "camera permissions untouched");
+
+      // Cleanup.
+      yield closeStream(true);
+
+      Perms.remove(uri.host, "camera");
+      Perms.remove(uri.host, "microphone");
+    }
+
+    info("request audio+video, stop sharing resets both");
+    yield stopAndCheckPerm(true, true);
+    info("request audio, stop sharing resets audio only");
+    yield stopAndCheckPerm(true, false);
+    info("request video, stop sharing resets video only");
+    yield stopAndCheckPerm(false, true);
+  }
+}
+
+];
+
+function test() {
+  waitForExplicitFinish();
+
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+  tab.linkedBrowser.addEventListener("load", function onload() {
+    tab.linkedBrowser.removeEventListener("load", onload, true);
+
+    kObservedTopics.forEach(topic => {
+      Services.obs.addObserver(observer, topic, false);
+    });
+    Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true);
+
+    Task.spawn(function () {
+      for (let test of gTests) {
+        info(test.desc);
+        yield test.run();
+
+        // Cleanup before the next test
+        expectNoNotifications();
+      }
+    }).then(finish, ex => {
+     ok(false, "Unexpected Exception: " + ex);
+     finish();
+    });
+  }, true);
+  let rootDir = getRootDirectory(gTestPath)
+  rootDir = rootDir.replace("chrome://mochitests/content/",
+                            "http://127.0.0.1:8888/");
+  content.location = rootDir + "get_user_media.html";
+}
+
+
+function wait(time) {
+  let deferred = Promise.defer();
+  setTimeout(deferred.resolve, time);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/get_user_media.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="UTF-8"></head>
+<body>
+<div id="message"></div>
+<script>
+function message(m) {
+  document.getElementById("message").innerHTML = m;
+  window.parent.postMessage(m, "*");
+}
+
+var gStream;
+
+function requestDevice(aAudio, aVideo) {
+  window.navigator.mozGetUserMedia({video: aVideo, audio: aAudio, fake: true},
+                                   function(stream) {
+    gStream = stream;
+    message("ok");
+  }, function(err) { message("error: " + err); });
+}
+message("pending");
+
+function closeStream() {
+  if (!gStream)
+    return;
+  gStream.stop();
+  gStream = null;
+  message("closed");
+}
+</script>
+</body>
+</html>
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -554,17 +554,20 @@ function openHealthReport()
 }
 #endif
 
 /**
  * Opens the feedback page for this version of the application.
  */
 function openFeedbackPage()
 {
-  openUILinkIn("https://input.mozilla.org/feedback", "tab");
+  var url = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+                      .getService(Components.interfaces.nsIURLFormatter)
+                      .formatURLPref("app.feedback.baseURL");
+  openUILinkIn(url, "tab");
 }
 
 function buildHelpMenu()
 {
   // Enable/disable the "Report Web Forgery" menu item.
   if (typeof gSafeBrowsing != "undefined")
     gSafeBrowsing.setReportPhishingMenu();
 }
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -220,17 +220,17 @@ let CustomizableUIInternal = {
       legacy: true,
       type: CustomizableUI.TYPE_TOOLBAR,
       defaultPlacements: [
         "tabbrowser-tabs",
         "new-tab-button",
         "alltabs-button",
         "tabs-closebutton",
       ],
-      defaultCollapsed: false,
+      defaultCollapsed: null,
     }, true);
     this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
       legacy: true,
       type: CustomizableUI.TYPE_TOOLBAR,
       defaultPlacements: [
         "personal-bookmarks",
       ],
       defaultCollapsed: true,
@@ -2122,17 +2122,19 @@ let CustomizableUIInternal = {
       let placements = gPlacements.get(areaId);
       for (let areaNode of areaNodes) {
         this.buildArea(areaId, placements, areaNode);
 
         let area = gAreas.get(areaId);
         if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
           let defaultCollapsed = area.get("defaultCollapsed");
           let win = areaNode.ownerDocument.defaultView;
-          win.setToolbarVisibility(areaNode, !defaultCollapsed);
+          if (defaultCollapsed !== null) {
+            win.setToolbarVisibility(areaNode, !defaultCollapsed);
+          }
         }
       }
     }
   },
 
   /**
    * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
    */
@@ -2146,21 +2148,25 @@ let CustomizableUIInternal = {
 
     // 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);
+    // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
+    // and we don't need to do anything else here:
+    if (gSavedState) {
+      for (let areaId of Object.keys(gSavedState.placements)) {
+        let placements = gSavedState.placements[areaId];
+        gPlacements.set(areaId, placements);
+      }
+      this._rebuildRegisteredAreas();
     }
-    this._rebuildRegisteredAreas();
   },
 
   _clearPreviousUIState: function() {
     Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
       gUIStateBeforeReset[prop] = null;
     });
   },
 
@@ -2281,17 +2287,17 @@ let CustomizableUIInternal = {
             return itemNode && removableOrDefault(itemNode || item);
           });
         }
 
         if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
           let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
           let collapsed = container.getAttribute(attribute) == "true";
           let defaultCollapsed = props.get("defaultCollapsed");
-          if (collapsed != defaultCollapsed) {
+          if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
             LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
             return false;
           }
         }
       }
       LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
           "\nvs.\n" + defaultPlacements.join(","));
 
@@ -2489,17 +2495,19 @@ this.CustomizableUI = {
    *                - legacy: set to true if you want customizableui to
    *                          automatically migrate the currentset attribute
    *                - overflowable: set to true if your toolbar is overflowable.
    *                                This requires an anchor, and only has an
    *                                effect for toolbars.
    *                - defaultPlacements: an array of widget IDs making up the
    *                                     default contents of the area
    *                - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
-   *                                    if toolbar is collapsed by default (default to true)
+   *                                    if toolbar is collapsed by default (default to true).
+   *                                    Specify null to ensure that reset/inDefaultArea don't care
+   *                                    about a toolbar's collapsed state
    */
   registerArea: function(aName, aProperties) {
     CustomizableUIInternal.registerArea(aName, aProperties);
   },
   /**
    * Register a concrete node for a registered area. This method is automatically
    * called from any toolbar in the main browser window that has its
    * "customizable" attribute set to true. There should normally be no need to
@@ -2872,17 +2880,18 @@ this.CustomizableUI = {
   getAreaType: function(aArea) {
     let area = gAreas.get(aArea);
     return area ? area.get("type") : null;
   },
   /**
    * Check if a toolbar is collapsed by default.
    *
    * @param aArea the ID of the area whose default-collapsed state you want to know.
-   * @return `true` or `false` depending on the area, null if the area is unknown.
+   * @return `true` or `false` depending on the area, null if the area is unknown,
+   *         or its collapsed state cannot normally be controlled by the user
    */
   isToolbarDefaultCollapsed: function(aArea) {
     let area = gAreas.get(aArea);
     return area ? area.get("defaultCollapsed") : null;
   },
   /**
    * Obtain the DOM node that is the customize target for an area in a
    * specific window.
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -378,16 +378,17 @@ const CustomizableWidgets = [{
         class: cls,
         label: true,
         tooltiptext: "tooltiptext2",
         shortcutId: "key_fullZoomEnlarge",
       }];
 
       let node = aDocument.createElementNS(kNSXUL, "toolbaritem");
       node.setAttribute("id", "zoom-controls");
+      node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
       node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       // Set this as an attribute in addition to the property to make sure we can style correctly.
       node.setAttribute("removable", "true");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("toolbaritem-combined-buttons");
       node.classList.add(kWidePanelItemClass);
 
       buttons.forEach(function(aButton, aIndex) {
@@ -536,16 +537,17 @@ const CustomizableWidgets = [{
         class: cls,
         label: true,
         tooltiptext: "tooltiptext2",
         shortcutId: "key_paste",
       }];
 
       let node = aDocument.createElementNS(kNSXUL, "toolbaritem");
       node.setAttribute("id", "edit-controls");
+      node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
       node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       // Set this as an attribute in addition to the property to make sure we can style correctly.
       node.setAttribute("removable", "true");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("toolbaritem-combined-buttons");
       node.classList.add(kWidePanelItemClass);
 
       buttons.forEach(function(aButton, aIndex) {
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -686,20 +686,20 @@ CustomizeMode.prototype = {
       wrapper.setAttribute("itemchecked", "true");
       aNode.removeAttribute("checked");
     }
 
     if (aNode.hasAttribute("id")) {
       wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
     }
 
-    if (aNode.hasAttribute("title")) {
+    if (aNode.hasAttribute("label")) {
+      wrapper.setAttribute("title", aNode.getAttribute("label"));
+    } else if (aNode.hasAttribute("title")) {
       wrapper.setAttribute("title", aNode.getAttribute("title"));
-    } else if (aNode.hasAttribute("label")) {
-      wrapper.setAttribute("title", aNode.getAttribute("label"));
     }
 
     if (aNode.hasAttribute("flex")) {
       wrapper.setAttribute("flex", aNode.getAttribute("flex"));
     }
 
     let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
     wrapper.setAttribute("removable", removable);
--- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js
@@ -30,16 +30,51 @@ add_task(function() {
   );
   checkContextMenu(contextMenu, expectedEntries);
 
   let hiddenPromise = contextMenuHidden(contextMenu);
   contextMenu.hidePopup();
   yield hiddenPromise;
 });
 
+// Right-click on an empty bit of extra toolbar should
+// show a context menu with moving options disabled,
+// and a toggle option for the extra toolbar
+add_task(function() {
+  let contextMenu = document.getElementById("toolbar-context-menu");
+  let shownPromise = contextMenuShown(contextMenu);
+  let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []);
+  toolbar.setAttribute("context", "toolbar-context-menu");
+  toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu");
+  EventUtils.synthesizeMouseAtCenter(toolbar, {type: "contextmenu", button: 2 });
+  yield shownPromise;
+
+  let expectedEntries = [
+    [".customize-context-moveToPanel", false],
+    [".customize-context-removeFromToolbar", false],
+    ["---"]
+  ];
+  if (!isOSX) {
+    expectedEntries.push(["#toggle_toolbar-menubar", true]);
+  }
+  expectedEntries.push(
+    ["#toggle_PersonalToolbar", true],
+    ["#toggle_880164_empty_toolbar", true],
+    ["---"],
+    [".viewCustomizeToolbar", true]
+  );
+  checkContextMenu(contextMenu, expectedEntries);
+
+  let hiddenPromise = contextMenuHidden(contextMenu);
+  contextMenu.hidePopup();
+  yield hiddenPromise;
+  removeCustomToolbars();
+});
+
+
 // Right-click on the urlbar-container should
 // show a context menu with disabled options to move it.
 add_task(function() {
   let contextMenu = document.getElementById("toolbar-context-menu");
   let shownPromise = contextMenuShown(contextMenu);
   let urlBarContainer = document.getElementById("urlbar-container");
   // Need to make sure not to click within an edit field.
   let urlbarRect = urlBarContainer.getBoundingClientRect();
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -78,17 +78,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
                                   "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
                                   "resource:///modules/BrowserUITelemetry.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
-                                  "resource:///modules/AsyncShutdown.jsm");
+                                  "resource://gre/modules/AsyncShutdown.jsm");
 
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
@@ -633,16 +633,19 @@ BrowserGlue.prototype = {
 
   /**
    * Application shutdown handler.
    */
   _onQuitApplicationGranted: function () {
     // This pref must be set here because SessionStore will use its value
     // on quit-application.
     this._setPrefToSaveSession();
+
+    // Call trackStartupCrashEnd here in case the delayed call on startup hasn't
+    // yet occurred (see trackStartupCrashEnd caller in browser.js).
     try {
       let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
                          .getService(Ci.nsIAppStartup);
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
@@ -1289,17 +1292,17 @@ BrowserGlue.prototype = {
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 19;
+    const UI_VERSION = 20;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul#";
     let currentUIVersion = 0;
     try {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } catch(ex) {}
     if (currentUIVersion >= UI_VERSION)
       return;
 
@@ -1552,16 +1555,25 @@ BrowserGlue.prototype = {
             detector == "ruprob" ||
             detector == "ukprob")) {
         // If the encoding detector pref value is not reachable from the UI,
         // reset to default (varies by localization).
         Services.prefs.clearUserPref("intl.charset.detector");
       }
     }
 
+    if (currentUIVersion < 20) {
+      // Remove persisted collapsed state from TabsToolbar.
+      let resource = this._rdf.GetResource("collapsed");
+      let toolbar = this._rdf.GetResource(BROWSER_DOCURL + "TabsToolbar");
+      if (this._getPersist(toolbar, resource)) {
+        this._setPersist(toolbar, resource);
+      }
+    }
+
     if (this._dirty)
       this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
 
     delete this._rdf;
     delete this._dataSource;
 
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
--- a/browser/components/places/content/menu.xml
+++ b/browser/components/places/content/menu.xml
@@ -573,15 +573,31 @@
     </implementation>
 
     <handlers>
       <handler event="popupshowing" phase="target"><![CDATA[
         this.adjustArrowPosition();
       ]]></handler>
       <handler event="popupshown" phase="target"><![CDATA[
         this.setAttribute("panelopen", "true");
+        //XXXgijs: this is sadfaces, reading styles right after we dirty layout, but
+        //I don't know of a way around it.
+        let container = document.getAnonymousElementByAttribute(this, "anonid", "container");
+        let cs = getComputedStyle(container);
+        let transitionProp = cs.transitionProperty;
+        let transitionTime = parseFloat(cs.transitionDuration);
+        if ((transitionProp.indexOf("transform") > -1 || transitionProp == "all") &&
+            transitionTime > 0) {
+          this.style.pointerEvents = 'none';
+        }
+      ]]></handler>
+      <handler event="transitionend"><![CDATA[
+        if (event.originalTarget.getAttribute("anonid") == "container" &&
+            event.propertyName == "transform") {
+          this.style.removeProperty("pointer-events");
+        }
       ]]></handler>
       <handler event="popuphidden" phase="target"><![CDATA[
         this.removeAttribute("panelopen");
       ]]></handler>
     </handlers>
   </binding>
 </bindings>
--- a/browser/components/preferences/aboutPermissions.js
+++ b/browser/components/preferences/aboutPermissions.js
@@ -33,17 +33,17 @@ let gVisitStmt = gPlacesDatabase.createA
                   "SELECT SUM(visit_count) AS count " +
                   "FROM moz_places " +
                   "WHERE rev_host = :rev_host");
 
 /**
  * Permission types that should be tested with testExactPermission, as opposed
  * to testPermission. This is based on what consumers use to test these permissions.
  */
-let TEST_EXACT_PERM_TYPES = ["geo"];
+let TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"];
 
 /**
  * Site object represents a single site, uniquely identified by a host.
  */
 function Site(host) {
   this.host = host;
   this.listitem = null;
 
@@ -325,26 +325,29 @@ let PermissionDefaults = {
     if (!Services.prefs.getBoolPref("full-screen-api.enabled")) {
       return this.DENY;
     }
     return this.UNKNOWN;
   },
   set fullscreen(aValue) {
     let value = (aValue != this.DENY);
     Services.prefs.setBoolPref("full-screen-api.enabled", value);
-  }
-}
+  },
+
+  get camera() this.UNKNOWN,
+  get microphone() this.UNKNOWN
+};
 
 /**
  * AboutPermissions manages the about:permissions page.
  */
 let AboutPermissions = {
   /**
    * Number of sites to return from the places database.
-   */  
+   */
   PLACES_SITES_LIMIT: 50,
 
   /**
    * When adding sites to the dom sites-list, divide workload into intervals.
    */
   LIST_BUILD_CHUNK: 5, // interval size
   LIST_BUILD_DELAY: 100, // delay between intervals
 
@@ -364,27 +367,28 @@ let AboutPermissions = {
 
   /**
    * This reflects the permissions that we expose in the UI. These correspond
    * to permission type strings in the permission manager, PermissionDefaults,
    * and element ids in aboutPermissions.xul.
    *
    * Potential future additions: "sts/use", "sts/subd"
    */
-  _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup", "fullscreen"],
+  _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup",
+                          "fullscreen", "camera", "microphone"],
 
   /**
    * Permissions that don't have a global "Allow" option.
    */
-  _noGlobalAllow: ["geo", "indexedDB", "fullscreen"],
+  _noGlobalAllow: ["geo", "indexedDB", "fullscreen", "camera", "microphone"],
 
   /**
    * Permissions that don't have a global "Deny" option.
    */
-  _noGlobalDeny: [],
+  _noGlobalDeny: ["camera", "microphone"],
 
   _stringBundle: Services.strings.
                  createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
 
   /**
    * Called on page load.
    */
   init: function() {
@@ -402,17 +406,17 @@ let AboutPermissions = {
     Services.prefs.addObserver("dom.indexedDB.enabled", this, false);
     Services.prefs.addObserver("dom.disable_open_during_load", this, false);
     Services.prefs.addObserver("full-screen-api.enabled", this, false);
 
     Services.obs.addObserver(this, "perm-changed", false);
     Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
     Services.obs.addObserver(this, "cookie-changed", false);
     Services.obs.addObserver(this, "browser:purge-domain-data", false);
-    
+
     this._observersInitialized = true;
     Services.obs.notifyObservers(null, "browser-permissions-preinit", null);
   },
 
   /**
    * Called on page unload.
    */
   cleanUp: function() {
@@ -537,32 +541,32 @@ let AboutPermissions = {
       if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
         yield true;
       }
       try {
         // aLogin.hostname is a string in origin URL format (e.g. "http://foo.com")
         let uri = NetUtil.newURI(aLogin.hostname);
         this.addHost(uri.host);
       } catch (e) {
-        // newURI will throw for add-ons logins stored in chrome:// URIs 
+        // newURI will throw for add-ons logins stored in chrome:// URIs
       }
       itemCnt++;
     }, this);
 
     let disabledHosts = Services.logins.getAllDisabledHosts();
     disabledHosts.forEach(function(aHostname) {
       if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
         yield true;
       }
       try {
         // aHostname is a string in origin URL format (e.g. "http://foo.com")
         let uri = NetUtil.newURI(aHostname);
         this.addHost(uri.host);
       } catch (e) {
-        // newURI will throw for add-ons logins stored in chrome:// URIs 
+        // newURI will throw for add-ons logins stored in chrome:// URIs
       }
       itemCnt++;
     }, this);
 
     let (enumerator = Services.perms.enumerator) {
       while (enumerator.hasMoreElements()) {
         if (itemCnt % this.LIST_BUILD_CHUNK == 0) {
           yield true;
@@ -773,17 +777,17 @@ let AboutPermissions = {
   },
 
   updateVisitCount: function() {
     this._selectedSite.getVisitCount(function(aCount) {
       let visitForm = AboutPermissions._stringBundle.GetStringFromName("visitCount");
       let visitLabel = PluralForm.get(aCount, visitForm)
                                   .replace("#1", aCount);
       document.getElementById("site-visit-count").value = visitLabel;
-    });  
+    });
   },
 
   updatePasswordsCount: function() {
     if (!this._selectedSite) {
       document.getElementById("passwords-count").hidden = true;
       document.getElementById("passwords-manage-all-button").hidden = false;
       return;
     }
--- a/browser/components/preferences/aboutPermissions.xul
+++ b/browser/components/preferences/aboutPermissions.xul
@@ -108,16 +108,58 @@
                 <menuitem id="geo-1" value="1" label="&permission.allow;"/>
                 <menuitem id="geo-2" value="2" label="&permission.block;"/>
               </menupopup>
             </menulist>
           </hbox>
         </vbox>
       </hbox>
 
+      <!-- Camera -->
+      <hbox id="camera-pref-item"
+            class="pref-item" align="top">
+        <image class="pref-icon" type="camera"/>
+        <vbox>
+          <label class="pref-title" value="&camera.label;"/>
+          <hbox align="center">
+            <menulist id="camera-menulist"
+                      class="pref-menulist"
+                      type="camera"
+                      oncommand="AboutPermissions.onPermissionCommand(event);">
+              <menupopup>
+                <menuitem id="camera-0" value="0" label="&permission.alwaysAsk;"/>
+                <menuitem id="camera-1" value="1" label="&permission.allow;"/>
+                <menuitem id="camera-2" value="2" label="&permission.block;"/>
+              </menupopup>
+            </menulist>
+          </hbox>
+        </vbox>
+      </hbox>
+
+      <!-- Microphone -->
+      <hbox id="microphone-pref-item"
+            class="pref-item" align="top">
+        <image class="pref-icon" type="microphone"/>
+        <vbox>
+          <label class="pref-title" value="&microphone.label;"/>
+          <hbox align="center">
+            <menulist id="microphone-menulist"
+                      class="pref-menulist"
+                      type="microphone"
+                      oncommand="AboutPermissions.onPermissionCommand(event);">
+              <menupopup>
+                <menuitem id="microphone-0" value="0" label="&permission.alwaysAsk;"/>
+                <menuitem id="microphone-1" value="1" label="&permission.allow;"/>
+                <menuitem id="microphone-2" value="2" label="&permission.block;"/>
+              </menupopup>
+            </menulist>
+          </hbox>
+        </vbox>
+      </hbox>
+
       <!-- Cookies -->
       <hbox id="cookie-pref-item"
             class="pref-item" align="top">
         <image class="pref-icon" type="cookie"/>
         <vbox>
           <label class="pref-title" value="&cookie.label;"/>
           <hbox align="center">
             <menulist id="cookie-menulist"
--- a/browser/components/preferences/languages.xul
+++ b/browser/components/preferences/languages.xul
@@ -42,18 +42,17 @@
 
     <stringbundleset id="languageSet">
       <stringbundle id="bundleRegions"      src="chrome://global/locale/regionNames.properties"/>
       <stringbundle id="bundleLanguages"    src="chrome://global/locale/languageNames.properties"/>
       <stringbundle id="bundlePreferences"  src="chrome://browser/locale/preferences/preferences.properties"/>
       <stringbundle id="bundleAccepted"     src="resource://gre/res/language.properties"/>
     </stringbundleset>
 
-    <description>&languages.customize.prefLangDescript;</description>
-    <label>&languages.customize.active.label;</label>
+    <description>&languages.customize.description;</description>
     <grid flex="1">
       <columns>
         <column flex="1"/>
         <column/>
       </columns>
       <rows>
         <row flex="1">
           <listbox id="activeLanguages" flex="1" rows="6"
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -292,23 +292,24 @@
                 <richlistitem>
                   <checkbox label="&engine.tabs.label;"
                             accesskey="&engine.tabs.accesskey;"
                             preference="engine.tabs"/>
                 </richlistitem>
               </richlistbox>
             </vbox>
           </groupbox>
-          <vbox>
+          <hbox align="center">
             <label value="&syncDeviceName.label;"
                    accesskey="&syncDeviceName.accesskey;"
                    control="syncComputerName"/>
             <textbox id="fxaSyncComputerName"
+                     flex="1"
                      onchange="gSyncUtils.changeName(this)"/>
-          </vbox>
+          </hbox>
           <hbox id="tosPP" pack="center">
             <label class="text-link"
                    onclick="event.stopPropagation();gSyncUtils.openToS();"
                    value="&prefs.tosLink.label;"/>
             <label class="text-link"
                    onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"
                    value="&fxaPrivacyNotice.link.label;"/>
           </hbox>
--- a/browser/components/preferences/tests/browser_permissions.js
+++ b/browser/components/preferences/tests/browser_permissions.js
@@ -22,26 +22,28 @@ const PERM_FIRST_PARTY_ONLY = 9;
 // used to set permissions on test sites
 const TEST_PERMS = {
   "password": PERM_ALLOW,
   "cookie": PERM_ALLOW,
   "geo": PERM_UNKNOWN,
   "indexedDB": PERM_UNKNOWN,
   "popup": PERM_DENY,
   "fullscreen" : PERM_UNKNOWN,
+  "camera": PERM_UNKNOWN,
+  "microphone": PERM_UNKNOWN
 };
 
 const NO_GLOBAL_ALLOW = [
   "geo",
   "indexedDB",
   "fullscreen"
 ];
 
 // number of managed permissions in the interface
-const TEST_PERMS_COUNT = 6;
+const TEST_PERMS_COUNT = 8;
 
 function test() {
   waitForExplicitFinish();
   registerCleanupFunction(cleanUp);
 
   // add test history visit
   addVisits(TEST_URI_1, function() {
     // set permissions ourselves to avoid problems with different defaults
@@ -159,17 +161,17 @@ var tests = [
     });
 
     runNextTest();
   },
 
   function test_all_sites_permission() {
     // apply the old default of allowing all cookies
     Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
-  
+
     // there should be no user-set pref for cookie behavior
     is(Services.prefs.getIntPref("network.cookie.cookieBehavior"), PERM_UNKNOWN,
        "network.cookie.cookieBehavior is expected default");
 
     // the default behavior is to allow cookies
     let cookieMenulist = getPermissionMenulist("cookie");
     is(cookieMenulist.value, PERM_ALLOW,
        "menulist correctly shows that cookies are allowed");
@@ -184,22 +186,22 @@ var tests = [
 
     runNextTest();
   },
 
   function test_manage_all_passwords() {
     // make sure "Manage All Passwords..." button opens the correct dialog
     addWindowListener("chrome://passwordmgr/content/passwordManager.xul", runNextTest);
     gBrowser.contentDocument.getElementById("passwords-manage-all-button").doCommand();
-    
+
   },
 
   function test_manage_all_cookies() {
     // make sure "Manage All Cookies..." button opens the correct dialog
-    addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);    
+    addWindowListener("chrome://browser/content/preferences/cookies.xul", runNextTest);
     gBrowser.contentDocument.getElementById("cookies-manage-all-button").doCommand();
   },
 
   function test_select_site() {
     // select the site that has the permissions we set at the beginning of the test
     let testSiteItem = getSiteItem(TEST_URI_2.host);
     gSitesList.selectedItem = testSiteItem;
 
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -1707,16 +1707,20 @@ let SessionStoreInternal = {
       let data = DyingWindowCache.get(aWindow).extData || {};
       return data[aKey] || "";
     }
 
     throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
   },
 
   setWindowValue: function ssi_setWindowValue(aWindow, aKey, aStringValue) {
+    if (typeof aStringValue != "string") {
+      throw new TypeError("setWindowValue only accepts string values");
+    }
+
     if (!("__SSi" in aWindow)) {
       throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
     }
     if (!this._windows[aWindow.__SSi].extData) {
       this._windows[aWindow.__SSi].extData = {};
     }
     this._windows[aWindow.__SSi].extData[aKey] = aStringValue;
     this.saveStateDelayed(aWindow);
@@ -1737,16 +1741,20 @@ let SessionStoreInternal = {
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       // If the tab hasn't been fully restored, get the data from the to-be-restored data
       data = aTab.linkedBrowser.__SS_data.extData;
     }
     return data[aKey] || "";
   },
 
   setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
+    if (typeof aStringValue != "string") {
+      throw new TypeError("setTabValue only accepts string values");
+    }
+
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
     let saveTo;
     if (aTab.__SS_extdata) {
       saveTo = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       saveTo = aTab.linkedBrowser.__SS_data.extData;
@@ -1778,16 +1786,20 @@ let SessionStoreInternal = {
     }
   },
 
   getGlobalValue: function ssi_getGlobalValue(aKey) {
     return this._globalState.get(aKey);
   },
 
   setGlobalValue: function ssi_setGlobalValue(aKey, aStringValue) {
+    if (typeof aStringValue != "string") {
+      throw new TypeError("setGlobalValue only accepts string values");
+    }
+
     this._globalState.set(aKey, aStringValue);
     this.saveStateDelayed();
   },
 
   deleteGlobalValue: function ssi_deleteGlobalValue(aKey) {
     this._globalState.delete(aKey);
     this.saveStateDelayed();
   },
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -257,35 +257,23 @@ InspectorPanel.prototype = {
   markDirty: function InspectorPanel_markDirty() {
     this.isDirty = true;
   },
 
   /**
    * Hooks the searchbar to show result and auto completion suggestions.
    */
   setupSearchBox: function InspectorPanel_setupSearchBox() {
-    let searchDoc;
-    if (this.target.isLocalTab) {
-      searchDoc = this.browser.contentDocument;
-    } else if (this.target.window) {
-      searchDoc = this.target.window.document;
-    } else {
-      searchDoc = null;
-    }
     // Initiate the selectors search object.
-    let setNodeFunction = function(eventName, node) {
-      this.selection.setNodeFront(node, "selectorsearch");
-    }.bind(this);
     if (this.searchSuggestions) {
       this.searchSuggestions.destroy();
       this.searchSuggestions = null;
     }
     this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
-    this.searchSuggestions = new SelectorSearch(this, searchDoc, this.searchBox);
-    this.searchSuggestions.on("node-selected", setNodeFunction);
+    this.searchSuggestions = new SelectorSearch(this, this.searchBox);
   },
 
   /**
    * Build the sidebar.
    */
   setupSidebar: function InspectorPanel_setupSidebar() {
     let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
     this.sidebar = new ToolSidebar(tabbox, this, "inspector");
--- a/browser/devtools/inspector/selector-search.js
+++ b/browser/devtools/inspector/selector-search.js
@@ -1,39 +1,34 @@
 /* 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";
 
-const EventEmitter = require("devtools/shared/event-emitter");
 const promise = require("sdk/core/promise");
 
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 
 // Maximum number of selector suggestions shown in the panel.
 const MAX_SUGGESTIONS = 15;
 
 /**
  * Converts any input box on a page to a CSS selector search and suggestion box.
  *
  * @constructor
  * @param InspectorPanel aInspector
  *        The InspectorPanel whose `walker` attribute should be used for
  *        document traversal.
- * @param nsIDOMDocument aContentDocument
- *        The content document which inspector is attached to, or null if
- *        a remote document.
  * @param nsiInputElement aInputNode
  *        The input element to which the panel will be attached and from where
  *        search input will be taken.
  */
-function SelectorSearch(aInspector, aContentDocument, aInputNode) {
+function SelectorSearch(aInspector, aInputNode) {
   this.inspector = aInspector;
-  this.doc = aContentDocument;
   this.searchBox = aInputNode;
   this.panelDoc = this.searchBox.ownerDocument;
 
   // initialize variables.
   this._lastSearched = null;
   this._lastValidSearch = "";
   this._lastToLastValidSearch = null;
   this._searchResults = null;
@@ -50,29 +45,27 @@ function SelectorSearch(aInspector, aCon
   let options = {
     panelId: "inspector-searchbox-panel",
     listBoxId: "searchbox-panel-listbox",
     autoSelect: true,
     position: "before_start",
     direction: "ltr",
     theme: "auto",
     onClick: this._onListBoxKeypress,
-    onKeypress: this._onListBoxKeypress,
+    onKeypress: this._onListBoxKeypress
   };
   this.searchPopup = new AutocompletePopup(this.panelDoc, options);
 
   // event listeners.
   this.searchBox.addEventListener("command", this._onHTMLSearch, true);
   this.searchBox.addEventListener("keypress", this._onSearchKeypress, true);
 
   // For testing, we need to be able to wait for the most recent node request
   // to finish.  Tests can watch this promise for that.
   this._lastQuery = promise.resolve(null);
-
-  EventEmitter.decorate(this);
 }
 
 exports.SelectorSearch = SelectorSearch;
 
 SelectorSearch.prototype = {
 
   get walker() this.inspector.walker,
 
@@ -160,41 +153,39 @@ SelectorSearch.prototype = {
       }
     }
     return this._state;
   },
 
   /**
    * Removes event listeners and cleans up references.
    */
-  destroy: function SelectorSearch_destroy() {
+  destroy: function() {
     // event listeners.
     this.searchBox.removeEventListener("command", this._onHTMLSearch, true);
     this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true);
     this.searchPopup.destroy();
     this.searchPopup = null;
     this.searchBox = null;
-    this.doc = null;
     this.panelDoc = null;
     this._searchResults = null;
     this._searchSuggestions = null;
-    EventEmitter.decorate(this);
   },
 
   _selectResult: function(index) {
     return this._searchResults.item(index).then(node => {
-      this.emit("node-selected", node);
+      this.inspector.selection.setNodeFront(node, "selectorsearch");
     });
   },
 
   /**
    * The command callback for the input box. This function is automatically
    * invoked as the user is typing if the input box type is search.
    */
-  _onHTMLSearch: function SelectorSearch__onHTMLSearch() {
+  _onHTMLSearch: function() {
     let query = this.searchBox.value;
     if (query == this._lastSearched) {
       return;
     }
     this._lastSearched = query;
     this._searchResults = [];
     this._searchIndex = 0;
 
@@ -251,34 +242,34 @@ SelectorSearch.prototype = {
             this.searchPopup.hidePopup();
           }
           this.searchBox.classList.remove("devtools-no-search-result");
 
           return this._selectResult(0);
         }
         return this._selectResult(0).then(() => {
           this.searchBox.classList.remove("devtools-no-search-result");
-        }).then( () => this.showSuggestions());
+        }).then(() => this.showSuggestions());
       }
       if (query.match(/[\s>+]$/)) {
         this._lastValidSearch = query + "*";
       }
       else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) {
         let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0];
         this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*";
       }
       this.searchBox.classList.add("devtools-no-search-result");
       return this.showSuggestions();
     });
   },
 
   /**
    * Handles keypresses inside the input box.
    */
-  _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) {
+  _onSearchKeypress: function(aEvent) {
     let query = this.searchBox.value;
     switch(aEvent.keyCode) {
       case aEvent.DOM_VK_RETURN:
         if (query == this._lastSearched && this._searchResults) {
           this._searchIndex = (this._searchIndex + 1) % this._searchResults.length;
         }
         else {
           this._onHTMLSearch();
@@ -343,17 +334,17 @@ SelectorSearch.prototype = {
     if (this._searchResults && this._searchResults.length > 0) {
       this._lastQuery = this._selectResult(this._searchIndex);
     }
   },
 
   /**
    * Handles keypress and mouse click on the suggestions richlistbox.
    */
-  _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) {
+  _onListBoxKeypress: function(aEvent) {
     switch(aEvent.keyCode || aEvent.button) {
       case aEvent.DOM_VK_RETURN:
       case aEvent.DOM_VK_TAB:
       case 0: // left mouse button
         aEvent.stopPropagation();
         aEvent.preventDefault();
         this.searchBox.value = this.searchPopup.selectedItem.label;
         this.searchBox.focus();
@@ -399,21 +390,20 @@ SelectorSearch.prototype = {
         this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) ||
                                  query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) ||
                                  ["",""])[1];
         this._onHTMLSearch();
         break;
     }
   },
 
-  
   /**
    * Populates the suggestions list and show the suggestion popup.
    */
-  _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) {
+  _showPopup: function(aList, aFirstPart) {
     let total = 0;
     let query = this.searchBox.value;
     let toLowerCase = false;
     let items = [];
     // In case of tagNames, change the case to small.
     if (query.match(/.*[\.#][^\.#]{0,}$/) == null) {
       toLowerCase = true;
     }
@@ -453,17 +443,17 @@ SelectorSearch.prototype = {
       this.searchPopup.hidePopup();
     }
   },
 
   /**
    * Suggests classes,ids and tags based on the user input as user types in the
    * searchbox.
    */
-  showSuggestions: function SelectorSearch_showSuggestions() {
+  showSuggestions: function() {
     let query = this.searchBox.value;
     let firstPart = "";
     if (this.state == this.States.TAG) {
       // gets the tag that is being completed. For ex. 'div.foo > s' returns 's',
       // 'di' returns 'di' and likewise.
       firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["", query])[1];
       query = query.slice(0, query.length - firstPart.length);
     }
@@ -493,10 +483,10 @@ SelectorSearch.prototype = {
       if (this.state == this.States.CLASS) {
         firstPart = "." + firstPart;
       }
       else if (this.state == this.States.ID) {
         firstPart = "#" + firstPart;
       }
       this._showPopup(result.suggestions, firstPart);
     });
-  },
+  }
 };
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -161,16 +161,19 @@ MarkupView.prototype = {
       this._containers.get(nodeFront).hovered = true;
 
       this._hoveredNode = nodeFront;
     }
   },
 
   _onMouseLeave: function() {
     this._hideBoxModel();
+    if (this._hoveredNode) {
+      this._containers.get(this._hoveredNode).hovered = false;
+    }
     this._hoveredNode = null;
   },
 
   _showBoxModel: function(nodeFront, options={}) {
     this._inspector.toolbox.highlighterUtils.highlightNodeFront(nodeFront, options);
   },
 
   _hideBoxModel: function() {
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -2,16 +2,17 @@
 support-files =
   browser_inspector_markup_edit.html
   browser_inspector_markup_mutation.html
   browser_inspector_markup_mutation_flashing.html
   browser_inspector_markup_navigation.html
   browser_inspector_markup_subset.html
   browser_inspector_markup_765105_tooltip.png
   browser_inspector_markup_950732.html
+  browser_inspector_markup_962647_search.html
   head.js
 
 [browser_bug896181_css_mixed_completion_new_attribute.js]
 # Bug 916763 - too many intermittent failures
 skip-if = true
 [browser_inspector_markup_edit.js]
 [browser_inspector_markup_edit_2.js]
 [browser_inspector_markup_edit_3.js]
@@ -23,8 +24,9 @@ skip-if = true
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.js]
 [browser_inspector_markup_765105_tooltip.js]
 [browser_inspector_markup_950732.js]
 [browser_inspector_markup_964014_copy_image_data.js]
 [browser_inspector_markup_968316_highlit_node_on_hover_then_select.js]
 [browser_inspector_markup_968316_highlight_node_after_mouseleave_mousemove.js]
+[browser_inspector_markup_962647_search.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head></head>
+<body>
+  <ul>
+    <li>
+      <span>this is an <em>important</em> node</span>
+    </li>
+  </ul>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_962647_search.js
@@ -0,0 +1,50 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that searching for nodes using the selector-search input expands and
+// selects the right nodes in the markup-view, even when those nodes are deeply
+// nested (and therefore not attached yet when the markup-view is initialized).
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_962647_search.html";
+
+function test() {
+  waitForExplicitFinish();
+
+  let p = content.document.querySelector("p");
+  Task.spawn(function() {
+    info("loading the test page");
+    yield addTab(TEST_URL);
+
+    info("opening the inspector");
+    let {inspector, toolbox} = yield openInspector();
+
+    ok(!getContainerForRawNode(inspector.markup, getNode("em")),
+      "The <em> tag isn't present yet in the markup-view");
+
+    // Searching for the innermost element first makes sure that the inspector
+    // back-end is able to attach the resulting node to the tree it knows at the
+    // moment. When the inspector is started, the <body> is the default selected
+    // node, and only the parents up to the ROOT are known, and its direct children
+    info("searching for the innermost child: <em>");
+    let updated = inspector.once("inspector-updated");
+    searchUsingSelectorSearch("em", inspector);
+    yield updated;
+
+    ok(getContainerForRawNode(inspector.markup, getNode("em")),
+      "The <em> tag is now imported in the markup-view");
+    is(inspector.selection.node, getNode("em"),
+      "The <em> tag is the currently selected node");
+
+    info("searching for other nodes too");
+    for (let node of ["span", "li", "ul"]) {
+      let updated = inspector.once("inspector-updated");
+      searchUsingSelectorSearch(node, inspector);
+      yield updated;
+      is(inspector.selection.node, getNode(node),
+        "The <" + node + "> tag is the currently selected node");
+    }
+
+    gBrowser.removeCurrentTab();
+  }).then(null, ok.bind(null, false)).then(finish);
+}
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -63,17 +63,16 @@ function openInspector() {
  * HTML node
  * @param {MarkupView} markupView The instance of MarkupView currently loaded into the inspector panel
  * @param {DOMNode} rawNode The DOM node for which the container is required
  * @return {MarkupContainer}
  */
 function getContainerForRawNode(markupView, rawNode) {
   let front = markupView.walker.frontForRawNode(rawNode);
   let container = markupView.getContainer(front);
-  ok(container, "A markup-container object was found");
   return container;
 }
 
 /**
  * Simple DOM node accesor function that takes either a node or a string css
  * selector as argument and returns the corresponding node
  * @param {String|DOMNode} nodeOrSelector
  * @return {DOMNode}
@@ -235,8 +234,31 @@ function redoChange(inspector) {
   if (!canRedo) {
     return promise.reject();
   }
 
   let mutated = inspector.once("markupmutation");
   inspector.markup.undo.redo();
   return mutated;
 }
+
+/**
+ * Get the selector-search input box from the inspector panel
+ * @return {DOMNode}
+ */
+function getSelectorSearchBox(inspector) {
+  return inspector.panelWin.document.getElementById("inspector-searchbox");
+}
+
+/**
+ * Using the inspector panel's selector search box, search for a given selector.
+ * The selector input string will be entered in the input field and the <ENTER>
+ * keypress will be simulated.
+ * This function won't wait for any events and is not async. It's up to callers
+ * to subscribe to events and react accordingly.
+ */
+function searchUsingSelectorSearch(selector, inspector) {
+  info("Entering \"" + selector + "\" into the selector-search input field");
+  let field = getSelectorSearchBox(inspector);
+  field.focus();
+  field.value = selector;
+  EventUtils.sendKey("return", inspector.panelWin);
+}
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -221,24 +221,26 @@ run-if = os == "win"
 [browser_webconsole_bug_770099_violation.js]
 [browser_webconsole_bug_782653_CSS_links_in_Style_Editor.js]
 [browser_webconsole_bug_804845_ctrl_key_nav.js]
 run-if = os == "mac"
 [browser_webconsole_bug_817834_add_edited_input_to_history.js]
 [browser_webconsole_bug_821877_csp_errors.js]
 [browser_webconsole_bug_837351_securityerrors.js]
 [browser_webconsole_bug_846918_hsts_invalid-headers.js]
+[browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js]
 [browser_webconsole_cached_autocomplete.js]
 [browser_webconsole_change_font_size.js]
 [browser_webconsole_chrome.js]
 [browser_webconsole_closure_inspection.js]
 [browser_webconsole_completion.js]
 [browser_webconsole_console_extras.js]
 [browser_webconsole_console_logging_api.js]
 [browser_webconsole_count.js]
+[browser_webconsole_dont_navigate_on_doubleclick.js]
 [browser_webconsole_execution_scope.js]
 [browser_webconsole_for_of.js]
 [browser_webconsole_history.js]
 [browser_webconsole_input_field_focus_on_panel_select.js]
 [browser_webconsole_js_input_expansion.js]
 [browser_webconsole_jsterm.js]
 [browser_webconsole_live_filtering_of_message_types.js]
 [browser_webconsole_live_filtering_on_search_strings.js]
--- a/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_autocomplete_in_debugger_stackframe.js
@@ -177,38 +177,47 @@ function testCompletion(hud) {
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
   yield undefined;
 
   newItems = popup.getItems();
   ok(!newItems.every(function(item) {
        return item.label != "prop1";
      }), "autocomplete results do contain prop1");
 
-  // Test if 'foo1Obj.prop1.' gives 'prop11'
+  // Test if 'foo2Obj.prop1.' gives 'prop11'
   input.value = "foo2Obj.prop1.";
   input.setSelectionRange(14, 14);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
   yield undefined;
 
   newItems = popup.getItems();
   ok(!newItems.every(function(item) {
        return item.label != "prop11";
      }), "autocomplete results do contain prop11");
 
-  // Test if 'foo1Obj.prop1.prop11.' gives suggestions for a string i.e. 'length'
+  // Test if 'foo2Obj.prop1.prop11.' gives suggestions for a string i.e. 'length'
   input.value = "foo2Obj.prop1.prop11.";
   input.setSelectionRange(21, 21);
   jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
   yield undefined;
 
   newItems = popup.getItems();
   ok(!newItems.every(function(item) {
        return item.label != "length";
      }), "autocomplete results do contain length");
 
+  // Test if 'foo1Obj[0].' throws no errors.
+  input.value = "foo2Obj[0].";
+  input.setSelectionRange(11, 11);
+  jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext);
+  yield undefined;
+
+  newItems = popup.getItems();
+  is(newItems.length, 0, "no items for foo2Obj[0]");
+
   testDriver = null;
   executeSoon(finishTest);
   yield undefined;
 }
 
 function debuggerOpened(aResult)
 {
   let debuggerWin = aResult.panelWin;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_915141_toggle_response_logging_with_keyboard.js
@@ -0,0 +1,112 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the 'Log Request and Response Bodies' buttons can be toggled with keyboard.
+const TEST_URI = "data:text/html;charset=utf-8,Web Console test for bug 915141: Toggle log response bodies with keyboard";
+let hud;
+
+function test() {
+  let saveBodiesMenuItem;
+  let saveBodiesContextMenuItem;
+
+  loadTab(TEST_URI).then(({tab: tab}) => {
+    return openConsole(tab);
+  })
+  .then((aHud) => {
+    hud = aHud;
+    saveBodiesMenuItem = hud.ui.rootElement.querySelector("#saveBodies");
+    saveBodiesContextMenuItem = hud.ui.rootElement.querySelector("#saveBodiesContextMenu");
+
+    // Test the context menu action.
+    info("Testing 'Log Request and Response Bodies' menuitem of right click context menu.");
+
+    return openPopup(saveBodiesContextMenuItem);
+  })
+  .then(() => {
+    is(saveBodiesContextMenuItem.getAttribute("checked"), "false",
+       "Context menu: 'log responses' is not checked before action.");
+    is(hud.ui._saveRequestAndResponseBodies, false,
+       "Context menu: Responses are not logged before action.");
+
+    EventUtils.synthesizeKey("VK_DOWN", {});
+    EventUtils.synthesizeKey("VK_RETURN", {});
+
+   return waitForUpdate(saveBodiesContextMenuItem);
+  })
+  .then(() => {
+    is(saveBodiesContextMenuItem.getAttribute("checked"), "true",
+       "Context menu: 'log responses' is checked after menuitem was selected with keyboard.");
+    is(hud.ui._saveRequestAndResponseBodies, true,
+       "Context menu: Responses are saved after menuitem was selected with keyboard.");
+
+    return openPopup(saveBodiesMenuItem);
+  })
+  .then(() => {
+    // Test the 'Net' menu item.
+    info("Testing 'Log Request and Response Bodies' menuitem of 'Net' menu in the console.");
+    // 'Log Request and Response Bodies' should be selected due to previous test.
+
+    is(saveBodiesMenuItem.getAttribute("checked"), "true",
+       "Console net menu: 'log responses' is checked before action.");
+    is(hud.ui._saveRequestAndResponseBodies, true,
+       "Console net menu: Responses are logged before action.");
+
+    // The correct item is the last one in the menu.
+    EventUtils.synthesizeKey("VK_UP", {});
+    EventUtils.synthesizeKey("VK_RETURN", {});
+
+   return waitForUpdate(saveBodiesMenuItem);
+  })
+  .then(() => {
+    is(saveBodiesMenuItem.getAttribute("checked"), "false",
+       "Console net menu: 'log responses' is NOT checked after menuitem was selected with keyboard.");
+    is(hud.ui._saveRequestAndResponseBodies, false,
+       "Responses are NOT saved after menuitem was selected with keyboard.");
+  })
+  .then(finishTest);
+}
+
+/**
+ * Opens and waits for the menu containing aMenuItem to open.
+ * @param aMenuItem MenuItem
+ *        A MenuItem in a menu that should be opened.
+ * @return A promise that's resolved once menu is open.
+ */
+function openPopup(aMenuItem) {
+  let menu = aMenuItem.parentNode;
+
+  let menuOpened = promise.defer();
+  let uiUpdated = promise.defer();
+  // The checkbox menuitem is updated asynchronously on 'popupshowing' event so
+  // it's better to wait for both the update to happen and the menu to open
+  // before continuing or the test might fail due to a race between menu being
+  // shown and the item updated to have the correct state.
+  hud.ui.once("save-bodies-ui-toggled", uiUpdated.resolve);
+  menu.addEventListener("popupshown", function onPopup () {
+    menu.removeEventListener("popupshown", onPopup);
+    menuOpened.resolve();
+  });
+
+  menu.openPopup();
+  return Promise.all([menuOpened.promise, uiUpdated.promise]);
+}
+
+/**
+ * Waits for the settings and menu containing aMenuItem to update.
+ * @param aMenuItem MenuItem
+ *        The menuitem that should be updated.
+ * @return A promise that's resolved once the settings and menus are updated.
+ */
+function waitForUpdate(aMenuItem) {
+  info("Waiting for settings update to complete.");
+  let deferred = promise.defer();
+  hud.ui.once("save-bodies-pref-reversed", function () {
+    hud.ui.once("save-bodies-ui-toggled", deferred.resolve);
+    // The checked state is only updated once the popup is shown.
+    aMenuItem.parentNode.openPopup();
+  });
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
@@ -0,0 +1,43 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that if a link in console is double clicked, the console frame doesn't
+// navigate to that destination (bug 975707).
+
+function test() {
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    const TEST_PAGE_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console.html" + "?_uniq=" + Date.now();
+
+    const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
+    const hud = yield openConsole(tab);
+
+    content.location = TEST_PAGE_URI;
+
+    let messages = yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        name: "Network request message",
+        url: TEST_PAGE_URI,
+        category: CATEGORY_NETWORK
+      }]
+    });
+
+    let networkEventMessage = messages[0].matched.values().next().value;
+    let urlNode = networkEventMessage.querySelector(".url");
+
+    let deferred = promise.defer();
+    urlNode.addEventListener("click", function onClick(aEvent) {
+      urlNode.removeEventListener("click", onClick);
+      ok(aEvent.defaultPrevented, "The default action was prevented.");
+
+      deferred.resolve();
+    });
+
+    EventUtils.synthesizeMouseAtCenter(urlNode, {clickCount: 2}, hud.iframeWindow);
+
+    yield deferred.promise;
+  }
+}
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -532,22 +532,22 @@ WebConsoleFrame.prototype = {
       this.getSaveRequestAndResponseBodies().then(aValue => {
         this.setSaveRequestAndResponseBodies(!aValue);
         aElement.setAttribute("checked", aValue);
         this.emit("save-bodies-pref-reversed");
       });
     }
 
     let saveBodies = doc.getElementById("saveBodies");
-    saveBodies.addEventListener("click", reverseSaveBodiesPref);
+    saveBodies.addEventListener("command", reverseSaveBodiesPref);
     saveBodies.disabled = !this.getFilterState("networkinfo") &&
                           !this.getFilterState("network");
 
     let saveBodiesContextMenu = doc.getElementById("saveBodiesContextMenu");
-    saveBodiesContextMenu.addEventListener("click", reverseSaveBodiesPref);
+    saveBodiesContextMenu.addEventListener("command", reverseSaveBodiesPref);
     saveBodiesContextMenu.disabled = !this.getFilterState("networkinfo") &&
                                      !this.getFilterState("network");
 
     saveBodies.parentNode.addEventListener("popupshowing", () => {
       updateSaveBodiesPrefUI(saveBodies);
       saveBodies.disabled = !this.getFilterState("networkinfo") &&
                             !this.getFilterState("network");
     });
@@ -2671,23 +2671,23 @@ WebConsoleFrame.prototype = {
       this._startX = aEvent.clientX;
       this._startY = aEvent.clientY;
     }, false);
 
     aNode.addEventListener("click", (aEvent) => {
       let mousedown = this._mousedown;
       this._mousedown = false;
 
+      aEvent.preventDefault();
+
       // Do not allow middle/right-click or 2+ clicks.
       if (aEvent.detail != 1 || aEvent.button != 0) {
         return;
       }
 
-      aEvent.preventDefault();
-
       // If this event started with a mousedown event and it ends at a different
       // location, we consider this text selection.
       if (mousedown &&
           (this._startX != aEvent.clientX) &&
           (this._startY != aEvent.clientY))
       {
         this._startX = this._startY = undefined;
         return;
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -492,18 +492,22 @@ identity.loggedIn.signOut.accessKey = O
 # The number of devices can be either one or two.
 getUserMedia.shareCamera.message = Would you like to share your camera with %S?
 getUserMedia.shareMicrophone.message = Would you like to share your microphone with %S?
 getUserMedia.shareCameraAndMicrophone.message = Would you like to share your camera and microphone with %S?
 getUserMedia.noVideo.label = No Video
 getUserMedia.noAudio.label = No Audio
 getUserMedia.shareSelectedDevices.label = Share Selected Device;Share Selected Devices
 getUserMedia.shareSelectedDevices.accesskey = S
+getUserMedia.always.label = Always Share
+getUserMedia.always.accesskey = A
 getUserMedia.denyRequest.label = Don't Share
 getUserMedia.denyRequest.accesskey = D
+getUserMedia.never.label = Never Share
+getUserMedia.never.accesskey = N
 getUserMedia.sharingCamera.message2 = You are currently sharing your camera with this page.
 getUserMedia.sharingMicrophone.message2 = You are currently sharing your microphone with this page.
 getUserMedia.sharingCameraAndMicrophone.message2 = You are currently sharing your camera and microphone with this page.
 getUserMedia.continueSharing.label = Continue Sharing
 getUserMedia.continueSharing.accesskey = C
 getUserMedia.stopSharing.label = Stop Sharing
 getUserMedia.stopSharing.accesskey = S
 
--- a/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd
@@ -37,8 +37,10 @@
 
 <!-- LOCALIZATION NOTE (indexedDB.label): This is describing indexedDB storage
      using the same language used for the permIndexedDB string in browser/pageInfo.dtd -->
 <!ENTITY indexedDB.label                 "Maintain Offline Storage">
 
 <!ENTITY popup.label                     "Open Pop-up Windows">
 
 <!ENTITY fullscreen.label                "Fullscreen">
+<!ENTITY camera.label                    "Use the Camera">
+<!ENTITY microphone.label                "Use the Microphone">
--- a/browser/locales/en-US/chrome/browser/preferences/languages.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/languages.dtd
@@ -1,17 +1,16 @@
 <!-- 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/. -->
 
 <!ENTITY window.width                                   "30em">
 
 <!ENTITY languages.customize.Header                     "Languages">
-<!ENTITY languages.customize.prefLangDescript           "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference.">
-<!ENTITY languages.customize.active.label               "Languages in order of preference:">
+<!ENTITY languages.customize.description                "Web pages are sometimes offered in more than one language. Choose languages for displaying these web pages, in order of preference:">
 <!ENTITY languages.customize.moveUp.label               "Move Up">
 <!ENTITY languages.customize.moveUp.accesskey           "U">
 <!ENTITY languages.customize.moveDown.label             "Move Down">
 <!ENTITY languages.customize.moveDown.accesskey         "D">
 <!ENTITY languages.customize.deleteButton.label         "Remove">
 <!ENTITY languages.customize.deleteButton.accesskey     "R">
 <!ENTITY languages.customize.selectLanguage.label       "Select a language to add…">
 <!ENTITY languages.customize.addButton.label            "Add">
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -5,15 +5,17 @@
 allow = Allow
 allowForSession = Allow for Session
 block = Block
 alwaysAsk = Always Ask
 
 permission.cookie.label = Set Cookies
 permission.desktop-notification.label = Show Notifications
 permission.image.label = Load Images
+permission.camera.label = Use the Camera
+permission.microphone.label = Use the Microphone
 permission.install.label = Install Add-ons
 permission.popup.label = Open Pop-up Windows
 permission.geo.label = Access Your Location
 permission.indexedDB.label = Maintain Offline Storage
 permission.fullscreen.label = Enter Fullscreen
 permission.pointerLock.label = Hide the Mouse Pointer
 
--- a/browser/locales/en-US/profile/bookmarks.inc
+++ b/browser/locales/en-US/profile/bookmarks.inc
@@ -1,15 +1,15 @@
 # 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/.
 #filter emptyLines
 
 # LOCALIZATION NOTE: The 'en-US' strings in the URLs will be replaced with
-# your locale code, and link to your translated pages as soon as they're 
+# your locale code, and link to your translated pages as soon as they're
 # live.
 
 #define bookmarks_title Bookmarks
 #define bookmarks_heading Bookmarks
 
 #define bookmarks_toolbarfolder Bookmarks Toolbar Folder
 #define bookmarks_toolbarfolder_description Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
 
@@ -32,9 +32,14 @@
 # LOCALIZATION NOTE (firefox_community):
 # link title for https://www.mozilla.org/en-US/contribute/
 #define firefox_community Get Involved
 
 # LOCALIZATION NOTE (firefox_about):
 # link title for https://www.mozilla.org/en-US/about/
 #define firefox_about About Us
 
+# LOCALIZATION NOTE (firefox_feedback):
+# link title for browser feedback page
+# currently used by Metro only: https://input.mozilla.org/feedback/metrofirefox
+#define firefox_feedback Give Feedback
+
 #unfilter emptyLines
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -158,17 +158,16 @@ var Browser = {
     }
 
     messageManager.addMessageListener("DOMLinkAdded", this);
     messageManager.addMessageListener("Browser:FormSubmit", this);
     messageManager.addMessageListener("Browser:CanUnload:Return", this);
     messageManager.addMessageListener("scroll", this);
     messageManager.addMessageListener("Browser:CertException", this);
     messageManager.addMessageListener("Browser:BlockedSite", this);
-    messageManager.addMessageListener("Browser:TapOnSelection", this);
 
     Task.spawn(function() {
       // Activation URIs come from protocol activations, secondary tiles, and file activations
       let activationURI = yield this.getShortcutOrURI(Services.metro.activationURI);
 
       let self = this;
       function loadStartupURI() {
         if (activationURI) {
@@ -231,17 +230,16 @@ var Browser = {
     ClickEventHandler.uninit();
     ContentAreaObserver.shutdown();
     Appbar.shutdown();
 
     messageManager.removeMessageListener("Browser:FormSubmit", this);
     messageManager.removeMessageListener("scroll", this);
     messageManager.removeMessageListener("Browser:CertException", this);
     messageManager.removeMessageListener("Browser:BlockedSite", this);
-    messageManager.removeMessageListener("Browser:TapOnSelection", this);
 
     Services.obs.removeObserver(SessionHistoryObserver, "browser:purge-session-history");
 
     window.controllers.removeController(this);
     window.controllers.removeController(BrowserUI);
   },
 
   getHomePage: function getHomePage(aOptions) {
@@ -861,26 +859,16 @@ var Browser = {
         break;
       }
       case "Browser:CertException":
         this._handleCertException(aMessage);
         break;
       case "Browser:BlockedSite":
         this._handleBlockedSite(aMessage);
         break;
-      case "Browser:TapOnSelection":
-        if (!InputSourceHelper.isPrecise) {
-          if (SelectionHelperUI.isActive) {
-            SelectionHelperUI.shutdown();
-          }
-          if (SelectionHelperUI.canHandle(aMessage)) {
-            SelectionHelperUI.openEditSession(aMessage);
-          }
-        }
-        break;
     }
   },
 };
 
 Browser.MainDragger = function MainDragger() {
   this._horizontalScrollbar = document.getElementById("horizontal-scroller");
   this._verticalScrollbar = document.getElementById("vertical-scroller");
   this._scrollScales = { x: 0, y: 0 };
--- a/browser/metro/base/content/startui/Start.xul
+++ b/browser/metro/base/content/startui/Start.xul
@@ -62,17 +62,17 @@
         </richgrid>
       </vbox>
 
       <vbox id="start-bookmarks" class="meta-section">
         <label class="meta-section-title wide-title" value="&bookmarksHeader.label;"/>
         <html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')">
           &narrowBookmarksHeader.label;
         </html:div>
-        <richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="2">
+        <richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="3">
           <richgriditem/>
           <richgriditem/>
         </richgrid>
       </vbox>
 
       <vbox id="start-history" class="meta-section">
         <label class="meta-section-title wide-title" value="&recentHistoryHeader.label;"/>
         <html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-history')">
--- a/browser/metro/locales/generic/profile/bookmarks.json.in
+++ b/browser/metro/locales/generic/profile/bookmarks.json.in
@@ -1,14 +1,17 @@
 #filter substitution
 {"type":"text/x-moz-place-container","root":"placesRoot","children":
   [{"type":"text/x-moz-place-container","title":"@bookmarks_title@","annos":[{"name":"metro/bookmarksRoot","expires":4,"type":1,"value":1}],
     "children":
      [
        {"index":1,"title":"@firefox_about@", "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/about/",
         "iconUri":"data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AVVBO%2F1NOTv9UT07%2FVVBOAFVQTwBUT04AU05N%2F1ROTv9VT0%2F%2FVVBPAFROTgBUT04AVE5O%2F1VQTv9VUE7%2F%2F%2F%2F%2FAFVQTv9UTk7%2FVE5O%2F1NOTgBUTk4AVE9OAFRPTvdTTk7%2FVE5O%2F1NOTgBVUE4AU05OAFVQTv9UTk7%2FU05N%2F%2F%2F%2F%2FwBTTk7%2FVE5O%2F1VQTv9VUE4AVVBOAFROTgBUT073VE5O%2F1ROTv9TTk4AVE5OAFROTgBVUE7%2FVE5O%2F1VQT%2F%2F%2F%2F%2F8AVE5O%2F1VQTv9UTk7%2FVE5OAFRPTgBUTk4AVE9O91VQT%2F9UTk7%2FU05OAFRPTgBUT04AVE5O%2F1NOTv9UTk7%2F%2F%2F%2F%2FAFROTv9UTk7%2FVVBP%2F1ROTgBUTk4AVE5OAFVQTvdUTk7%2FU05N%2F1NOTQBTTk4AU05OAFVPT%2F9TTk7%2FVE5O%2F%2F%2F%2F%2FwBUTk7%2FVE9O%2F1ROTv9UTk4AVU9PAFVQTwBVT0%2F3VE9O%2F1ROTv9UTk4AVE9OAFNOTgBVUE7%2FU05O%2F1ROTv%2F%2F%2F%2F8AVE9O%2F1VQT%2F9VUE%2F%2FVE5OAFVQTgBUTk4AVE9O91ROTv9UTk7%2FVE5OAFNOTgBUTk4AVE5O%2F1NOTf9UTk7%2F%2F%2F%2F%2FAFVQTv9UT07%2FU05N%2F1ROTgBVUE4AVE5OAFVQTutVUE7%2FVE9O%2F1NOTQBUT04AVVBOAFROTv9UTk7%2FVE9O%2F1ROTiFUTk7%2FVE5O%2F1NOTf9UTk4%2B%2F%2F%2F%2FAP%2F%2F%2FwBUTk7%2FVVBO%2F1ROTv9UTk4%2BVE9OAFRPTgBTTk3%2FVE5O%2F1ROTv9UT05lU05O%2F1ROTv9VUE7%2FU05N%2F1VQToBUT06eU05O%2F1RPTv9UTk7%2FVE9O%2F1VPT51VT0%2BaVE5O%2F1ROTv9VT0%2F%2FVE5Or1VQTv9UTk7%2FU05N%2F1ROTv9TTk7%2FVVBO%2F1NOTf9TTk7%2FU05N%2BVNOTv9UTk7%2FVVBP%2F1RPTv9VUE%2F%2FU05OzP%2F%2F%2FwBUT07%2FU05N%2F1ROTo%2F%2F%2F%2F8AVE9O%2F1RPTv9UTk7%2FVE5OqFROTgBVUE5hU05O%2F1VQTv9TTk7%2FVE9O31ROTgD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2FAAD%2F%2FwAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAgAAAAAAAAACIYQAA%2F%2F8AAP%2F%2FAAAoAAAAIAAAAEAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk3BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NAFNOTQBTTk0AU05NAFNOTQBTTk3LU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk1WU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NkVNOTQBTTk0AU05NHFNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk29U05NHFNOTQBTTk0cU05N%2B1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2FlNOTfBTTk3%2BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N9lNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfb%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk1aU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3P%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTahTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3bU05N21NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk12%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NwVNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05NHFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3FNOTQBTTk0AU05NqVNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NqVNOTQD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05NAFNOTTlTTk29U05N51NOTdxTTk0AU05NAFNOTXZTTk3cU05N9lNOTf9TTk37U05N51NOTXVTTk0AU05NAFNOTQBTTk0AU05NOFNOTc9TTk32U05N%2F1NOTf9TTk3wU05Nz1NOTThTTk0AU05NAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8A4BwPAAAADwAAAA4AAAAeBAGAH8cH4H%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F"
        },
-       {"index":2,"title":"@firefox_community@",  "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/contribute/",
+       {"index":2,"title":"@firefox_feedback@", "type":"text/x-moz-place", "uri":"https://input.mozilla.org/feedback/metrofirefox",
+        "iconUri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA0xJREFUeNpUU11sFFUYPffO3d2Z7i677NIuNM3uUJRWBEMUTLARwYAlEEwISVMDPKHiz4OgiYEmYuID+OArPvDog0Ejhgcx4UdDaCk2soiW7lpafnZp6lK3u9vd7czszNwZ72wpwZN8+e795p5zJ9/5LnFdF1/K0g8ATh41eDratWtDt1Z4J8gbm9PhVBsWMCriioifRNzwCpXx880P5LGA621+jq89UfIF+/cXRjq9/T6Dg4jct+8rHD1Qx+R3152rf2iXtxdvflErGte8M1SQ93uLtwei2Dl7e8Aje2sPuQaQr3McP/URPjmtoPddlR7e477Bg6HL6suxvqYAIdgSi0tNwt7+GFIpHxYxFJHwcdSPsWoDfYd24vXPVXTu7sa2T5+XazV6es2u1uVUouSllc/6TC1vw6no2LAp/ESsKUiBKhjKyWfgBvz4djgK9ZUE4kklUn9kHaO+AGlv75D8lu4gqC7crs8lsGzHQZw9U4J7qYI/b87i9tUHcCAhM2libs5G1/Y45ovGViZ6uCQYpWCUQKtRhF5d1RRhvkwzT08UUM5kUZsuwHYY0lkKhXIkUjLmy7ybNQwnABuQV1AMXqzB+uUGpgtNU3Ci92tY33wvrOKC5EA3CBKqhuiSEsqaBqNqMxrwkXptThCEX7GEhNawgxVCzMPAhQ/w46ZfcXxjDtWZIsxqGa915IFyCfW/H0EJEY2J/v/1T97qCS+VsHqNjIejNtalGF7YKCMo2xjJ/QucuwK74z0klTwOrc+iUTAxNVSGRJxRZnP3zMSo+WIkzpS2pIyOdTHYJkf6t3nk7priP8ZwLPgWelrG8f6OWcSlBioTFWR+l+CPxi6RkwEaUiQMMkLWb+0PQw4wcM5AZQn3y34UqwZayDySbRZUNQBdWJodruNeTqkuT7Z2SdsYMXUXt8RVb06NmUG5haBF4aCWjbhPx6qQgYQM+B3qDT5yd2yMZyyjOKlvmblXusO8Zn1m8BEx0r0mcc+nh7X2eIiis8uPSIxAV8QBMR4zUxZygwSubmvFGadHVG89eUyLOBViKx2CI47r7uaUqNxecEcOECyNUINb7rl6xR34sG7fX+T8T+BpIZGeE9H+uDQtIvs0cRH/CTAA8jZdCaXrR1kAAAAASUVORK5CYII="
+       },
+       {"index":3,"title":"@firefox_community@",  "type":"text/x-moz-place", "uri":"http://www.mozilla.org/@AB_CD@/contribute/",
         "iconUri":"data:image/x-icon;base64,AAABAAIAEBAAAAEAIABoBAAAJgAAACAgAAABACAAqBAAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AVVBO%2F1NOTv9UT07%2FVVBOAFVQTwBUT04AU05N%2F1ROTv9VT0%2F%2FVVBPAFROTgBUT04AVE5O%2F1VQTv9VUE7%2F%2F%2F%2F%2FAFVQTv9UTk7%2FVE5O%2F1NOTgBUTk4AVE9OAFRPTvdTTk7%2FVE5O%2F1NOTgBVUE4AU05OAFVQTv9UTk7%2FU05N%2F%2F%2F%2F%2FwBTTk7%2FVE5O%2F1VQTv9VUE4AVVBOAFROTgBUT073VE5O%2F1ROTv9TTk4AVE5OAFROTgBVUE7%2FVE5O%2F1VQT%2F%2F%2F%2F%2F8AVE5O%2F1VQTv9UTk7%2FVE5OAFRPTgBUTk4AVE9O91VQT%2F9UTk7%2FU05OAFRPTgBUT04AVE5O%2F1NOTv9UTk7%2F%2F%2F%2F%2FAFROTv9UTk7%2FVVBP%2F1ROTgBUTk4AVE5OAFVQTvdUTk7%2FU05N%2F1NOTQBTTk4AU05OAFVPT%2F9TTk7%2FVE5O%2F%2F%2F%2F%2FwBUTk7%2FVE9O%2F1ROTv9UTk4AVU9PAFVQTwBVT0%2F3VE9O%2F1ROTv9UTk4AVE9OAFNOTgBVUE7%2FU05O%2F1ROTv%2F%2F%2F%2F8AVE9O%2F1VQT%2F9VUE%2F%2FVE5OAFVQTgBUTk4AVE9O91ROTv9UTk7%2FVE5OAFNOTgBUTk4AVE5O%2F1NOTf9UTk7%2F%2F%2F%2F%2FAFVQTv9UT07%2FU05N%2F1ROTgBVUE4AVE5OAFVQTutVUE7%2FVE9O%2F1NOTQBUT04AVVBOAFROTv9UTk7%2FVE9O%2F1ROTiFUTk7%2FVE5O%2F1NOTf9UTk4%2B%2F%2F%2F%2FAP%2F%2F%2FwBUTk7%2FVVBO%2F1ROTv9UTk4%2BVE9OAFRPTgBTTk3%2FVE5O%2F1ROTv9UT05lU05O%2F1ROTv9VUE7%2FU05N%2F1VQToBUT06eU05O%2F1RPTv9UTk7%2FVE9O%2F1VPT51VT0%2BaVE5O%2F1ROTv9VT0%2F%2FVE5Or1VQTv9UTk7%2FU05N%2F1ROTv9TTk7%2FVVBO%2F1NOTf9TTk7%2FU05N%2BVNOTv9UTk7%2FVVBP%2F1RPTv9VUE%2F%2FU05OzP%2F%2F%2FwBUT07%2FU05N%2F1ROTo%2F%2F%2F%2F8AVE9O%2F1RPTv9UTk7%2FVE5OqFROTgBVUE5hU05O%2F1VQTv9TTk7%2FVE9O31ROTgD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2FAAD%2F%2FwAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAjjgAAI44AACOOAAAgAAAAAAAAACIYQAA%2F%2F8AAP%2F%2FAAAoAAAAIAAAAEAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk3BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3wU05NAFNOTQBTTk0AU05NAFNOTQBTTk26U05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk0AU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfBTTk0AU05NAFNOTQBTTk0AU05NAFNOTbpTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTQBTTk0AU05NAFNOTQBTTk0AU05N8FNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N8FNOTQBTTk0AU05NAFNOTQBTTk0AU05NulNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NAFNOTQBTTk0AU05NAFNOTQBTTk3wU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NAFNOTQBTTk0AU05NAFNOTQBTTk3LU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk1WU05NAFNOTQBTTk0AU05NAFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTQBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk32U05NkVNOTQBTTk0AU05NHFNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk29U05NHFNOTQBTTk0cU05N%2B1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NAFNOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2FlNOTfBTTk3%2BU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N9lNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTfb%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk1aU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3P%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAFNOTahTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3bU05N21NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk12%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8AU05NwVNOTftTTk3%2FU05N%2F1NOTf9TTk3%2FU05NHFNOTfBTTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05N3FNOTQBTTk0AU05NqVNOTf5TTk3%2FU05N%2F1NOTf9TTk3%2FU05N%2F1NOTf9TTk3%2FU05NqVNOTQD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwBTTk0AU05NAFNOTTlTTk29U05N51NOTdxTTk0AU05NAFNOTXZTTk3cU05N9lNOTf9TTk37U05N51NOTXVTTk0AU05NAFNOTQBTTk0AU05NOFNOTc9TTk32U05N%2F1NOTf9TTk3wU05Nz1NOTThTTk0AU05NAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8D4HwPA%2BB8DwPgfA8A4BwPAAAADwAAAA4AAAAeBAGAH8cH4H%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F"
        }
      ]
   }]
 }
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -181,16 +181,19 @@ let gPermissionObject = {
         return SitePermissions.SESSION;
 
       return SitePermissions.ALLOW;
     }
   },
 
   "desktop-notification": {},
 
+  "camera": {},
+  "microphone": {},
+
   "popup": {
     getDefault: function () {
       return Services.prefs.getBoolPref("dom.disable_open_during_load") ?
                SitePermissions.BLOCK : SitePermissions.ALLOW;
     }
   },
 
   "install": {
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -115,41 +115,62 @@ function prompt(aContentWindow, aCallID,
     requestType = "Microphone";
   else if (videoDevices.length)
     requestType = "Camera";
   else {
     denyRequest(aCallID, "NO_DEVICES_FOUND");
     return;
   }
 
-  let host = aContentWindow.document.documentURIObject.host;
+  let uri = aContentWindow.document.documentURIObject;
   let browser = getBrowserForWindow(aContentWindow);
   let chromeDoc = browser.ownerDocument;
   let chromeWin = chromeDoc.defaultView;
   let stringBundle = chromeWin.gNavigatorBundle;
   let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message",
-                                                [ host ]);
+                                                [ uri.host ]);
 
   let mainAction = {
     label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1,
                           stringBundle.getString("getUserMedia.shareSelectedDevices.label")),
     accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
     // The real callback will be set during the "showing" event. The
     // empty function here is so that PopupNotifications.show doesn't
     // reject the action.
     callback: function() {}
   };
 
-  let secondaryActions = [{
-    label: stringBundle.getString("getUserMedia.denyRequest.label"),
-    accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
-    callback: function () {
-      denyRequest(aCallID);
+  let secondaryActions = [
+    {
+      label: stringBundle.getString("getUserMedia.always.label"),
+      accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
+      callback: function () {
+        mainAction.callback(true);
+      }
+    },
+    {
+      label: stringBundle.getString("getUserMedia.denyRequest.label"),
+      accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
+      callback: function () {
+        denyRequest(aCallID);
+      }
+    },
+    {
+      label: stringBundle.getString("getUserMedia.never.label"),
+      accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
+      callback: function () {
+        denyRequest(aCallID);
+        let perms = Services.perms;
+        if (audioDevices.length)
+          perms.add(uri, "microphone", perms.DENY_ACTION);
+        if (videoDevices.length)
+          perms.add(uri, "camera", perms.DENY_ACTION);
+      }
     }
-  }];
+  ];
 
   let options = {
     eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "swapping")
         return true;
 
       if (aTopic != "showing")
         return false;
@@ -183,28 +204,39 @@ function prompt(aContentWindow, aCallID,
       listDevices(camMenupopup, videoDevices);
       listDevices(micMenupopup, audioDevices);
       if (requestType == "CameraAndMicrophone") {
         let stringBundle = chromeDoc.defaultView.gNavigatorBundle;
         addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1");
         addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
       }
 
-      this.mainAction.callback = function() {
+      this.mainAction.callback = function(aRemember) {
         let allowedDevices = Cc["@mozilla.org/supports-array;1"]
                                .createInstance(Ci.nsISupportsArray);
+        let perms = Services.perms;
         if (videoDevices.length) {
           let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value;
-          if (videoDeviceIndex != "-1")
+          let allowCamera = videoDeviceIndex != "-1";
+          if (allowCamera)
             allowedDevices.AppendElement(videoDevices[videoDeviceIndex]);
+          if (aRemember) {
+            perms.add(uri, "camera",
+                      allowCamera ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+          }
         }
         if (audioDevices.length) {
           let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
-          if (audioDeviceIndex != "-1")
+          let allowMic = audioDeviceIndex != "-1";
+          if (allowMic)
             allowedDevices.AppendElement(audioDevices[audioDeviceIndex]);
+          if (aRemember) {
+            perms.add(uri, "microphone",
+                      allowMic ? perms.ALLOW_ACTION : perms.DENY_ACTION);
+          }
         }
 
         if (allowedDevices.Count() == 0) {
           denyRequest(aCallID);
           return;
         }
 
         Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
@@ -247,30 +279,39 @@ function showBrowserSpecificIndicator(aB
     return;
   }
 
   let chromeWin = aBrowser.ownerDocument.defaultView;
   let stringBundle = chromeWin.gNavigatorBundle;
 
   let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
 
+  let uri = aBrowser.contentWindow.document.documentURIObject;
   let windowId = aBrowser.contentWindow
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils)
                          .currentInnerWindowID;
   let mainAction = {
     label: stringBundle.getString("getUserMedia.continueSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.continueSharing.accesskey"),
     callback: function () {},
     dismiss: true
   };
   let secondaryActions = [{
     label: stringBundle.getString("getUserMedia.stopSharing.label"),
     accessKey: stringBundle.getString("getUserMedia.stopSharing.accesskey"),
     callback: function () {
+      let perms = Services.perms;
+      if (hasVideo.value &&
+          perms.testExactPermission(uri, "camera") == perms.ALLOW_ACTION)
+        perms.remove(uri.host, "camera");
+      if (hasAudio.value &&
+          perms.testExactPermission(uri, "microphone") == perms.ALLOW_ACTION)
+        perms.remove(uri.host, "microphone");
+
       Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId);
     }
   }];
   let options = {
     hideNotNow: true,
     dismissed: true,
     eventCallback: function(aTopic) aTopic == "swapping"
   };
--- a/browser/themes/linux/preferences/aboutPermissions.css
+++ b/browser/themes/linux/preferences/aboutPermissions.css
@@ -92,16 +92,22 @@
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="plugins"] {
   list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png);
 }
 .pref-icon[type="fullscreen"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
+.pref-icon[type="camera"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
+.pref-icon[type="microphone"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
 
 .pref-title {
   font-size: 125%;
   margin-bottom: 0;
   font-weight: bold;
 }
 
 .pref-menulist {
--- a/browser/themes/osx/preferences/aboutPermissions.css
+++ b/browser/themes/osx/preferences/aboutPermissions.css
@@ -102,16 +102,22 @@
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="plugins"] {
   list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png);
 }
 .pref-icon[type="fullscreen"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
+.pref-icon[type="camera"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
+.pref-icon[type="microphone"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
 
 @media (min-resolution: 2dppx) {
   .pref-icon[type="geo"] {
     list-style-image: url(chrome://browser/skin/Geolocation-64@2x.png);
   }
 }
 
 .pref-title {
--- a/browser/themes/shared/devtools/common.css
+++ b/browser/themes/shared/devtools/common.css
@@ -127,44 +127,16 @@
 .devtools-autocomplete-listbox:focus.light-theme > richlistitem[selected] > .initial-value {
   color: #222;
 }
 
 .devtools-autocomplete-listbox.light-theme > richlistitem > label {
   color: #666;
 }
 
-/* Responsive container */
-
-.devtools-responsive-container {
-  -moz-box-orient: horizontal;
-}
-
-@media (max-width: 700px) {
-  .devtools-responsive-container {
-    -moz-box-orient: vertical;
-  }
-
-  .devtools-responsive-container > .devtools-side-splitter {
-    border: 0;
-    margin: 0;
-    border-top: 1px solid black;
-    min-height: 3px;
-    height: 3px;
-    margin-bottom: -3px;
-    /* In some edge case the cursor is not changed to n-resize */
-    cursor: n-resize;
-  }
-
-  .devtools-responsive-container > .devtools-sidebar-tabs {
-    min-height: 35vh;
-    max-height: 75vh;
-  }
-}
-
 /* Tooltip widget (see browser/devtools/shared/widgets/Tooltip.js) */
 
 .devtools-tooltip .panel-arrowcontent {
   padding: 4px;
 }
 
 .devtools-tooltip .panel-arrowcontainer {
   /* Reseting the transition used when panels are shown */
--- a/browser/themes/shared/devtools/dark-theme.css
+++ b/browser/themes/shared/devtools/dark-theme.css
@@ -310,15 +310,19 @@ div.CodeMirror span.eval-text {
 .devtools-horizontal-splitter {
   border-bottom: 1px solid black;
 }
 
 .devtools-side-splitter {
   -moz-border-end: 1px solid black;
 }
 
+.devtools-responsive-container > .devtools-side-splitter {
+  border-top: 1px solid black;
+}
+
 .devtools-textinput,
 .devtools-searchinput {
   background-color: rgba(24, 29, 32, 1);
   color: rgba(184, 200, 217, 1);
 }
 
 %include toolbars.inc.css
--- a/browser/themes/shared/devtools/light-theme.css
+++ b/browser/themes/shared/devtools/light-theme.css
@@ -309,9 +309,13 @@ div.CodeMirror span.eval-text {
 .devtools-horizontal-splitter {
   border-bottom: 1px solid #aaa;
 }
 
 .devtools-side-splitter {
   -moz-border-end: 1px solid #aaa;
 }
 
+.devtools-responsive-container > .devtools-side-splitter {
+  border-top: 1px solid #aaa;
+}
+
 %include toolbars.inc.css
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -11,16 +11,43 @@
      so both the left and right margins are set via js, while the start margin
      is always overridden here. */
 }
 
 .generic-toggled-side-pane[animated] {
   transition: margin 0.25s ease-in-out;
 }
 
+/* Responsive container */
+
+.devtools-responsive-container {
+  -moz-box-orient: horizontal;
+}
+
+@media (max-width: 700px) {
+  .devtools-responsive-container {
+    -moz-box-orient: vertical;
+  }
+
+  .devtools-responsive-container > .devtools-side-splitter {
+    border: 0;
+    margin: 0;
+    min-height: 3px;
+    height: 3px;
+    margin-bottom: -3px;
+    /* In some edge case the cursor is not changed to n-resize */
+    cursor: n-resize;
+  }
+
+  .devtools-responsive-container > .devtools-sidebar-tabs {
+    min-height: 35vh;
+    max-height: 75vh;
+  }
+}
+
 /* BreacrumbsWidget */
 
 .breadcrumbs-widget-container {
   -moz-margin-end: 3px;
   max-height: 25px; /* Set max-height for proper sizing on linux */
   height: 25px; /* Set height to prevent starting small waiting for content */
   /* A fake 1px-shadow is included in the border-images of the
      breadcrumbs-widget-items, to match toolbar-buttons style.
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -218,29 +218,23 @@
 }
 
 @media (-moz-windows-glass) {
   #main-window[sizemode=fullscreen]:not(:-moz-lwtheme) {
     -moz-appearance: none;
     background-color: #556;
   }
 
-  /* Use inverted icons for glassed toolbars */
-  #TabsToolbar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
-  #TabsToolbar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
+  /* Use inverted icons for non-fogged glassed toolbars */
   #toolbar-menubar > toolbarpaletteitem > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
   #toolbar-menubar > #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon:not(:-moz-lwtheme),
   #toolbar-menubar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
   #toolbar-menubar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
-  #TabsToolbar > toolbarpaletteitem > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
-  #TabsToolbar > toolbaritem > :-moz-any(@nestedButtons@):not(:-moz-lwtheme),
   #toolbar-menubar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
-  #toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
-  #TabsToolbar > toolbarpaletteitem > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme),
-  #TabsToolbar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) {
+  #toolbar-menubar > :-moz-any(@primaryToolbarButtons@):not(:-moz-lwtheme) {
     list-style-image: url("chrome://browser/skin/Toolbar-inverted.png");
   }
 
   /* Glass Fog */
 
   #TabsToolbar:not(:-moz-lwtheme) {
     background-image: none;
     position: relative;
--- a/browser/themes/windows/downloads/indicator-aero.css
+++ b/browser/themes/windows/downloads/indicator-aero.css
@@ -1,30 +1,28 @@
 /* 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/. */
 
-@media (-moz-windows-compositor) {
+@media (-moz-windows-glass) {
   /* The following rules are for the downloads indicator when in its normal,
      non-downloading, non-paused state (ie, it's just showing the downloads
      button icon). */
-  :-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme),
+  #toolbar-menubar #downloads-button:not([attention]) > #downloads-indicator-anchor > #downloads-indicator-icon:not(:-moz-lwtheme),
 
   /* The following rules are for the downloads indicator when in its paused
      or undetermined progress state. We use :not([counter]) as a shortcut for
      :-moz-any([progress], [paused]). */
 
-  /* This is the case where the downloads indicator has been moved next to the menubar as well as
-     the case where the downloads indicator is in the tabstrip toolbar. */
-  #toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter,
-  #TabsToolbar #downloads-button:not(:-moz-lwtheme):not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
+  /* This is the case where the downloads indicator has been moved next to the menubar. */
+  #toolbar-menubar #downloads-button:not([counter]) > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
     background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 0, 198, 18, 180);
   }
 
-  :-moz-any(#toolbar-menubar, #TabsToolbar) #downloads-indicator-counter:not(:-moz-lwtheme) {
+  #toolbar-menubar #downloads-indicator-counter:not(:-moz-lwtheme) {
     color: white;
     text-shadow: 0 0 1px rgba(0,0,0,.7),
                  0 1px 1.5px rgba(0,0,0,.5);
   }
 }
 
 #downloads-indicator-counter {
   margin-bottom: -1px;
--- a/browser/themes/windows/preferences/aboutPermissions.css
+++ b/browser/themes/windows/preferences/aboutPermissions.css
@@ -95,16 +95,22 @@
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="plugins"] {
   list-style-image: url(chrome://mozapps/skin/plugins/pluginGeneric.png);
 }
 .pref-icon[type="fullscreen"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
+.pref-icon[type="camera"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
+.pref-icon[type="microphone"] {
+  list-style-image: url(chrome://global/skin/icons/question-64.png);
+}
 
 .pref-title {
   font-size: 125%;
   margin-bottom: 0;
   font-weight: bold;
 }
 
 .pref-menulist {
--- a/build/mobile/robocop/Makefile.in
+++ b/build/mobile/robocop/Makefile.in
@@ -40,16 +40,17 @@ java-tests   := \
 PP_TARGETS         += testconstants
 testconstants-dep  := $(dir-tests)/TestConstants.java
 testconstants      := $(TESTPATH)/TestConstants.java.in
 testconstants_PATH := $(dir-tests)
 
 PP_TARGETS        += manifest
 manifest          := $(srcdir)/AndroidManifest.xml.in
 manifest_TARGET   := AndroidManifest.xml
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
 
 # Install robocop configs and helper
 INSTALL_TARGETS += robocop
 robocop_TARGET  := libs
 robocop_DEST    := $(CURDIR)
 robocop_FILES   := \
   $(TESTPATH)/robocop.ini \
   $(TESTPATH)/robocop_autophone.ini \
--- a/build/mobile/robocop/moz.build
+++ b/build/mobile/robocop/moz.build
@@ -6,17 +6,17 @@
 
 DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
 
 main = add_android_eclipse_project('Robocop', OBJDIR + '/AndroidManifest.xml')
 main.package_name = 'org.mozilla.roboexample.test'
 main.res = SRCDIR + '/res'
 main.recursive_make_targets += [
     OBJDIR + '/AndroidManifest.xml',
-    TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java']
+    '../../../mobile/android/base/tests/TestConstants.java']
 main.extra_jars += [SRCDIR + '/robotium-solo-4.3.1.jar']
 main.assets = TOPSRCDIR + '/mobile/android/base/tests/assets'
 main.referenced_projects += ['Fennec']
 
 main.add_classpathentry('harness', SRCDIR,
     dstdir='harness/org/mozilla/gecko')
 main.add_classpathentry('src', TOPSRCDIR + '/mobile/android/base/tests',
     dstdir='src/org/mozilla/gecko/tests')
--- a/config/makefiles/java-build.mk
+++ b/config/makefiles/java-build.mk
@@ -14,37 +14,40 @@ export:: classes
 classes: $(call mkdir_deps,classes)
 endif #} JAVAFILES
 
 
 ifdef ANDROID_APK_NAME #{
 android_res_dirs := $(addprefix $(srcdir)/,$(or $(ANDROID_RES_DIRS),res))
 _ANDROID_RES_FLAG := $(addprefix -S ,$(android_res_dirs))
 _ANDROID_ASSETS_FLAG := $(addprefix -A ,$(ANDROID_ASSETS_DIR))
+android_manifest := $(or $(ANDROID_MANIFEST_FILE),AndroidManifest.xml)
 
 GENERATED_DIRS += classes
 
 classes.dex: $(call mkdir_deps,classes)
 classes.dex: R.java
 classes.dex: $(ANDROID_APK_NAME).ap_
+classes.dex: $(ANDROID_EXTRA_JARS)
 classes.dex: $(JAVAFILES)
-	$(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^)
+	$(JAVAC) $(JAVAC_FLAGS) -d classes $(filter %.java,$^) \
+		$(if $(strip $(ANDROID_EXTRA_JARS)),-classpath $(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS))))
 	$(DX) --dex --output=$@ classes $(ANDROID_EXTRA_JARS)
 
 # R.java and $(ANDROID_APK_NAME).ap_ are both produced by aapt.  To
 # save an aapt invocation, we produce them both at the same time.
 
 R.java: .aapt.deps
 $(ANDROID_APK_NAME).ap_: .aapt.deps
 
 # This uses the fact that Android resource directories list all
 # resource files one subdirectory below the parent resource directory.
 android_res_files := $(wildcard $(addsuffix /*,$(wildcard $(addsuffix /*,$(android_res_dirs)))))
 
-.aapt.deps: AndroidManifest.xml $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR))
+.aapt.deps: $(android_manifest) $(android_res_files) $(wildcard $(ANDROID_ASSETS_DIR))
 	$(AAPT) package -f -M $< -I $(ANDROID_SDK)/android.jar $(_ANDROID_RES_FLAG) $(_ANDROID_ASSETS_FLAG) \
 		-J ${@D} \
 		-F $(ANDROID_APK_NAME).ap_
 	@$(TOUCH) $@
 
 $(ANDROID_APK_NAME)-unsigned-unaligned.apk: $(ANDROID_APK_NAME).ap_ classes.dex
 	cp $< $@
 	$(ZIP) -0 $@ classes.dex
@@ -61,19 +64,16 @@ GARBAGE += \
   classes.dex  \
   $(ANDROID_APK_NAME).ap_ \
   $(ANDROID_APK_NAME)-unsigned-unaligned.apk \
   $(ANDROID_APK_NAME)-unaligned.apk \
   $(ANDROID_APK_NAME).apk \
   $(NULL)
 
 JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar
-ifdef ANDROID_EXTRA_JARS #{
-JAVA_CLASSPATH := $(JAVA_CLASSPATH):$(subst $(NULL) ,:,$(strip $(ANDROID_EXTRA_JARS)))
-endif #} ANDROID_EXTRA_JARS
 
 # Include Android specific java flags, instead of what's in rules.mk.
 include $(topsrcdir)/config/android-common.mk
 endif #} ANDROID_APK_NAME
 
 
 ifdef JAVA_JAR_TARGETS #{
 # Arg 1: Output target name with .jar suffix, like jars/jarfile.jar.
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -9,16 +9,17 @@
 #include "nsHashPropertyBag.h"
 #ifdef MOZ_WIDGET_GONK
 #include "nsIAudioManager.h"
 #endif
 #include "nsIDOMFile.h"
 #include "nsIEventTarget.h"
 #include "nsIUUIDGenerator.h"
 #include "nsIScriptGlobalObject.h"
+#include "nsIPermissionManager.h"
 #include "nsIPopupWindowManager.h"
 #include "nsISupportsArray.h"
 #include "nsIDocShell.h"
 #include "nsIDocument.h"
 #include "nsISupportsPrimitives.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "mozilla/dom/ContentChild.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
@@ -28,18 +29,16 @@
 
 // For PR_snprintf
 #include "prprf.h"
 
 #include "nsJSUtils.h"
 #include "nsDOMFile.h"
 #include "nsGlobalWindow.h"
 
-#include "mozilla/Preferences.h"
-
 /* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */
 #include "MediaEngineDefault.h"
 #if defined(MOZ_WEBRTC)
 #include "MediaEngineWebRTC.h"
 #endif
 
 #ifdef MOZ_B2G
 #include "MediaPermissionGonk.h"
@@ -903,16 +902,23 @@ public:
       // MUST happen after ErrorCallbackRunnable Run()s, as it checks the active window list
       NS_DispatchToMainThread(new GetUserMediaListenerRemove(mWindowID, mListener));
     }
 
     return NS_OK;
   }
 
   nsresult
+  SetContraints(const MediaStreamConstraintsInternal& aConstraints)
+  {
+    mConstraints = aConstraints;
+    return NS_OK;
+  }
+
+  nsresult
   SetAudioDevice(MediaDevice* aAudioDevice)
   {
     mAudioDevice = aAudioDevice;
     mDeviceChosen = true;
     return NS_OK;
   }
 
   nsresult
@@ -1070,17 +1076,21 @@ public:
     , mLoopbackAudioDevice(aAudioLoopbackDev)
     , mLoopbackVideoDevice(aVideoLoopbackDev) {}
 
   NS_IMETHOD
   Run()
   {
     NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
 
-    MediaEngine *backend = mManager->GetBackend(mWindowId);
+    nsRefPtr<MediaEngine> backend;
+    if (mConstraints.mFake)
+      backend = new MediaEngineDefault();
+    else
+      backend = mManager->GetBackend(mWindowId);
 
     ScopedDeletePtr<SourceSet> final (GetSources(backend, mConstraints.mVideom,
                                           &MediaEngine::EnumerateVideoDevices,
                                           mLoopbackVideoDevice));
     {
       ScopedDeletePtr<SourceSet> s (GetSources(backend, mConstraints.mAudiom,
                                         &MediaEngine::EnumerateAudioDevices,
                                         mLoopbackAudioDevice));
@@ -1410,24 +1420,69 @@ MediaManager::GetUserMedia(JSContext* aC
   if (c.mPicture) {
     // ShowFilePickerForMimeType() must run on the Main Thread! (on Android)
     runnable->Arm();
     NS_DispatchToMainThread(runnable);
     return NS_OK;
   }
 #endif
   // XXX No full support for picture in Desktop yet (needs proper UI)
-  if (aPrivileged || c.mFake) {
+  if (aPrivileged ||
+      (c.mFake && !Preferences::GetBool("media.navigator.permission.fake"))) {
     runnable->Arm();
     mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
   } else {
+    // Check if this site has persistent permissions.
+    nsresult rv;
+    nsCOMPtr<nsIPermissionManager> permManager =
+      do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    uint32_t audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
+    if (c.mAudio) {
+      rv = permManager->TestExactPermissionFromPrincipal(
+        aWindow->GetExtantDoc()->NodePrincipal(), "microphone", &audioPerm);
+      NS_ENSURE_SUCCESS(rv, rv);
+      if (audioPerm == nsIPermissionManager::PROMPT_ACTION) {
+        audioPerm = nsIPermissionManager::UNKNOWN_ACTION;
+      }
+    }
+
+    uint32_t videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
+    if (c.mVideo) {
+      rv = permManager->TestExactPermissionFromPrincipal(
+        aWindow->GetExtantDoc()->NodePrincipal(), "camera", &videoPerm);
+      NS_ENSURE_SUCCESS(rv, rv);
+      if (videoPerm == nsIPermissionManager::PROMPT_ACTION) {
+        videoPerm = nsIPermissionManager::UNKNOWN_ACTION;
+      }
+    }
+
+    if ((!c.mAudio || audioPerm) && (!c.mVideo || videoPerm)) {
+      // All permissions we were about to request already have a saved value.
+      if (c.mAudio && audioPerm == nsIPermissionManager::DENY_ACTION) {
+        c.mAudio = false;
+        runnable->SetContraints(c);
+      }
+      if (c.mVideo && videoPerm == nsIPermissionManager::DENY_ACTION) {
+        c.mVideo = false;
+        runnable->SetContraints(c);
+      }
+
+      runnable->Arm();
+      if (!c.mAudio && !c.mVideo) {
+        return runnable->Denied(NS_LITERAL_STRING("PERMISSION_DENIED"));
+      }
+
+      return mMediaThread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+    }
+
     // Ask for user permission, and dispatch runnable (or not) when a response
     // is received via an observer notification. Each call is paired with its
     // runnable by a GUID.
-    nsresult rv;
     nsCOMPtr<nsIUUIDGenerator> uuidgen =
       do_GetService("@mozilla.org/uuid-generator;1", &rv);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Generate a call ID.
     nsID id;
     rv = uuidgen->GenerateUUIDInPlace(&id);
     NS_ENSURE_SUCCESS(rv, rv);
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -14,16 +14,17 @@
 #include "nsObserverService.h"
 #include "nsIPrefService.h"
 #include "nsIPrefBranch.h"
 
 #include "nsPIDOMWindow.h"
 #include "nsIDOMNavigatorUserMedia.h"
 #include "nsXULAppAPI.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/Preferences.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
 #include "prlog.h"
 #include "DOMMediaStream.h"
 
 #ifdef MOZ_WEBRTC
 #include "mtransport/runnable_utils.h"
 #endif
@@ -97,22 +98,26 @@ public:
     return mStream->AsSourceStream();
   }
 
   // mVideo/AudioSource are set by Activate(), so we assume they're capturing
   // if set and represent a real capture device.
   bool CapturingVideo()
   {
     NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
-    return mVideoSource && !mVideoSource->IsFake() && !mStopped;
+    return mVideoSource && !mStopped &&
+           (!mVideoSource->IsFake() ||
+            Preferences::GetBool("media.navigator.permission.fake"));
   }
   bool CapturingAudio()
   {
     NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
-    return mAudioSource && !mAudioSource->IsFake() && !mStopped;
+    return mAudioSource && !mStopped &&
+           (!mAudioSource->IsFake() ||
+            Preferences::GetBool("media.navigator.permission.fake"));
   }
 
   void SetStopped()
   {
     mStopped = true;
   }
 
   // implement in .cpp to avoid circular dependency with MediaOperationRunnable
--- a/dom/system/OSFileConstants.cpp
+++ b/dom/system/OSFileConstants.cpp
@@ -696,16 +696,17 @@ static const dom::ConstantSpec gWinPrope
   INT_CONSTANT(INVALID_FILE_ATTRIBUTES),
 
   // GetNamedSecurityInfo and SetNamedSecurityInfo constants
   INT_CONSTANT(UNPROTECTED_DACL_SECURITY_INFORMATION),
   INT_CONSTANT(SE_FILE_OBJECT),
   INT_CONSTANT(DACL_SECURITY_INFORMATION),
 
   // Errors
+  INT_CONSTANT(ERROR_INVALID_HANDLE),
   INT_CONSTANT(ERROR_ACCESS_DENIED),
   INT_CONSTANT(ERROR_DIR_NOT_EMPTY),
   INT_CONSTANT(ERROR_FILE_EXISTS),
   INT_CONSTANT(ERROR_ALREADY_EXISTS),
   INT_CONSTANT(ERROR_FILE_NOT_FOUND),
   INT_CONSTANT(ERROR_NO_MORE_FILES),
   INT_CONSTANT(ERROR_PATH_NOT_FOUND),
 
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -36,16 +36,17 @@ import org.mozilla.gecko.home.BrowserSea
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfigInvalidator;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
+import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.StringUtils;
@@ -86,16 +87,17 @@ import android.view.MotionEvent;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.animation.Interpolator;
 import android.widget.RelativeLayout;
+import android.widget.ListView;
 import android.widget.Toast;
 import android.widget.ViewFlipper;
 
 abstract public class BrowserApp extends GeckoApp
                                  implements TabsPanel.TabsLayoutChangeListener,
                                             PropertyAnimator.PropertyAnimationListener,
                                             View.OnKeyListener,
                                             GeckoLayerClient.OnMetricsChangedListener,
@@ -501,16 +503,25 @@ abstract public class BrowserApp extends
         });
 
         mBrowserToolbar.setOnFilterListener(new BrowserToolbar.OnFilterListener() {
             public void onFilter(String searchText, AutocompleteHandler handler) {
                 filterEditingMode(searchText, handler);
             }
         });
 
+        mBrowserToolbar.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (isHomePagerVisible()) {
+                    mHomePager.onToolbarFocusChange(hasFocus);
+                }
+            }
+        });
+
         mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
             public void onStartEditing() {
                 // Temporarily disable doorhanger notifications.
                 mDoorHangerPopup.disable();
             }
         });
 
         mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
@@ -2409,17 +2420,17 @@ abstract public class BrowserApp extends
         if (type == GuestModeDialog.ENTERING) {
             titleString = R.string.new_guest_session_title;
             msgString = R.string.new_guest_session_text;
         } else {
             titleString = R.string.exit_guest_session_title;
             msgString = R.string.exit_guest_session_text;
         }
 
-        ps.show(res.getString(titleString), res.getString(msgString), null, false);
+        ps.show(res.getString(titleString), res.getString(msgString), null, ListView.CHOICE_MODE_NONE);
     }
 
     public void subscribeToFeeds(Tab tab) {
         if (!tab.hasFeeds()) {
             return;
         }
 
         JSONObject args = new JSONObject();
--- a/mobile/android/base/EventDispatcher.java
+++ b/mobile/android/base/EventDispatcher.java
@@ -101,18 +101,22 @@ public final class EventDispatcher {
         }
 
     }
 
     public static void sendResponse(JSONObject message, JSONObject response) {
         try {
             response.put(GUID, message.getString(GUID));
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Return", response.toString()));
-        } catch(Exception ex) { }
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "Unable to send response", ex);
+        }
     }
 
     public static void sendError(JSONObject message, JSONObject error) {
         try {
             error.put(GUID, message.getString(GUID));
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(message.getString("type") + ":Error", error.toString()));
-        } catch(Exception ex) { }
+        } catch (Exception ex) {
+            Log.e(LOGTAG, "Unable to send error", ex);
+        }
     }
 }
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -411,22 +411,18 @@ public abstract class GeckoApp
             return onCreateOptionsMenu(menu);
         }
 
         return super.onCreatePanelMenu(featureId, menu);
     }
 
     @Override
     public boolean onPreparePanel(int featureId, View view, Menu menu) {
-        if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL) {
-            if (menu instanceof GeckoMenu) {
-                ((GeckoMenu) menu).refresh();
-            }
+        if (Build.VERSION.SDK_INT >= 11 && featureId == Window.FEATURE_OPTIONS_PANEL)
             return onPrepareOptionsMenu(menu);
-        }
 
         return super.onPreparePanel(featureId, view, menu);
     }
 
     @Override
     public boolean onMenuOpened(int featureId, Menu menu) {
         // exit full-screen mode whenever the menu is opened
         if (mLayerView != null && mLayerView.isFullScreen()) {
@@ -681,16 +677,46 @@ public abstract class GeckoApp
                 }
                 JSONObject handlersJSON = new JSONObject();
                 handlersJSON.put("apps", new JSONArray(appList));
                 EventDispatcher.sendResponse(message, handlersJSON);
             } else if (event.equals("Intent:Open")) {
                 GeckoAppShell.openUriExternal(message.optString("url"),
                     message.optString("mime"), message.optString("packageName"),
                     message.optString("className"), message.optString("action"), message.optString("title"));
+            } else if (event.equals("Intent:OpenForResult")) {
+                Intent intent = GeckoAppShell.getOpenURIIntent(this,
+                                                               message.optString("url"),
+                                                               message.optString("mime"),
+                                                               message.optString("action"),
+                                                               message.optString("title"));
+                intent.setClassName(message.optString("packageName"), message.optString("className"));
+
+                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+                final JSONObject originalMessage = message;
+                ActivityHandlerHelper.startIntentForActivity(this,
+                                                             intent,
+                        new ActivityResultHandler() {
+                            @Override
+                            public void onActivityResult (int resultCode, Intent data) {
+                                JSONObject response = new JSONObject();
+
+                                try {
+                                    if (data != null) {
+                                        response.put("extras", bundleToJSON(data.getExtras()));
+                                    }
+                                    response.put("resultCode", resultCode);
+                                } catch (JSONException e) {
+                                    Log.w(LOGTAG, "Error building JSON response.", e);
+                                }
+
+                                EventDispatcher.sendResponse(originalMessage, response);
+                            }
+                        });
             } else if (event.equals("Locale:Set")) {
                 setLocale(message.getString("locale"));
             } else if (event.equals("NativeApp:IsDebuggable")) {
                 JSONObject ret = new JSONObject();
                 ret.put("isDebuggable", getIsDebuggable() ? "true" : "false");
                 EventDispatcher.sendResponse(message, ret);
             } else if (event.equals("SystemUI:Visibility")) {
                 setSystemUiVisible(message.getBoolean("visible"));
@@ -841,16 +867,33 @@ public abstract class GeckoApp
                             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
                         }
                     }
                 });
             }
         });
     }
 
+    private JSONObject bundleToJSON(Bundle bundle) {
+        JSONObject json = new JSONObject();
+        if (bundle == null) {
+            return json;
+        }
+
+        for (String key : bundle.keySet()) {
+            try {
+                json.put(key, bundle.get(key));
+            } catch (JSONException e) {
+                Log.w(LOGTAG, "Error building JSON response.", e);
+            }
+        }
+
+        return json;
+    }
+
     private void addFullScreenPluginView(View view) {
         if (mFullScreenPluginView != null) {
             Log.w(LOGTAG, "Already have a fullscreen plugin view");
             return;
         }
 
         setFullScreen(true);
 
@@ -1532,16 +1575,17 @@ public abstract class GeckoApp
         registerEventListener("Image:SetAs");
         registerEventListener("Sanitize:ClearHistory");
         registerEventListener("Update:Check");
         registerEventListener("Update:Download");
         registerEventListener("Update:Install");
         registerEventListener("PrivateBrowsing:Data");
         registerEventListener("Contact:Add");
         registerEventListener("Intent:Open");
+        registerEventListener("Intent:OpenForResult");
         registerEventListener("Intent:GetHandlers");
         registerEventListener("Locale:Set");
         registerEventListener("NativeApp:IsDebuggable");
         registerEventListener("SystemUI:Visibility");
 
         EventListener.registerEvents();
 
         if (SmsManager.getInstance() != null) {
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -64,17 +64,16 @@ public class GeckoApplication extends Ap
 
         mLightweightTheme = new LightweightTheme(this);
 
         GeckoConnectivityReceiver.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().init(getApplicationContext());
         GeckoBatteryManager.getInstance().start();
         GeckoNetworkManager.getInstance().init(getApplicationContext());
         MemoryMonitor.getInstance().init(getApplicationContext());
-        HomeConfigInvalidator.getInstance().init(getApplicationContext());
 
         mInited = true;
     }
 
     public void onActivityPause(GeckoActivityStatus activity) {
         mInBackground = true;
 
         if ((activity.isFinishing() == false) &&
@@ -111,16 +110,17 @@ public class GeckoApplication extends Ap
     }
 
     @Override
     public void onCreate() {
         HardwareUtils.init(getApplicationContext());
         Clipboard.init(getApplicationContext());
         FilePicker.init(getApplicationContext());
         GeckoLoader.loadMozGlue();
+        HomeConfigInvalidator.getInstance().init(getApplicationContext());
         super.onCreate();
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
     public LightweightTheme getLightweightTheme() {
--- a/mobile/android/base/home/HomeBanner.java
+++ b/mobile/android/base/home/HomeBanner.java
@@ -14,16 +14,17 @@ import org.mozilla.gecko.animation.Prope
 import org.mozilla.gecko.animation.PropertyAnimator.Property;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.os.Build;
 import android.text.Html;
 import android.text.Spanned;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
@@ -100,21 +101,32 @@ public class HomeBanner extends LinearLa
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
 
         GeckoAppShell.getEventDispatcher().unregisterEventListener("HomeBanner:Data", this);
-     }
+    }
 
-     public void setScrollingPages(boolean scrollingPages) {
-         mScrollingPages = scrollingPages;
-     }
+    @Override
+    public void setVisibility(int visibility) {
+        // On pre-Honeycomb devices, setting the visibility to GONE won't actually
+        // hide the view unless we clear animations first.
+        if (Build.VERSION.SDK_INT < 11 && visibility == View.GONE) {
+            clearAnimation();
+        }
+
+        super.setVisibility(visibility);
+    }
+
+    public void setScrollingPages(boolean scrollingPages) {
+        mScrollingPages = scrollingPages;
+    }
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
             // Store the current message id to pass back to JS in the view's OnClickListener.
             setTag(message.getString("id"));
 
             // Display styled text from an HTML string.
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -239,16 +239,22 @@ public class HomePager extends ViewPager
     public boolean dispatchTouchEvent(MotionEvent event) {
         if (mHomeBanner != null) {
             mHomeBanner.handleHomeTouch(event);
         }
 
         return super.dispatchTouchEvent(event);
     }
 
+    public void onToolbarFocusChange(boolean hasFocus) {
+        // We should only enable the banner if the toolbar is not focused and we are on the default page
+        final boolean enabled = !hasFocus && getCurrentItem() == mDefaultPageIndex;
+        mHomeBanner.setEnabled(enabled);
+    }
+
     private void updateUiFromPanelConfigs(List<PanelConfig> panelConfigs) {
         // We only care about the adapter if HomePager is currently
         // loaded, which means it's visible in the activity.
         if (!mLoaded) {
             return;
         }
 
         if (mDecor != null) {
--- a/mobile/android/base/home/PanelGridView.java
+++ b/mobile/android/base/home/PanelGridView.java
@@ -22,54 +22,45 @@ import android.widget.AdapterView;
 import android.widget.GridView;
 
 public class PanelGridView extends GridView
                            implements DatasetBacked, PanelView {
     private static final String LOGTAG = "GeckoPanelGridView";
 
     private final ViewConfig mViewConfig;
     private final PanelViewAdapter mAdapter;
-    protected OnUrlOpenListener mUrlOpenListener;
+    private PanelViewUrlHandler mUrlHandler;
 
     public PanelGridView(Context context, ViewConfig viewConfig) {
         super(context, null, R.attr.panelGridViewStyle);
+
         mViewConfig = viewConfig;
+        mUrlHandler = new PanelViewUrlHandler(viewConfig);
+
         mAdapter = new PanelViewAdapter(context, viewConfig.getItemType());
         setAdapter(mAdapter);
+
         setOnItemClickListener(new PanelGridItemClickListener());
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
-        mUrlOpenListener = null;
+        mUrlHandler.setOnUrlOpenListener(null);
     }
 
     @Override
     public void setDataset(Cursor cursor) {
         mAdapter.swapCursor(cursor);
     }
 
     @Override
     public void setOnUrlOpenListener(OnUrlOpenListener listener) {
-        mUrlOpenListener = listener;
+        mUrlHandler.setOnUrlOpenListener(listener);
     }
 
     private class PanelGridItemClickListener implements AdapterView.OnItemClickListener {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            Cursor cursor = mAdapter.getCursor();
-            if (cursor == null || !cursor.moveToPosition(position)) {
-                throw new IllegalStateException("Couldn't move cursor to position " + position);
-            }
-
-            int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
-            final String url = cursor.getString(urlIndex);
-
-            EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
-            if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
-                flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
-            }
-
-            mUrlOpenListener.onUrlOpen(url, flags);
+            mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position);
         }
     }
 }
--- a/mobile/android/base/home/PanelItemView.java
+++ b/mobile/android/base/home/PanelItemView.java
@@ -61,17 +61,16 @@ class PanelItemView extends LinearLayout
 
         // Only try to load the image if the item has define image URL
         final boolean hasImageUrl = !TextUtils.isEmpty(imageUrl);
         mImage.setVisibility(hasImageUrl ? View.VISIBLE : View.GONE);
 
         if (hasImageUrl) {
             Picasso.with(getContext())
                    .load(imageUrl)
-                   .error(R.drawable.favicon)
                    .into(mImage);
         }
     }
 
     private static class ArticleItemView extends PanelItemView {
         private ArticleItemView(Context context) {
             super(context, R.layout.panel_article_item);
             setOrientation(LinearLayout.HORIZONTAL);
--- a/mobile/android/base/home/PanelListView.java
+++ b/mobile/android/base/home/PanelListView.java
@@ -22,43 +22,41 @@ import android.widget.AdapterView;
 
 public class PanelListView extends HomeListView
                            implements DatasetBacked, PanelView {
 
     private static final String LOGTAG = "GeckoPanelListView";
 
     private final PanelViewAdapter mAdapter;
     private final ViewConfig mViewConfig;
+    private final PanelViewUrlHandler mUrlHandler;
 
     public PanelListView(Context context, ViewConfig viewConfig) {
         super(context);
+
         mViewConfig = viewConfig;
+        mUrlHandler = new PanelViewUrlHandler(viewConfig);
+
         mAdapter = new PanelViewAdapter(context, viewConfig.getItemType());
         setAdapter(mAdapter);
+
         setOnItemClickListener(new PanelListItemClickListener());
     }
 
     @Override
     public void setDataset(Cursor cursor) {
         Log.d(LOGTAG, "Setting dataset: " + mViewConfig.getDatasetId());
         mAdapter.swapCursor(cursor);
     }
 
+    @Override
+    public void setOnUrlOpenListener(OnUrlOpenListener listener) {
+        super.setOnUrlOpenListener(listener);
+        mUrlHandler.setOnUrlOpenListener(listener);
+    }
+
     private class PanelListItemClickListener implements AdapterView.OnItemClickListener {
         @Override
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            Cursor cursor = mAdapter.getCursor();
-            if (cursor == null || !cursor.moveToPosition(position)) {
-                throw new IllegalStateException("Couldn't move cursor to position " + position);
-            }
-
-            int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
-            final String url = cursor.getString(urlIndex);
-
-            EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
-            if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
-                flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
-            }
-
-            mUrlOpenListener.onUrlOpen(url, flags);
+            mUrlHandler.openUrlAtPosition(mAdapter.getCursor(), position);
         }
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/PanelViewUrlHandler.java
@@ -0,0 +1,46 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.db.BrowserContract.HomeItems;
+import org.mozilla.gecko.home.HomeConfig.ItemHandler;
+import org.mozilla.gecko.home.HomeConfig.ViewConfig;
+import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+
+import android.database.Cursor;
+
+import java.util.EnumSet;
+
+class PanelViewUrlHandler {
+    private final ViewConfig mViewConfig;
+    private OnUrlOpenListener mUrlOpenListener;
+
+    public PanelViewUrlHandler(ViewConfig viewConfig) {
+        mViewConfig = viewConfig;
+    }
+
+    public void setOnUrlOpenListener(OnUrlOpenListener listener) {
+        mUrlOpenListener = listener;
+    }
+
+    public void openUrlAtPosition(Cursor cursor, int position) {
+        if (cursor == null || !cursor.moveToPosition(position)) {
+            throw new IllegalStateException("Couldn't move cursor to position " + position);
+        }
+
+        int urlIndex = cursor.getColumnIndexOrThrow(HomeItems.URL);
+        final String url = cursor.getString(urlIndex);
+
+        EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
+        if (mViewConfig.getItemHandler() == ItemHandler.INTENT) {
+            flags.add(OnUrlOpenListener.Flags.OPEN_WITH_INTENT);
+        }
+
+        if (mUrlOpenListener != null) {
+            mUrlOpenListener.onUrlOpen(url, flags);
+        }
+    }
+}
--- a/mobile/android/base/menu/GeckoMenu.java
+++ b/mobile/android/base/menu/GeckoMenu.java
@@ -21,17 +21,16 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
 public class GeckoMenu extends ListView 
                        implements Menu,
                                   AdapterView.OnItemClickListener,
                                   GeckoMenuItem.OnShowAsActionChangedListener {
     private static final String LOGTAG = "GeckoMenu";
@@ -632,28 +631,16 @@ public class GeckoMenu extends ListView
             if (indexOfChild(actionItem) != -1) {
                 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) actionItem.getLayoutParams();
                 mWeightSum -= params.weight;
                 removeView(actionItem);
             }
         }
     }
 
-    public void refresh() {
-        for (Iterator<GeckoMenuItem> i = mPrimaryActionItems.keySet().iterator(); i.hasNext();) {
-            GeckoMenuItem item = i.next();
-            item.refreshIfChanged();
-        }
-
-        for (Iterator<GeckoMenuItem> i = mSecondaryActionItems.keySet().iterator(); i.hasNext();) {
-            GeckoMenuItem item = i.next();
-            item.refreshIfChanged();
-        }
-    }
-
     // Adapter to bind menu items to the list.
     private class MenuItemsAdapter extends BaseAdapter {
         private static final int VIEW_TYPE_DEFAULT = 0;
         private static final int VIEW_TYPE_ACTION_MODE = 1;
 
         private List<GeckoMenuItem> mItems;
 
         public MenuItemsAdapter() {
@@ -742,21 +729,18 @@ public class GeckoMenu extends ListView
 
         @Override
         public boolean hasStableIds() {
             return false;
         }
 
         @Override
         public boolean areAllItemsEnabled() {
-            for (GeckoMenuItem item : mItems) {
-                 if (!item.isEnabled())
-                     return false;
-            }
-
+            // Setting this to true is a workaround to fix disappearing
+            // dividers in the menu (bug 963249).
             return true;
         }
 
         @Override
         public boolean isEnabled(int position) {
             return getItem(position).isEnabled();
         }
 
--- a/mobile/android/base/menu/GeckoMenuItem.java
+++ b/mobile/android/base/menu/GeckoMenuItem.java
@@ -217,27 +217,16 @@ public class GeckoMenuItem implements Me
                 }
             });
         }
 
         mShowAsActionChangedListener.onShowAsActionChanged(this);
         return this;
     }
 
-    public void refreshIfChanged() {
-        if (mActionProvider == null)
-            return;
-
-        if (mActionProvider instanceof GeckoActionProvider) {
-            if (((GeckoActionProvider) mActionProvider).hasChanged()) {
-                mShowAsActionChangedListener.onShowAsActionChanged(GeckoMenuItem.this);
-            }
-        }
-    }
-
     @Override
     public MenuItem setActionView(int resId) {
         return this;
     }
 
     @Override
     public MenuItem setActionView(View view) {
         return this;
--- a/mobile/android/base/menu/MenuItemDefault.java
+++ b/mobile/android/base/menu/MenuItemDefault.java
@@ -43,17 +43,17 @@ public class MenuItemDefault extends Tex
         int width = res.getDimensionPixelSize(R.dimen.menu_item_row_width);
         int height = res.getDimensionPixelSize(R.dimen.menu_item_row_height);
         setMinimumWidth(width);
         setMinimumHeight(height);
 
         int stateIconSize = res.getDimensionPixelSize(R.dimen.menu_item_state_icon);
         Rect stateIconBounds = new Rect(0, 0, stateIconSize, stateIconSize);
 
-        mState = res.getDrawable(R.drawable.menu_item_state);
+        mState = res.getDrawable(R.drawable.menu_item_state).mutate();
         mState.setBounds(stateIconBounds);
 
         if (sIconBounds == null) {
             int iconSize = res.getDimensionPixelSize(R.dimen.menu_item_icon);
             sIconBounds = new Rect(0, 0, iconSize, iconSize);
         }
 
         setCompoundDrawables(mIcon, null, mState, null);
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -234,16 +234,17 @@ gbjar.sources += [
     'home/MostRecentPanel.java',
     'home/MultiTypeCursorAdapter.java',
     'home/PanelGridView.java',
     'home/PanelItemView.java',
     'home/PanelLayout.java',
     'home/PanelListView.java',
     'home/PanelManager.java',
     'home/PanelViewAdapter.java',
+    'home/PanelViewUrlHandler.java',
     'home/PinSiteDialog.java',
     'home/ReadingListPanel.java',
     'home/SearchEngine.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
     'home/SuggestClient.java',
     'home/TabMenuStrip.java',
@@ -293,16 +294,18 @@ gbjar.sources += [
     'preferences/SearchPreferenceCategory.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrivateTab.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
+    'prompts/PromptListAdapter.java',
+    'prompts/PromptListItem.java',
     'prompts/PromptService.java',
     'ReaderModeUtils.java',
     'ReferrerReceiver.java',
     'RemoteTabs.java',
     'Restarter.java',
     'ScrollAnimator.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
@@ -480,41 +483,60 @@ for var in ('ANDROID_PACKAGE_NAME', 'AND
 DEFINES['MANGLED_ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME'].replace('fennec', 'f3nn3c')
 DEFINES['MOZ_APP_ABI'] = CONFIG['TARGET_XPCOM_ABI']
 
 if '-march=armv7' in CONFIG['OS_CFLAGS']:
     DEFINES['MOZ_MIN_CPU_VERSION'] = 7
 else:
     DEFINES['MOZ_MIN_CPU_VERSION'] = 5
 
-generated = add_android_eclipse_library_project('FennecGeneratedResources')
-generated.package_name = 'org.mozilla.fennec.generatedresources'
+generated = add_android_eclipse_library_project('FennecResourcesGenerated')
+generated.package_name = 'org.mozilla.fennec.resources.generated'
 generated.res = OBJDIR + '/res'
 
-branding = add_android_eclipse_library_project('FennecBrandingResources')
-branding.package_name = 'org.mozilla.fennec.brandingresources'
+branding = add_android_eclipse_library_project('FennecResourcesBranding')
+branding.package_name = 'org.mozilla.fennec.resources.branding'
 branding.res = TOPSRCDIR + '/' + CONFIG['MOZ_BRANDING_DIRECTORY'] + '/res'
 
 main = add_android_eclipse_project('Fennec', OBJDIR + '/AndroidManifest.xml')
 main.package_name = 'org.mozilla.gecko'
-main.res = SRCDIR + '/resources'
 
 main.recursive_make_targets += ['.aapt.deps'] # Captures dependencies on Android manifest and all resources.
 main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in mgjar.generated_sources]
 main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in gbjar.generated_sources]
 
 main.included_projects += ['../' + generated.name, '../' + branding.name]
 main.referenced_projects += [generated.name, branding.name]
 main.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
 main.assets = TOPOBJDIR + '/dist/fennec/assets'
 main.libs = TOPOBJDIR + '/dist/fennec/lib'
+main.res = None
 
 cpe = main.add_classpathentry('src', SRCDIR,
     dstdir='src/org/mozilla/gecko',
     exclude_patterns=['org/mozilla/gecko/tests/**',
         'org/mozilla/gecko/resources/**'])
 if not CONFIG['MOZ_CRASHREPORTER']:
     cpe.exclude_patterns += ['org/mozilla/gecko/CrashReporter.java']
 main.add_classpathentry('generated', OBJDIR + '/generated',
     dstdir='generated')
 main.add_classpathentry('thirdparty', TOPSRCDIR + '/mobile/android/thirdparty',
     dstdir='thirdparty',
     ignore_warnings=True)
+
+resources = add_android_eclipse_library_project('FennecResources')
+resources.package_name = 'org.mozilla.fennec.resources'
+resources.res = SRCDIR + '/resources'
+resources.included_projects += ['../' + generated.name, '../' + branding.name]
+resources.referenced_projects += [generated.name, branding.name]
+
+main.included_projects += ['../' + resources.name]
+main.referenced_projects += [resources.name]
+
+if CONFIG['MOZ_CRASHREPORTER']:
+    crashreporter = add_android_eclipse_library_project('FennecResourcesCrashReporter')
+    crashreporter.package_name = 'org.mozilla.fennec.resources.crashreporter'
+    crashreporter.res = SRCDIR + '/crashreporter/res'
+    crashreporter.included_projects += ['../' + resources.name]
+    crashreporter.referenced_projects += [resources.name]
+
+    main.included_projects += ['../' + crashreporter.name]
+    main.referenced_projects += [crashreporter.name]
--- a/mobile/android/base/prompts/Prompt.java
+++ b/mobile/android/base/prompts/Prompt.java
@@ -31,28 +31,30 @@ import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.CheckedTextView;
 import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
+import java.util.ArrayList;
+
 public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener {
     private static final String LOGTAG = "GeckoPromptService";
 
     private String[] mButtons;
     private PromptInput[] mInputs;
-    private boolean[] mSelected;
     private AlertDialog mDialog;
 
     private final LayoutInflater mInflater;
     private final Context mContext;
     private PromptCallback mCallback;
     private String mGuid;
+    private PromptListAdapter mAdapter;
 
     private static boolean mInitialized = false;
     private static int mGroupPaddingSize;
     private static int mLeftRightTextWithIconPadding;
     private static int mTopBottomTextWithIconPadding;
     private static int mIconTextPadding;
     private static int mIconSize;
     private static int mInputPaddingSize;
@@ -79,42 +81,43 @@ public class Prompt implements OnClickLi
             mInitialized = true;
         }
     }
 
     private View applyInputStyle(View view, PromptInput input) {
         // Don't add padding to color picker views
         if (input.canApplyInputStyle()) {
             view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0);
-        }
+	}
         return view;
     }
 
     public void show(JSONObject message) {
         processMessage(message);
     }
 
-    public void show(String title, String text, PromptListItem[] listItems, boolean multipleSelection) {
+
+    public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
         ThreadUtils.assertOnUiThread();
 
         GeckoAppShell.getLayerView().abortPanning();
 
         AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
         if (!TextUtils.isEmpty(title)) {
             builder.setTitle(title);
         }
 
         if (!TextUtils.isEmpty(text)) {
             builder.setMessage(text);
         }
 
         // Because lists are currently added through the normal Android AlertBuilder interface, they're
         // incompatible with also adding additional input elements to a dialog.
         if (listItems != null && listItems.length > 0) {
-            addlistItems(builder, listItems, multipleSelection);
+            addListItems(builder, listItems, choiceMode);
         } else if (!addInputs(builder)) {
             // If we failed to add any requested input elements, don't show the dialog
             return;
         }
 
         int length = mButtons == null ? 0 : mButtons.length;
         if (length > 0) {
             builder.setPositiveButton(mButtons[0], this);
@@ -141,32 +144,35 @@ public class Prompt implements OnClickLi
 
     /* 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, 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++) {
-                    if (mSelected[i]) {
-                        selected.put(i);
-                    }
+            JSONArray selected = new JSONArray();
+
+            // If the button has already been filled in
+            ArrayList<Integer> selectedItems = mAdapter.getSelected();
+            for (Integer item : selectedItems) {
+                selected.put(item);
+            }
+
+            // If we haven't assigned a button yet, or we assigned it to -1, assign the which
+            // parameter to both selected and the button.
+            if (!result.has("button") || result.optInt("button") == -1) {
+                if (!selectedItems.contains(which)) {
+                    selected.put(which);
                 }
-                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);
             }
+
+            result.put("list", selected);
         } catch(JSONException ex) { }
     }
 
     /* Adds to a result value from the inputs that can be shown in dialogs.
      * Each input will set its own value in the result.
      */
     private void addInputValues(final JSONObject result) {
         try {
@@ -194,113 +200,101 @@ public class Prompt implements OnClickLi
         } catch(JSONException ex) { }
     }
 
     @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) {
+            if (mAdapter != null) {
                 addListResult(ret, which);
             }
         } catch(Exception ex) {
             Log.i(LOGTAG, "Error building return: " + ex);
         }
 
         if (dialog != null) {
             dialog.dismiss();
         }
 
         finishDialog(ret);
     }
 
     /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists,
-     * or multiple selection lists. If mSelected is set in the prompt before addlistItems is called, the items will be
-     * shown with "checkmarks" on their left side.
+     * or multiple selection lists.
      *
      * @param builder
      *        The alert builder currently building this dialog.
      * @param listItems
      *        The items to add.
-     * @param multipleSelection
-     *        If true, and mSelected is defined to be a non-zero-length list, the list will show checkmarks on the
-     *        left and allow multiple selection. 
+     * @param choiceMode
+     *        One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. 
     */
-    private void addlistItems(AlertDialog.Builder builder, PromptListItem[] listItems, boolean multipleSelection) {
-        if (mSelected != null && mSelected.length > 0) {
-            if (multipleSelection) {
+    private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) {
+        switch(choiceMode) {
+            case ListView.CHOICE_MODE_MULTIPLE_MODAL:
+            case ListView.CHOICE_MODE_MULTIPLE:
                 addMultiSelectList(builder, listItems);
-            } else {
+                break;
+            case ListView.CHOICE_MODE_SINGLE:
                 addSingleSelectList(builder, listItems);
-            }
-        } else {
-            addMenuList(builder, listItems);
+                break;
+            case ListView.CHOICE_MODE_NONE:
+            default:
+                addMenuList(builder, listItems);
         }
     }
 
     /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for
      * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things
      * to the rows like disabling/indenting them.
      *
      * @param builder
      *        The alert builder currently building this dialog.
      * @param listItems
      *        The items to add.
      */
     private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
-        PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
-        adapter.listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
-        adapter.listView.setOnItemClickListener(this);
-        builder.setInverseBackgroundForced(true);
-        adapter.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
-        adapter.listView.setAdapter(adapter);
-        builder.setView(adapter.listView);
+        ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null);
+        listView.setOnItemClickListener(this);
+        listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+        mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
+        listView.setAdapter(mAdapter);
+        builder.setView(listView);
     }
 
     /* Shows a single-select list with radio boxes on the side.
      *
      * @param builder
      *        the alert builder currently building this dialog.
      * @param listItems
      *        The items to add.
      */
     private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) {
-        PromptListAdapter adapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
-        // For single select, we only maintain a single index of the selected row
-        int selectedIndex = -1;
-        for (int i = 0; i < mSelected.length; i++) {
-            if (mSelected[i]) {
-                selectedIndex = i;
-                break;
-            }
-        }
-        mSelected = null;
-
-        builder.setSingleChoiceItems(adapter, selectedIndex, this);
+        mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems);
+        builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), this);
     }
 
     /* Shows a single-select list.
      *
      * @param builder
      *        the alert builder currently building this dialog.
      * @param listItems
      *        The items to add.
      */
     private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) {
-        PromptListAdapter adapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
-        builder.setAdapter(adapter, this);
-        mSelected = null;
+        mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems);
+        builder.setAdapter(mAdapter, this);
     }
 
-
     /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background
      * drawable for the view.
      */
     private View wrapInput(final PromptInput input) {
         final LinearLayout linearLayout = new LinearLayout(mContext);
         linearLayout.setOrientation(LinearLayout.VERTICAL);
         applyInputStyle(linearLayout, input);
 
@@ -328,23 +322,21 @@ public class Prompt implements OnClickLi
             boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView
 
             if (length == 1) {
                 root = wrapInput(mInputs[0]);
                 scrollable |= mInputs[0].getScrollable();
             } else if (length > 1) {
                 LinearLayout linearLayout = new LinearLayout(mContext);
                 linearLayout.setOrientation(LinearLayout.VERTICAL);
-
                 for (int i = 0; i < length; i++) {
                     View content = wrapInput(mInputs[i]);
                     linearLayout.addView(content);
                     scrollable |= mInputs[i].getScrollable();
                 }
-
                 root = linearLayout;
             }
 
             if (scrollable) {
                 builder.setView(root);
             } else {
                 ScrollView view = new ScrollView(mContext);
                 view.addView(root);
@@ -362,17 +354,17 @@ public class Prompt implements OnClickLi
     }
 
     /* AdapterView.OnItemClickListener
      * Called when a list item is clicked
      */
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         ThreadUtils.assertOnUiThread();
-        mSelected[position] = !mSelected[position];
+        mAdapter.toggleSelected(position);
     }
 
     /* @DialogInterface.OnCancelListener
      * Called when the user hits back to cancel a dialog. The dialog will close itself when this
      * ends. Setup the correct return values here.
      *
      * @param aDialog
      *          A dialog interface for the dialog that's being closed.
@@ -397,17 +389,16 @@ public class Prompt implements OnClickLi
 
     /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
      * is closing.
      */
     public void finishDialog(JSONObject aReturn) {
         mInputs = null;
         mButtons = null;
         mDialog = null;
-        mSelected = null;
         try {
             aReturn.put("guid", mGuid);
         } catch(JSONException ex) { }
 
         // poke the Gecko thread in case it's waiting for new events
         GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
 
         if (mCallback != null) {
@@ -428,20 +419,27 @@ public class Prompt implements OnClickLi
         JSONArray inputs = getSafeArray(geckoObject, "inputs");
         mInputs = new PromptInput[inputs.length()];
         for (int i = 0; i < mInputs.length; i++) {
             try {
                 mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
             } catch(Exception ex) { }
         }
 
-        PromptListItem[] menuitems = getListItemArray(geckoObject, "listitems");
-        mSelected = getBooleanArray(geckoObject, "selected");
-        boolean multiple = geckoObject.optBoolean("multiple");
-        show(title, text, menuitems, multiple);
+        PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems"));
+        String selected = geckoObject.optString("choiceMode");
+
+        int choiceMode = ListView.CHOICE_MODE_NONE;
+        if ("single".equals(selected)) {
+            choiceMode = ListView.CHOICE_MODE_SINGLE;
+        } else if ("multiple".equals(selected)) {
+            choiceMode = ListView.CHOICE_MODE_MULTIPLE;
+        }
+
+        show(title, text, menuitems, choiceMode);
     }
 
     private static JSONArray getSafeArray(JSONObject json, String key) {
         try {
             return json.getJSONArray(key);
         } catch (Exception e) {
             return new JSONArray();
         }
@@ -469,194 +467,12 @@ public class Prompt implements OnClickLi
         for (int i = 0; i < length; i++) {
             try {
                 list[i] = items.getBoolean(i);
             } catch(Exception ex) { }
         }
         return list;
     }
 
-    private PromptListItem[] getListItemArray(JSONObject aObject, String aName) {
-        JSONArray items = getSafeArray(aObject, aName);
-        int length = items.length();
-        PromptListItem[] list = new PromptListItem[length];
-        for (int i = 0; i < length; i++) {
-            try {
-                list[i] = new PromptListItem(items.getJSONObject(i));
-            } catch(Exception ex) { }
-        }
-        return list;
-    }
-
-    public static class PromptListItem {
-        public final String label;
-        public final boolean isGroup;
-        public final boolean inGroup;
-        public final boolean disabled;
-        public final int id;
-        public final boolean isParent;
-
-        // This member can't be accessible from JS, see bug 733749.
-        public Drawable icon;
-
-        PromptListItem(JSONObject aObject) {
-            label = aObject.optString("label");
-            isGroup = aObject.optBoolean("isGroup");
-            inGroup = aObject.optBoolean("inGroup");
-            disabled = aObject.optBoolean("disabled");
-            id = aObject.optInt("id");
-            isParent = aObject.optBoolean("isParent");
-        }
-
-        public PromptListItem(String aLabel) {
-            label = aLabel;
-            isGroup = false;
-            inGroup = false;
-            disabled = false;
-            id = 0;
-            isParent = false;
-        }
-    }
-
     public interface PromptCallback {
         public void onPromptFinished(String jsonResult);
     }
-
-    public class PromptListAdapter extends ArrayAdapter<PromptListItem> {
-        private static final int VIEW_TYPE_ITEM = 0;
-        private static final int VIEW_TYPE_GROUP = 1;
-        private static final int VIEW_TYPE_COUNT = 2;
-
-        public ListView listView;
-        private int mResourceId = -1;
-        private Drawable mBlankDrawable = null;
-        private Drawable mMoreDrawable = null;
-
-        PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) {
-            super(context, textViewResourceId, objects);
-            mResourceId = textViewResourceId;
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            PromptListItem item = getItem(position);
-            return (item.isGroup ? VIEW_TYPE_GROUP : VIEW_TYPE_ITEM);
-        }
-
-        @Override
-        public int getViewTypeCount() {
-            return VIEW_TYPE_COUNT;
-        }
-
-        private Drawable getMoreDrawable(Resources res) {
-            if (mMoreDrawable == null) {
-                mMoreDrawable = res.getDrawable(android.R.drawable.ic_menu_more);
-            }
-            return mMoreDrawable;
-        }
-
-        private Drawable getBlankDrawable(Resources res) {
-            if (mBlankDrawable == null) {
-                mBlankDrawable = res.getDrawable(R.drawable.blank);
-            }
-            return mBlankDrawable;
-        }
-
-        private void maybeUpdateIcon(PromptListItem item, TextView t) {
-            if (item.icon == null && !item.inGroup && !item.isParent) {
-                t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
-                return;
-            }
-
-            Drawable d = null;
-            Resources res = mContext.getResources();
-            // Set the padding between the icon and the text.
-            t.setCompoundDrawablePadding(mIconTextPadding);
-            if (item.icon != null) {
-                // We want the icon to be of a specific size. Some do not
-                // follow this rule so we have to resize them.
-                Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
-                d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
-            } else if (item.inGroup) {
-                // We don't currently support "indenting" items with icons
-                d = getBlankDrawable(res);
-            }
-
-            Drawable moreDrawable = null;
-            if (item.isParent) {
-                moreDrawable = getMoreDrawable(res);
-            }
-
-            if (d != null || moreDrawable != null) {
-                t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
-            }
-        }
-
-        private void maybeUpdateCheckedState(int position, PromptListItem item, ViewHolder viewHolder) {
-            viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
-            viewHolder.textView.setClickable(item.isGroup || item.disabled);
-
-            if (mSelected == null) {
-                return;
-            }
-
-            CheckedTextView ct;
-            try {
-                ct = (CheckedTextView) viewHolder.textView;
-                // Apparently just using ct.setChecked(true) doesn't work, so this
-                // is stolen from the android source code as a way to set the checked
-                // state of these items
-                if (listView != null) {
-                    listView.setItemChecked(position, mSelected[position]);
-                }
-            } catch (Exception e) {
-                return;
-            }
-
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            PromptListItem item = getItem(position);
-            ViewHolder viewHolder = null;
-
-            if (convertView == null) {
-                int resourceId = mResourceId;
-                if (item.isGroup) {
-                    resourceId = R.layout.list_item_header;
-                }
-
-                convertView = mInflater.inflate(resourceId, null);
-                convertView.setMinimumHeight(mMinRowSize);
-
-                TextView tv = (TextView) convertView.findViewById(android.R.id.text1);
-                viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(),
-                                            tv.getPaddingTop(), tv.getPaddingBottom());
-
-                convertView.setTag(viewHolder);
-            } else {
-                viewHolder = (ViewHolder) convertView.getTag();
-            }
-
-            viewHolder.textView.setText(item.label);
-            maybeUpdateCheckedState(position, item, viewHolder);
-            maybeUpdateIcon(item, viewHolder.textView);
-
-            return convertView;
-        }
-
-        private class ViewHolder {
-            public final TextView textView;
-            public final int paddingLeft;
-            public final int paddingRight;
-            public final int paddingTop;
-            public final int paddingBottom;
-
-            ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) {
-                textView = aTextView;
-                paddingLeft = aLeft;
-                paddingRight = aRight;
-                paddingTop = aTop;
-                paddingBottom = aBottom;
-            }
-        }
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/PromptListAdapter.java
@@ -0,0 +1,210 @@
+package org.mozilla.gecko.prompts;
+
+import org.mozilla.gecko.R;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+import android.widget.CheckedTextView;
+import android.widget.TextView;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+
+public class PromptListAdapter extends ArrayAdapter<PromptListItem> {
+    private static final int VIEW_TYPE_ITEM = 0;
+    private static final int VIEW_TYPE_GROUP = 1;
+    private static final int VIEW_TYPE_COUNT = 2;
+
+    private static final String LOGTAG = "GeckoPromptListAdapter";
+
+    private final int mResourceId;
+    private Drawable mBlankDrawable;
+    private Drawable mMoreDrawable;
+    private static int mGroupPaddingSize;
+    private static int mLeftRightTextWithIconPadding;
+    private static int mTopBottomTextWithIconPadding;
+    private static int mIconSize;
+    private static int mMinRowSize;
+    private static int mIconTextPadding;
+    private static boolean mInitialized = false;
+
+    PromptListAdapter(Context context, int textViewResourceId, PromptListItem[] objects) {
+        super(context, textViewResourceId, objects);
+        mResourceId = textViewResourceId;
+        init();
+    }
+
+    private void init() {
+        if (!mInitialized) {
+            Resources res = getContext().getResources();
+            mGroupPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_group_padding_size));
+            mLeftRightTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_left_right_text_with_icon_padding));
+            mTopBottomTextWithIconPadding = (int) (res.getDimension(R.dimen.prompt_service_top_bottom_text_with_icon_padding));
+            mIconTextPadding = (int) (res.getDimension(R.dimen.prompt_service_icon_text_padding));
+            mIconSize = (int) (res.getDimension(R.dimen.prompt_service_icon_size));
+            mMinRowSize = (int) (res.getDimension(R.dimen.prompt_service_min_list_item_height));
+            mInitialized = true;
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        PromptListItem item = getItem(position);
+        if (item.isGroup) {
+            return VIEW_TYPE_GROUP;
+        } else {
+            return VIEW_TYPE_ITEM;
+        }
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return VIEW_TYPE_COUNT;
+    }
+
+    private Drawable getMoreDrawable(Resources res) {
+        if (mMoreDrawable == null) {
+            mMoreDrawable = res.getDrawable(R.drawable.menu_item_more);
+        }
+        return mMoreDrawable;
+    }
+
+    private Drawable getBlankDrawable(Resources res) {
+        if (mBlankDrawable == null) {
+            mBlankDrawable = res.getDrawable(R.drawable.blank);
+        }
+        return mBlankDrawable;
+    }
+
+    public void toggleSelected(int position) {
+        PromptListItem item = getItem(position);
+        item.selected = !item.selected;
+    }
+
+    private void maybeUpdateIcon(PromptListItem item, TextView t) {
+        if (item.icon == null && !item.inGroup && !item.isParent) {
+            t.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+            return;
+        }
+
+        Drawable d = null;
+        Resources res = getContext().getResources();
+        // Set the padding between the icon and the text.
+        t.setCompoundDrawablePadding(mIconTextPadding);
+        if (item.icon != null) {
+            // We want the icon to be of a specific size. Some do not
+            // follow this rule so we have to resize them.
+            Bitmap bitmap = ((BitmapDrawable) item.icon).getBitmap();
+            d = new BitmapDrawable(res, Bitmap.createScaledBitmap(bitmap, mIconSize, mIconSize, true));
+        } else if (item.inGroup) {
+            // We don't currently support "indenting" items with icons
+            d = getBlankDrawable(res);
+        }
+
+        Drawable moreDrawable = null;
+        if (item.isParent) {
+            moreDrawable = getMoreDrawable(res);
+        }
+
+        if (d != null || moreDrawable != null) {
+            t.setCompoundDrawablesWithIntrinsicBounds(d, null, moreDrawable, null);
+        }
+    }
+
+    private void maybeUpdateCheckedState(ListView list, int position, PromptListItem item, ViewHolder viewHolder) {
+        viewHolder.textView.setEnabled(!item.disabled && !item.isGroup);
+        viewHolder.textView.setClickable(item.isGroup || item.disabled);
+        if (viewHolder.textView instanceof CheckedTextView) {
+            // Apparently just using ct.setChecked(true) doesn't work, so this
+            // is stolen from the android source code as a way to set the checked
+            // state of these items
+            list.setItemChecked(position, item.selected);
+        }
+    }
+
+    boolean isSelected(int position){
+        return getItem(position).selected;
+    }
+
+    ArrayList<Integer> getSelected() {
+        int length = getCount();
+
+        ArrayList<Integer> selected = new ArrayList<Integer>();
+        for (int i = 0; i< length; i++) {
+            if (isSelected(i)) {
+                selected.add(i);
+            }
+        }
+
+        return selected;
+    }
+
+    int getSelectedIndex() {
+        int length = getCount();
+        for (int i = 0; i< length; i++) {
+            if (isSelected(i)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        PromptListItem item = getItem(position);
+        int type = getItemViewType(position);
+        ViewHolder viewHolder = null;
+
+        if (convertView == null) {
+            int resourceId = mResourceId;
+            if (item.isGroup) {
+                resourceId = R.layout.list_item_header;
+            }
+            LayoutInflater mInflater = LayoutInflater.from(getContext());
+            convertView = mInflater.inflate(resourceId, null);
+            convertView.setMinimumHeight(mMinRowSize);
+
+            TextView tv = (TextView) convertView.findViewById(android.R.id.text1);
+            viewHolder = new ViewHolder(tv, tv.getPaddingLeft(), tv.getPaddingRight(),
+                                        tv.getPaddingTop(), tv.getPaddingBottom());
+
+            convertView.setTag(viewHolder);
+        } else {
+            viewHolder = (ViewHolder) convertView.getTag();
+        }
+
+        viewHolder.textView.setText(item.label);
+        maybeUpdateCheckedState((ListView) parent, position, item, viewHolder);
+        maybeUpdateIcon(item, viewHolder.textView);
+
+        return convertView;
+    }
+
+    private static class ViewHolder {
+        public final TextView textView;
+        public final int paddingLeft;
+        public final int paddingRight;
+        public final int paddingTop;
+        public final int paddingBottom;
+
+        ViewHolder(TextView aTextView, int aLeft, int aRight, int aTop, int aBottom) {
+            textView = aTextView;
+            paddingLeft = aLeft;
+            paddingRight = aRight;
+            paddingTop = aTop;
+            paddingBottom = aBottom;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/PromptListItem.java
@@ -0,0 +1,60 @@
+package org.mozilla.gecko.prompts;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import android.graphics.drawable.Drawable;
+import java.util.List;
+import java.util.ArrayList;
+
+// This class should die and be replaced with normal menu items
+public class PromptListItem {
+    private static final String LOGTAG = "GeckoPromptListItem";
+    public final String label;
+    public final boolean isGroup;
+    public final boolean inGroup;
+    public final boolean disabled;
+    public final int id;
+    public boolean selected;
+
+    public boolean isParent;
+    public Drawable icon;
+
+    PromptListItem(JSONObject aObject) {
+        label = aObject.optString("label");
+        isGroup = aObject.optBoolean("isGroup");
+        inGroup = aObject.optBoolean("inGroup");
+        disabled = aObject.optBoolean("disabled");
+        id = aObject.optInt("id");
+        isParent = aObject.optBoolean("isParent");
+        selected = aObject.optBoolean("selected");
+    }
+
+    public PromptListItem(String aLabel) {
+        label = aLabel;
+        isGroup = false;
+        inGroup = false;
+        disabled = false;
+        id = 0;
+    }
+
+    static PromptListItem[] getArray(JSONArray items) {
+        if (items == null) {
+            return new PromptListItem[0];
+        }
+
+        int length = items.length();
+        List<PromptListItem> list = new ArrayList<PromptListItem>(length);
+        for (int i = 0; i < length; i++) {
+            try {
+                PromptListItem item = new PromptListItem(items.getJSONObject(i));
+                list.add(item);
+            } catch(Exception ex) { }
+        }
+
+        PromptListItem[] arrays = new PromptListItem[length];
+        list.toArray(arrays);
+        return arrays;
+    }
+}
--- a/mobile/android/base/prompts/PromptService.java
+++ b/mobile/android/base/prompts/PromptService.java
@@ -26,25 +26,25 @@ public class PromptService implements Ge
         mContext = context;
     }
 
     public void destroy() {
         GeckoAppShell.getEventDispatcher().unregisterEventListener("Prompt:Show", this);
         GeckoAppShell.getEventDispatcher().unregisterEventListener("Prompt:ShowTop", this);
     }
 
-    public void show(final String aTitle, final String aText, final Prompt.PromptListItem[] aMenuList,
-                     final boolean aMultipleSelection, final Prompt.PromptCallback callback) {
+    public void show(final String aTitle, final String aText, final PromptListItem[] aMenuList,
+                     final int aChoiceMode, final Prompt.PromptCallback callback) {
         // The dialog must be created on the UI thread.
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 Prompt p;
                 p = new Prompt(mContext, callback);
-                p.show(aTitle, aText, aMenuList, aMultipleSelection);
+                p.show(aTitle, aText, aMenuList, aChoiceMode);
             }
         });
     }
 
     // GeckoEventListener implementation
     @Override
     public void handleMessage(String event, final JSONObject message) {
         // The dialog must be created on the UI thread.
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -23,17 +23,18 @@
                         android:layout_width="fill_parent"
                         android:layout_height="fill_parent"
                         android:layout_above="@+id/find_in_page">
 
             <include layout="@layout/shared_ui_components"/>
 
             <FrameLayout android:id="@+id/home_pager_container"
                          android:layout_width="fill_parent"
-                         android:layout_height="fill_parent">
+                         android:layout_height="fill_parent"
+                         android:visibility="gone">
 
                 <ViewStub android:id="@+id/home_pager_stub"
                           android:layout="@layout/home_pager"
                           android:layout_width="fill_parent"
                           android:layout_height="fill_parent"/>
 
                 <org.mozilla.gecko.home.HomeBanner android:id="@+id/home_banner"
                                                    style="@style/Widget.HomeBanner"
--- a/mobile/android/base/tests/testHomeBanner.java
+++ b/mobile/android/base/tests/testHomeBanner.java
@@ -17,16 +17,20 @@ public class testHomeBanner extends UITe
         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();
+
+        // Make sure the banner hides when the user starts interacting with the url bar.
+        hideOnToolbarFocusTest();
+
         // TODO: API doesn't actually support this but it used to work due to how the banner was
         // part of TopSitesPanel's lifecycle
         // removeBannerTest();
 
         // Make sure to test dismissing the banner after everything else, since dismissing
         // the banner will prevent it from showing up again.
         dismissBannerTest();
     }
@@ -91,16 +95,28 @@ public class testHomeBanner extends UITe
         // 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();
 
         mAboutHome.assertBannerNotVisible();
     }
 
+    private void hideOnToolbarFocusTest() {
+        NavigationHelper.enterAndLoadUrl("about:home");
+        mAboutHome.assertVisible()
+                  .assertBannerVisible();
+
+        mToolbar.enterEditingMode();
+        mAboutHome.assertBannerNotVisible();
+
+        mToolbar.dismissEditingMode();
+        mAboutHome.assertBannerVisible();
+    }
+
     /**
      * 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();
     }
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -132,16 +132,17 @@ public class BrowserToolbar extends Geck
     private LinearLayout mActionItemBar;
     private MenuPopup mMenuPopup;
     private List<View> mFocusOrder;
 
     private OnActivateListener mActivateListener;
     private OnCommitListener mCommitListener;
     private OnDismissListener mDismissListener;
     private OnFilterListener mFilterListener;
+    private OnFocusChangeListener mFocusChangeListener;
     private OnStartEditingListener mStartEditingListener;
     private OnStopEditingListener mStopEditingListener;
 
     final private BrowserApp mActivity;
     private boolean mHasSoftMenuButton;
 
     private UIMode mUIMode;
     private boolean mAnimatingEntry;
@@ -310,16 +311,19 @@ public class BrowserToolbar extends Geck
                 setContentDescription(contentDescription);
             }
         });
 
         mUrlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
             @Override
             public void onFocusChange(View v, boolean hasFocus) {
                 setSelected(hasFocus);
+                if (mFocusChangeListener != null) {
+                    mFocusChangeListener.onFocusChange(v, hasFocus);
+                }
             }
         });
 
         mTabs.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 toggleTabs();
             }
@@ -780,16 +784,20 @@ public class BrowserToolbar extends Geck
         mUrlEditLayout.setOnDismissListener(listener);
     }
 
     public void setOnFilterListener(OnFilterListener listener) {
         mFilterListener = listener;
         mUrlEditLayout.setOnFilterListener(listener);
     }
 
+    public void setOnFocusChangeListener(OnFocusChangeListener listener) {
+        mFocusChangeListener = listener;
+    }
+
     public void setOnStartEditingListener(OnStartEditingListener listener) {
         mStartEditingListener = listener;
     }
 
     public void setOnStopEditingListener(OnStopEditingListener listener) {
         mStopEditingListener = listener;
     }
 
--- a/mobile/android/base/widget/ActivityChooserModel.java
+++ b/mobile/android/base/widget/ActivityChooserModel.java
@@ -314,18 +314,16 @@ public class ActivityChooserModel extend
      */
     private boolean mHistoricalRecordsChanged = true;
 
     /**
      * Flag whether to reload the activities for the current intent.
      */
     private boolean mReloadActivities = false;
 
-    private long mLastChanged = 0;
-
     /**
      * Policy for controlling how the model handles chosen activities.
      */
     private OnChooseActivityListener mActivityChoserModelPolicy;
 
     /**
      * Gets the data model backed by the contents of the provided file with historical data.
      * Note that only one data model is backed by a given file, thus multiple calls with
@@ -742,17 +740,16 @@ public class ActivityChooserModel extend
             mActivities.clear();
             List<ResolveInfo> resolveInfos = mContext.getPackageManager()
                     .queryIntentActivities(mIntent, 0);
             final int resolveInfoCount = resolveInfos.size();
             for (int i = 0; i < resolveInfoCount; i++) {
                 ResolveInfo resolveInfo = resolveInfos.get(i);
                 mActivities.add(new ActivityResolveInfo(resolveInfo));
             }
-            mLastChanged = System.currentTimeMillis();
             return true;
         }
         return false;
     }
 
     /**
      * Reads the historical data if necessary which is it has
      * changed, there is a history file, and there is not persist
@@ -1218,16 +1215,12 @@ public class ActivityChooserModel extend
         public void onReceive(Context context, Intent intent) {
             String action = intent.getAction();
             if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
                 String packageName = intent.getData().getSchemeSpecificPart();
                 removeHistoricalRecordsForPackage(packageName);
             }
 
             mReloadActivities = true;
-            mLastChanged = System.currentTimeMillis();
         }
     }
+}
 
-    public long getLastChanged() {
-        return mLastChanged;
-    }
-}
--- a/mobile/android/base/widget/GeckoActionProvider.java
+++ b/mobile/android/base/widget/GeckoActionProvider.java
@@ -16,17 +16,16 @@ import android.view.ActionProvider;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.View.OnClickListener;
 
 public class GeckoActionProvider extends ActionProvider {
     private static int MAX_HISTORY_SIZE = 2;
-    private long mLastChanged = 0;
 
     /**
      * A listener to know when a target was selected.
      * When setting a provider, the activity can listen to this,
      * to close the menu.
      */
     public interface OnTargetSelectedListener {
         public void onTargetSelected();
@@ -75,24 +74,16 @@ public class GeckoActionProvider extends
 
         return view;
     }
 
     public View getView() {
         return onCreateActionView();
     }
 
-    public boolean hasChanged() {
-        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
-        long lastChanged = dataModel.getLastChanged();
-        boolean ret = lastChanged != mLastChanged;
-        mLastChanged = lastChanged;
-        return ret;
-    }
-
     @Override
     public boolean hasSubMenu() {
         return true;
     }
 
     @Override
     public void onPrepareSubMenu(SubMenu subMenu) {
         // Clear since the order of items may change.
--- a/mobile/android/modules/HelperApps.jsm
+++ b/mobile/android/modules/HelperApps.jsm
@@ -22,18 +22,19 @@ function App(data) {
   this.name = data.name;
   this.isDefault = data.isDefault;
   this.packageName = data.packageName;
   this.activityName = data.activityName;
   this.iconUri = "-moz-icon://" + data.packageName;
 }
 
 App.prototype = {
-  launch: function(uri) {
-    HelperApps._launchApp(this, uri);
+  // callback will be null if a result is not requested
+  launch: function(uri, callback) {
+    HelperApps._launchApp(this, uri, callback);
     return false;
   }
 }
 
 var HelperApps =  {
   get defaultHttpHandlers() {
     delete this.defaultHttpHandlers;
     this.defaultHttpHandlers = this.getAppsForProtocol("http");
@@ -161,23 +162,34 @@ var HelperApps =  {
       mime: mimeType,
       action: options.action || "", // empty action string defaults to android.intent.action.VIEW
       url: uri ? uri.spec : "",
       packageName: options.packageName || "",
       className: options.className || ""
     };
   },
 
-  _launchApp: function launchApp(app, uri) {
-    let msg = this._getMessage("Intent:Open", uri, {
-      packageName: app.packageName,
-      className: app.activityName
-    });
+  _launchApp: function launchApp(app, uri, callback) {
+    if (callback) {
+        let msg = this._getMessage("Intent:OpenForResult", uri, {
+            packageName: app.packageName,
+            className: app.activityName
+        });
 
-    sendMessageToJava(msg);
+        sendMessageToJava(msg, function(data) {
+            callback(JSON.parse(data));
+        });
+    } else {
+        let msg = this._getMessage("Intent:Open", uri, {
+            packageName: app.packageName,
+            className: app.activityName
+        });
+
+        sendMessageToJava(msg);
+    }
   },
 
   _sendMessageSync: function(msg) {
     let res = null;
     sendMessageToJava(msg, function(data) {
       res = data;
     });
 
--- a/mobile/android/modules/Home.jsm
+++ b/mobile/android/modules/Home.jsm
@@ -151,54 +151,122 @@ let HomeBanner = (function () {
         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;
-
-  if ("title" in options)
-    this.title = options.title;
+function Panel(id, options) {
+  this.id = id;
+  this.title = options.title;
 
   if ("layout" in options)
     this.layout = options.layout;
 
   if ("views" in options)
     this.views = options.views;
 }
 
+// We need this function to have access to the HomePanels
+// private members without leaking it outside Home.jsm.
+let handlePanelsGet;
+
 let HomePanels = (function () {
-  // Holds the currrent set of registered panels.
-  let _panels = {};
+  // Holds the current set of registered panels that can be
+  // installed, updated, uninstalled, or unregistered. It maps
+  // panel ids with the functions that dynamically generate
+  // their respective panel options. This is used to retrieve
+  // the current list of available panels in the system.
+  // See HomePanels:Get handler.
+  let _registeredPanels = {};
+
+  // Valid layouts for a panel.
+  let Layout = Object.freeze({
+    FRAME: "frame"
+  });
+
+  // Valid types of views for a dataset.
+  let View = Object.freeze({
+    LIST: "list",
+    GRID: "grid"
+  });
+
+  // Valid item types for a panel view.
+  let Item = Object.freeze({
+    ARTICLE: "article",
+    IMAGE: "image"
+  });
+
+  // Valid item handlers for a panel view.
+  let ItemHandler = Object.freeze({
+    BROWSER: "browser",
+    INTENT: "intent"
+  });
+
+  let _generatePanel = function(id) {
+    let panel = new Panel(id, _registeredPanels[id]());
 
-  let _panelToJSON = function(panel) {
-    return {
-      id: panel.id,
-      title: panel.title,
-      layout: panel.layout,
-      views: panel.views
-    };
+    if (!panel.id || !panel.title) {
+      throw "Home.panels: Can't create a home panel without an id and title!";
+    }
+
+    if (!panel.layout) {
+      // Use FRAME layout by default
+      panel.layout = Layout.FRAME;
+    } else if (!_valueExists(Layout, panel.layout)) {
+      throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout;
+    }
+
+    for (let view of panel.views) {
+      if (!_valueExists(View, view.type)) {
+        throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type;
+      }
+
+      if (!view.itemType) {
+        if (view.type == View.LIST) {
+          // Use ARTICLE item type by default in LIST views
+          view.itemType = Item.ARTICLE;
+        } else if (view.type == View.GRID) {
+          // Use IMAGE item type by default in GRID views
+          view.itemType = Item.IMAGE;
+        }
+      } else if (!_valueExists(Item, view.itemType)) {
+        throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType;
+      }
+
+      if (!view.itemHandler) {
+        // Use BROWSER item handler by default
+        view.itemHandler = ItemHandler.BROWSER;
+      } else if (!_valueExists(ItemHandler, view.itemHandler)) {
+        throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler;
+      }
+
+      if (!view.dataset) {
+        throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
+      }
+    }
+
+    return panel;
   };
 
-  let _handleGet = function(data) {
+  handlePanelsGet = function(data) {
     let requestId = data.requestId;
     let ids = data.ids || null;
 
     let panels = [];
-    for (let id in _panels) {
-      let panel = _panels[id];
-
+    for (let id in _registeredPanels) {
       // Null ids means we want to fetch all available panels
-      if (ids == null || ids.indexOf(panel.id) >= 0) {
-        panels.push(_panelToJSON(panel));
+      if (ids == null || ids.indexOf(id) >= 0) {
+        try {
+          panels.push(_generatePanel(id));
+        } catch(e) {
+          Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
+        }
       }
     }
 
     sendMessageToJava({
       type: "HomePanels:Data",
       panels: panels,
       requestId: requestId
     });
@@ -210,105 +278,52 @@ let HomePanels = (function () {
       if (obj[key] == value) {
         return true;
       }
     }
     return false;
   };
 
   let _assertPanelExists = function(id) {
-    if (!(id in _panels)) {
+    if (!(id in _registeredPanels)) {
       throw "Home.panels: Panel doesn't exist: id = " + id;
     }
   };
 
   return Object.freeze({
-    // Valid layouts for a panel.
-    Layout: Object.freeze({
-      FRAME: "frame"
-    }),
-
-    // Valid types of views for a dataset.
-    View: Object.freeze({
-      LIST: "list",
-      GRID: "grid"
-    }),
-
-    // Valid item types for a panel view.
-    Item: Object.freeze({
-      ARTICLE: "article",
-      IMAGE: "image"
-    }),
+    Layout: Layout,
+    View: View,
+    Item: Item,
+    ItemHandler: ItemHandler,
 
-    // Valid item handlers for a panel view.
-    ItemHandler: Object.freeze({
-      BROWSER: "browser",
-      INTENT: "intent"
-    }),
-
-    register: function(options) {
-      let panel = new Panel(options);
-
+    register: function(id, optionsCallback) {
       // Bail if the panel already exists
-      if (panel.id in _panels) {
-        throw "Home.panels: Panel already exists: id = " + panel.id;
-      }
-
-      if (!panel.id || !panel.title) {
-        throw "Home.panels: Can't create a home panel without an id and title!";
+      if (id in _registeredPanels) {
+        throw "Home.panels: Panel already exists: id = " + id;
       }
 
-      if (!_valueExists(this.Layout, panel.layout)) {
-        throw "Home.panels: Invalid layout for panel: panel.id = " + panel.id + ", panel.layout =" + panel.layout;
+      if (!optionsCallback || typeof optionsCallback !== "function") {
+        throw "Home.panels: Panel callback must be a function: id = " + id;
       }
 
-      for (let view of panel.views) {
-        if (!_valueExists(this.View, view.type)) {
-          throw "Home.panels: Invalid view type: panel.id = " + panel.id + ", view.type = " + view.type;
-        }
-
-        if (!view.itemType) {
-          if (view.type == this.View.LIST) {
-            // Use ARTICLE item type by default in LIST views
-            view.itemType = this.Item.ARTICLE;
-          } else if (view.type == this.View.GRID) {
-            // Use IMAGE item type by default in GRID views
-            view.itemType = this.Item.IMAGE;
-          }
-        } else if (!_valueExists(this.Item, view.itemType)) {
-          throw "Home.panels: Invalid item type: panel.id = " + panel.id + ", view.itemType = " + view.itemType;
-        }
-
-        if (!view.itemHandler) {
-          // Use BROWSER item handler by default
-          view.itemHandler = this.ItemHandler.BROWSER;
-        } else if (!_valueExists(this.ItemHandler, view.itemHandler)) {
-          throw "Home.panels: Invalid item handler: panel.id = " + panel.id + ", view.itemHandler = " + view.itemHandler;
-        }
-
-        if (!view.dataset) {
-          throw "Home.panels: No dataset provided for view: panel.id = " + panel.id + ", view.type = " + view.type;
-        }
-      }
-
-      _panels[panel.id] = panel;
+      _registeredPanels[id] = optionsCallback;
     },
 
     unregister: function(id) {
       _assertPanelExists(id);
 
-      delete _panels[id];
+      delete _registeredPanels[id];
     },
 
     install: function(id) {
       _assertPanelExists(id);
 
       sendMessageToJava({
         type: "HomePanels:Install",
-        panel: _panelToJSON(_panels[id])
+        panel: _generatePanel(id)
       });
     },
 
     uninstall: function(id) {
       _assertPanelExists(id);
 
       sendMessageToJava({
         type: "HomePanels:Uninstall",
@@ -316,28 +331,28 @@ let HomePanels = (function () {
       });
     },
 
     update: function(id) {
       _assertPanelExists(id);
 
       sendMessageToJava({
         type: "HomePanels:Update",
-        panel: _panelToJSON(_panels[id])
+        panel: _generatePanel(id)
       });
     }
   });
 })();
 
 // Public API
 this.Home = Object.freeze({
   banner: HomeBanner,
   panels: HomePanels,
 
   // Lazy notification observer registered in browser.js
   observe: function(subject, topic, data) {
     switch(topic) {
       case "HomePanels:Get":
-        HomePanels._handleGet(JSON.parse(data));
+        handlePanelsGet(JSON.parse(data));
         break;
     }
   }
 });
--- a/mobile/android/modules/Prompt.jsm
+++ b/mobile/android/modules/Prompt.jsm
@@ -174,22 +174,21 @@ Prompt.prototype = {
     aItems.forEach(function(item) {
       let obj = { id: item.id };
 
       obj.label = item.label;
 
       if (item.disabled)
         obj.disabled = true;
 
-      if (item.selected || hasSelected || this.msg.multiple) {
-        if (!this.msg.selected) {
-          this.msg.selected = new Array(this.msg.listitems.length);
-          hasSelected = true;
+      if (item.selected) {
+        if (!this.msg.choiceMode) {
+          this.msg.choiceMode = "single";
         }
-        this.msg.selected[this.msg.listitems.length] = item.selected;
+        obj.selected = item.selected;
       }
 
       if (item.header)
         obj.isGroup = true;
 
       if (item.menu)
         obj.isParent = true;
 
@@ -202,13 +201,13 @@ Prompt.prototype = {
     return this;
   },
 
   setSingleChoiceItems: function(aItems) {
     return this._setListItems(aItems);
   },
 
   setMultiChoiceItems: function(aItems) {
-    this.msg.multiple = true;
+    this.msg.choiceMode = "multiple";
     return this._setListItems(aItems);
   },
 
 }
--- a/mobile/android/tests/background/junit3/AndroidManifest.xml.in
+++ b/mobile/android/tests/background/junit3/AndroidManifest.xml.in
@@ -2,17 +2,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="@ANDROID_BACKGROUND_TEST_PACKAGE_NAME@"
     sharedUserId="@MOZ_ANDROID_SHARED_ID@"
     android:versionCode="1"
     android:versionName="1.0" >
 
     <uses-sdk android:minSdkVersion="8"
-              android:targetSdkVersion="14" />
+              android:targetSdkVersion="16" />
 
     <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
     <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>
     <uses-permission android:name="@ANDROID_BACKGROUND_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/>
 
     <application
         android:icon="@drawable/icon"
         android:label="@ANDROID_BACKGROUND_APP_DISPLAYNAME@">
--- a/mobile/android/tests/background/junit3/Makefile.in
+++ b/mobile/android/tests/background/junit3/Makefile.in
@@ -1,24 +1,25 @@
 # 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/.
 
-ANDROID_APK_NAME := background-debug
+ANDROID_APK_NAME := background-junit3-debug
 
 PP_TARGETS        += manifest
 manifest          := $(srcdir)/AndroidManifest.xml.in
 manifest_TARGET   := AndroidManifest.xml
 manifest_FLAGS    += \
   -DANDROID_BACKGROUND_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \
-  -DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.background.test' \
+  -DANDROID_BACKGROUND_TEST_PACKAGE_NAME='org.mozilla.gecko.background.tests' \
   -DANDROID_BACKGROUND_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Background Tests' \
   -DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \
   -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE='$(ANDROID_PACKAGE_NAME)_sync' \
   $(NULL)
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
 
 GARBAGE += AndroidManifest.xml
 
 include $(srcdir)/android-services-files.mk
 
 # BACKGROUND_TESTS_{JAVA,RES}_FILES are defined in android-services-files.mk.
 JAVAFILES := $(BACKGROUND_TESTS_JAVA_FILES)
 
--- a/mobile/android/tests/background/junit3/moz.build
+++ b/mobile/android/tests/background/junit3/moz.build
@@ -1,18 +1,19 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
 include('android-services.mozbuild')
 
 main = add_android_eclipse_project('BackgroundInstrumentationTests', OBJDIR + '/AndroidManifest.xml')
-main.package_name = 'org.mozilla.background.test'
+main.package_name = 'org.mozilla.gecko.background.tests'
 main.res = SRCDIR + '/res'
 main.recursive_make_targets += [
-    OBJDIR + '/AndroidManifest.xml',
-    TOPOBJDIR + '/mobile/android/base/tests/TestConstants.java']
+    OBJDIR + '/AndroidManifest.xml']
 main.referenced_projects += ['Fennec']
 
 main.add_classpathentry('src', SRCDIR + '/src',
     dstdir='src/org/mozilla/gecko/background')
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/AndroidManifest.xml.in
@@ -0,0 +1,26 @@
+#filter substitution
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="@ANDROID_BROWSER_TEST_PACKAGE_NAME@"
+    sharedUserId="@MOZ_ANDROID_SHARED_ID@"
+    android:versionCode="1"
+    android:versionName="1.0" >
+
+    <uses-sdk android:minSdkVersion="8"
+              android:targetSdkVersion="16" />
+
+    <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
+    <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"/>
+    <uses-permission android:name="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"/>
+
+    <application
+        android:icon="@drawable/icon"
+        android:label="@ANDROID_BROWSER_APP_DISPLAYNAME@">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:label="@string/app_name"
+        android:name="org.mozilla.browser.harness.BrowserInstrumentationTestRunner"
+        android:targetPackage="@ANDROID_BROWSER_TARGET_PACKAGE_NAME@" />
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/Makefile.in
@@ -0,0 +1,32 @@
+# 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/.
+
+ANDROID_APK_NAME := browser-junit3-debug
+
+ANDROID_EXTRA_JARS += \
+	browser-junit3.jar \
+  $(NULL)
+
+PP_TARGETS += manifest
+manifest := AndroidManifest.xml.in
+manifest_FLAGS += \
+  -DANDROID_BROWSER_TARGET_PACKAGE_NAME='$(ANDROID_PACKAGE_NAME)' \
+  -DANDROID_BROWSER_TEST_PACKAGE_NAME='org.mozilla.gecko.browser.tests' \
+  -DANDROID_BROWSER_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME) Browser Tests' \
+  -DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \
+  $(NULL)
+ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
+
+include $(topsrcdir)/config/rules.mk
+
+tools:: $(ANDROID_APK_NAME).apk
+
+# The test APK needs to know the contents of the target APK while not
+# being linked against them.  This is a best effort to avoid getting
+# out of sync with base's build config.
+JARS_DIR := $(DEPTH)/mobile/android/base
+JAVA_BOOTCLASSPATH := $(JAVA_BOOTCLASSPATH):$(subst $(NULL) ,:,$(wildcard $(JARS_DIR)/*.jar))
+# We also want to re-compile classes.dex when the associated base
+# content changes.
+classes.dex: $(wildcard $(JARS_DIR)/*.jar)
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DEFINES['ANDROID_PACKAGE_NAME'] = CONFIG['ANDROID_PACKAGE_NAME']
+
+jar = add_java_jar('browser-junit3')
+jar.sources += [
+    'src/harness/BrowserInstrumentationTestRunner.java',
+    'src/harness/BrowserTestListener.java',
+    'src/tests/BrowserTestCase.java',
+    'src/tests/TestJarReader.java',
+]
+jar.generated_sources = [] # None yet -- try to keep it this way.
+jar.javac_flags += ['-Xlint:all,-unchecked']
+
+# Android Eclipse project.
+main = add_android_eclipse_project('BrowserInstrumentationTests', OBJDIR + '/AndroidManifest.xml')
+# The package name doesn't really matter, but it looks nicest if the
+# generated classes (org.mozilla.gecko.browser.tests.{BuildConfig,R})
+# are in the same hierarchy as the rest of the source files.
+main.package_name = 'org.mozilla.gecko.browser.tests'
+main.res = 'res'
+main.recursive_make_targets += [
+    OBJDIR + '/AndroidManifest.xml',
+]
+main.recursive_make_targets += [OBJDIR + '/generated/' + f for f in jar.generated_sources]
+main.referenced_projects += ['Fennec']
+
+main.add_classpathentry('src', SRCDIR + '/src',
+    dstdir='src/org/mozilla/gecko/browser')
new file mode 100644
index 0000000000000000000000000000000000000000..5c6a89f801c19b84469c7a609fca180c07e8bd41
GIT binary patch
literal 7834
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
MWuRHEZWHnU0FzMS2mk;8
new file mode 100644
index 0000000000000000000000000000000000000000..7e383f14932ff0635f6bcf0807a2ce97c6a49c41
GIT binary patch
literal 3279
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
new file mode 100644
index 0000000000000000000000000000000000000000..f2d917b905cb81ca29a8d528b4f0c33823332623
GIT binary patch
literal 4738
zc${@~XHb({)b&FM5PAfZVt|C+rArZo(1S>bC|ycG#7GAbks<^ep{Sv0=<OoyQX)<1
zgrX!W5{eWN5Rnq;P2lBzGvAN*$2;fjvu5_}HG9wOv({7e6?1M55e@(VxGgPAFej+`
zUqDz-UVG5q;}d{mEil#qa9<h#V&ef|@Aw240U+$8@V7ev=w$+cV89Ev9<!5LR!=K)
z6X5v2llQ*#<w=G;(84hU066*o3nt(dN$9`i-9RIs|KAIst`h74;8cvIiILr{sTIeF
zRFZdg^whyYbEHg&_f2)FEMd4~vXm5F>Ivl8zj8uG9;WDrr(JPnwe_JR;|d?*(_966
zSd2^^N3PjPJa#cl27@5rU>hV)FzvbS{i>YSwpoMil+}JCKAyoN^3j{A-_=Vit2+@>
zI}tJV%rWG)@#6r=7-|MZLl}VLp(uzY28p3N#awu4oAvaLpGwA(Qrd^G`QYyzh$5mF
z6CtqUAc}T&4lZn-+48G^^C*B_XF*0hBIY>u8QFh^luZ#@awiPlB#<>IB^cC_96}yj
z8Bn=3LlVRdcBx9t!vhnLp9QRnb-21<q^0X9KReXL4{}l7UOEQLvM*K-1&<=Urd}A9
z*~Kvqybd36XGp&+d;PM1E{oc9lafgjV#W%j*|VC-18|7s9H;4AqBPhRjOBtL;6x^%
zf{u<uEJh;oLtoczhttQsBj2_KeJ@JR(pexS$R@WZou7L0u^UX#Ua7lDYd(w@f47S*
z&AuyASPvqFuu_GTVxQg-i(m90_|!sv2oW)XD)3@_DjyP}vj9D_kvT?>E1|HeQrxpO
z=$Xot%v?%MbgWvsK~vpqH#D<8X17!DU*DmUtLKjU%%ht^7tLwqmc<Y(7ZD?Xk`CSB
zh8w7J_$lE9QVT#%3M>c~d@2z8FvnkWcsQ@FP_)d84|VOyDWrR4QoZ(vq~x|?ov`2Z
z%GV9<YcF~N5|i2{+2~c%fKENyETDu)^&mXgNj`;3+U%3tzPpp}zyrs}+Hnv#(JJi;
zz$}aqgdX@*7r^TxkoUHPn<9kw2o2;$fbq~5oNGa%bz}TlAMIzB2M!N*TBua&rIhwZ
z+-Fl2(@%ko^bwm^P>ZKY8b3(JFx)0LSAI(XKykDvYsZ6UL8J|ZT@EIMl-U<!iyiUy
zimQ4am8MYe@HLnDXlR_JBT9Y&jf%NgSGQpmaA$J=mYbXS&5xODtL$Df!Eo@P$Q&Cn
zRZ$q537)>BNys@u!~#f#KwB27ZewaH#_o!V#jVPkh*{PJMa%UBi3(=ER>o}{&ce_7
zO#v2Z2w}+wlhh$pAE0rgUN;1VeB!gu#u|um{-)o?Y-8{q1pianwPV;fNWkbV`abGa
z|FtTPXMRWoVHD(O@tE5Eio7Dc03S-e!gWLB`h|^poWGwW1`-RE)d|<?2mAWp4i(iX
zeMwEu^16LhlCPCl-g`dyY`k2uTS~z%J<2G3WTd)RI>YT~TlUZLv(pwf0;%X93T*3+
z6uGeBNm?57CU=R4DBRRk3}Y!L{rs{_ei3Mv3xl#ixF^x8Ti4g|YV@noU+i0H1bX*v
zYf5bdf#BVDwq>XJ<X@d<5QbD^reO7NFH2V_$lhFWp^TpIQvEyO>yw=pY0f7?6GRAQ
ziS?{yu;;Tc{W4nQDl&)SxDg6d#whmbjqkF2qjizI7pA-Z?)f~l<&Xyl^Y>Us-o0~p
zakeuft|9m9R8wPBzHdIXcG$_dA~}CHlh_F<AMtB%&AAbN)RZ_ep<yG-KFziOb6Qdq
z@}>>bt@p;J-bV%v$d|%$+XK=KV&PAW{%Ls?tCUBKb7J)oUGL$0W~8&I1}`>|u+?1;
z!;LhLocDv}VC-}Cn(8!kE6Kv(Zf{wN__udKhmrv7&zp?PGG_%<A{b*ERLAvx-yt6t
zif*;mX}(WEcu!q8t#!BdgIC@5-w?r<cIuWo3+m4bUJn^BmdYax0;U4*@4i;$g7z8}
z(tnCzm9!}BLP>JZY~|8AgyRK3C=RIyym29+eiK3g-|9Ug7&)T3qSm}eo(FGN9-ZP4
zH9w3DXWV$6RKeuVM10ocIh}jG(&1<+l5te(>U=)RS$$LU>u^C$G>6F2(&w+Ev0f5J
zZ~Y_VTkH3=?0G=FifcD{zE@RctFt&a_+2_;*D-g`j^cfi&Bmc)^&NJyh9#vUC^W9?
z>gg56I9YMDX=N)nV6uMuU~<yneaKJhWKdqi!SWsLgBw3Sj+=}9ousZfH@u4&y1Z2H
zd)U^zlM^{<EfaOb!Z>a)c~jDLi@Vam#N&x7S4}_+uch?WyZyR8S1CGVx+}S@<sgIj
zq6W*i+i!5GYt-<3B`$jpvEb6_#EFE!f}W6BOSxPEA&9)9&e30)V-1lD=Wav=4J(gC
z|9oUyEKndfe#xgp_mYU1*hS-@?H{=6Dk^_)ec`tzEZc)iw(l5ijHn_qne2bmX*#`i
z!4a2aR3TM>Mm$5-hEGS2e{~4Z*B=)~c!^1Su@^4)V^J1oIjc)*`gOx|LeegO)n&se
zlR}2OlaPU|IPOQR`?)c*TU89#n4tx8M&qHji7h@+rAfe9+uz~2n8>|<8yb8jb6PuQ
z6NHH$q+KG0hdT~;4rz>MdlSL5jnqF;MHtJkUyIN$Q5_owwK&?vx!{NoLRKP6Dz9Sb
z7b+fXHx=5%s<SeGl)%@=!%)SdfpVDZlg>2`eO%nI#trgt^(L&ftNi*!W+3adjmmNB
z_XnX%=_>Ei47Lv*&yda@Z&ywq?$!GasidQ{^kv=ghJCt4P)K3w*0$)Uq0$E1cI6vK
zy`{H{1ojFcCN)fm)lj5`GUD`#{=yqD;KaQb$kT(2o*$4s{_{gRre(>Yabmq!{99u9
zt=#4#*5&Dz@WU}q!K&+8Wr{N}k`^>h@|liF-}AwF3v5#dQ<1U7XEV9U2qJ#Gga(%O
z(k>)upp1M^2X2840T3HDRDfk(mj1rA@XzwVX@<Z;(8`h9^yA^`ccgH7#Tk+@JCn_|
zIpUuvsTb_3lv^qp4Zv@9L?z~pB*QQyM5AhN)LWIVrE1n8%v{KYN25_j<t8NN3bLeA
zuPd+e!Jr#g^cPlh^-ncZRAEI<`8+#mLzxHS?6mmO3w)-hD#!}SvwDc`=isW={o5|#
zgVSn&B^0EVMY^^jj`WrG4dyX>9ToNdJ)3rwb9g6Yb88C;xhSJEf`QyS+15MbF8e%U
zr;`<cv0BXk)58Q$Lm63Kk(UNhjpSKS*jv^fgh%8v+wM`z!Lzfo^HWn({ATTr%s4;(
zSvw0vdwG+fG*ApC+IHJsPEs3pwBnI9WO5&VbFFcf+QL;_55qAd8q2*h(&`@u@t8So
z-9R7DQLjW(JHzc;#Art)8&dajc%kZvwm;da2ynICT@3Qt;{|zDGc4Bc;k&H%{jH@L
zQXQ$Tg02SW1gM){Uv~%JB*ul%?@~xj+=&^aNF>B1Xpnn%PAeA*oU!aE?|5N+jU6VD
z-!7Dxq{f2#_?+*RW}&tCEV#$BpZr!c9txred7@6?xC`Vf)P^dhOZkyJBnZgG;=zfl
z`B);TJH$95AIV-S^dBcl_m8QVte+eSe{acXM9;AU?c%wPPvLTYK7@4HTHW8Iy4D3F
z`D?-_LrbN-Kt8C~&i~bwa$)-|z&KhnXk;{eco9B%hz;uHW$1aXCc6FQJ37>#meC@U
zb8p|dmD!<NuFV1l3%V1o&DjmcvRlLi*w7xj>W7P2yIa`{ela%IuMA@jg1!8_7PJsC
z5P|9_f02qZQeaovO&fpCLab>39~b6M#BbhW#gy43U-3Ezqn+WQPKFLGq$*|vt&~<@
zswNlD@Bb{jdSWHNw&bMKc7|K|7r268D-ZrWWRu-qo{G|ZSY>^-#(xmU)(QXRNs!1E
z%TMt9EW;PwlP1q%bNvd^LN`0;=v-^*@0<twyB*7$LpHtF>|IKJFa6HAa@p%>jW^}u
za?FqMB>)(6VVdpg(hzxFGNl)=N?LqsQXHF)j|Ki6cDYkK8ct2%=jT5cF!ts2WYmtU
zIsd)ge6~NGoacxXidk+g-Ha|$8%eu}w=Lpl7jI>BZ1D6y4T(+&*?q;==~lDegyn-1
z5ibYBztW4&6!AUfRirtO50iz(8+S2t8~iH+HWPbO8&e@O{n;z0Wo=iq9C4?5)#~Z0
z6gv@5kI@#tZ*T2y{2j6~uLwdjQ<>6Lm&e{Kj%bqiJP{^VP`u{r!KSjLB$ekDXe2}~
zKaB^3;0jD6gs>y~DYS_|<Gqoc!{1NDk3Vw9q{wkxG81Y!5y|<0so+|^<-I(Y9L8S1
zxZC~3;tZGIHPh~$D9C^ML4a*xl3bDwIJEp5rALog#hdrvl&4oZH_!<L0xTHx_}p%n
zesrvh_BVsN_Z#`?bYC9~%2!6CgC4M~?lWj`b!2!%jUtq_oM@)D6<X2enQg#KojjU&
zvtXamrK1-@w{)lrV*6qo&pv-39tOYjlaQ5pGE>|_j%!mzaoG4~ijwR2`V6lthmE{l
zxe65NPB1F7GlTWA1)17v)?yEmACp36UN0p|`g;gHYTTL*2suly7QKDv)qSyr0&W0R
ztS)-F)JR@!>+0_(b}ufgSU}`Rul=%}-y~(5fB(DLhu<cu_rM)0k`C1imGX&5=Wm*P
zxXHW2y_a&DzsKzuS+b+bzn3qf@*Pa_(d}M4Hoa=BY;59xsclp1Za8{HQcJV8sQl3E
z#HIZ)TFeY(n|gKC4nqfGt8RVP($X5gs;B>HbktA>E0wJEv;bl&V67s;W~Az*l>Vg{
zzA2Bb!{IPRZ%bIyt}q)bWL0RhzA?6QdLATzz4HKOE6}CO3CNvEJh*XhX3r*OcF8Sf
z%VBs@?eKeocw=9sTT977(V~Xag=sh7#3-{^`xiJj+uNbSFcGs>hhU16a~9}fd=j}t
z5<5$dI%HNpbK+rFSOoJ3OagiMPF?59UBE{a-Bcum;+4#$Nj{S>2_j(1>_%IozQz3A
zuy0%6Ele@EIkdR>Ir+yul~1~l6B5r1-ly#eP7Qw=-OTUjsSD%b0l#%xL=U(W7x#%u
ze@(NJ4rNRmXQDU1j@3O!Nu|Ep^!$zXmz1s!Wq3(ziHXzTZhiI=s*nmT+Y_s>VKi@C
z3UGWkH?ejQ6S=&3*!bQSB~J>8#HUWX{QGOd#CZ1Wj(jm>7M2V}qhp~CP(FS5GtJLI
zgwHdYGQljNq;Xa&9i{nCc~6uLlbAi3%{?6eI+KwQ{AIsHF0JdjYl)iPWnS*7<8Exq
zRO`*IpKBEP{@IQT7a!iXUZ%fmzHba$s#bQWpGz;R^b_CYx1=@#9x%MR4phfHFUhRN
z!A$#g5Al&%;G&UpJCxN}xLy+e@*i?Sm`G1>tc%KH&wNNF)}iv0`OK{tH`=+}i1n24
z6P=P7mF;cP54%44yR_Y%>^AwSm`IJB7W4_tx$k!ybtoXsl9>u8*xxtr0R=ZH8Ojx9
z#MbcC)Ii8Ab;G;1(jK1ijRJWNN)jB*nQ};&d0%qMVw8S^vhz{*Y{UG<-e;`qczEd3
zlrVVK+S=#$EqCt}-~}4(RysG9GVSo{Oq}r7!o7WeppfFUV^%!i4a1`W0e4tBw40KL
z=CGAxfy6;Q1oB~c$N<y8Jl}ATOU!zIw(Io%&-2XzhpT@6d;BT6P!M2PPj8N#&gsaB
zYB?C_0fqBy;ccg#tZK8k3RLkK7VSGP(a|7GggY`;jR&Xn!$KT<mWYB3c!PWR26`k=
zJs0AF$A0T<%N1*huB@EP){DG8^LhIC=v?}zQ^6orHEs2GtHnOK19zMb2GWH^OFuF)
z!}cro%aoNd5v>@pj2wH*$MVc1Qz4>9Wx`WQ6OS?cd~I@9`=jClmASfvV8kUcBFEx#
z_bwHw(xR0yShW##PBQJjvms+U#h=3vWC6+q<q5ZIq815uE5xiY?j+ga^IGRiZ<lD^
zYt8i{Xt2SPJ9XqTtmZKY`;XR_=W53YTFqYdRMz<~flV%}Y@c}DA?-r3d_WS)Z3WMl
z`Io0MCkK@JPx4m}N%nTh*ncEc0SUkxFbX3w3GkjZ+S2%$4IEoRV2NDiJkg^XmDHCC
zdHo*cEjXS?^5ela#RE5<L=Qp<lBNj&fOM#-L#UTWsJ9+A*!u(kbyYPr<^Ol>)YbLW
t&+Dn_D68t|sj4DW3F-ff;1}TKdo$wyCZPQ^6;23%rRf!uDx`bd{{YM_z2g7?
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/layout/main.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical" >
+
+    <TextView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/app_name" />
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="app_name">Gecko Browser Tests</string>
+
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/harness/BrowserInstrumentationTestRunner.java
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browser.harness;
+
+import android.os.Bundle;
+import android.test.AndroidTestRunner;
+import android.test.InstrumentationTestRunner;
+import android.util.Log;
+
+/**
+ * A test runner that installs a special test listener.
+ * <p>
+ * In future, this listener will turn JUnit 3 test events into log messages in
+ * the format that Mochitest parsers understand.
+ */
+public class BrowserInstrumentationTestRunner extends InstrumentationTestRunner {
+    private static final String LOG_TAG = "BInstTestRunner";
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        Log.d(LOG_TAG, "onCreate");
+        super.onCreate(arguments);
+    }
+
+    @Override
+    protected AndroidTestRunner getAndroidTestRunner() {
+        Log.d(LOG_TAG, "getAndroidTestRunner");
+        AndroidTestRunner testRunner = super.getAndroidTestRunner();
+        testRunner.addTestListener(new BrowserTestListener());
+        return testRunner;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/harness/BrowserTestListener.java
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browser.harness;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.Test;
+import junit.framework.TestListener;
+import android.util.Log;
+
+/**
+ * BrowserTestListener turns JUnit 3 test events into log messages in the format
+ * that Mochitest parsers understand.
+ * <p>
+ * The idea is that, on infrastructure, we'll be able to use the same test
+ * parsing code for Browser JUnit 3 tests as we do for Robocop tests.
+ * <p>
+ * In future, that is!
+ */
+public class BrowserTestListener implements TestListener {
+    public static final String LOG_TAG = "BTestListener";
+
+    @Override
+    public void startTest(Test test) {
+        Log.d(LOG_TAG, "startTest: " + test);
+    }
+
+    @Override
+    public void endTest(Test test) {
+        Log.d(LOG_TAG, "endTest: " + test);
+    }
+
+    @Override
+    public void addFailure(Test test, AssertionFailedError t) {
+        Log.d(LOG_TAG, "addFailure: " + test);
+    }
+
+    @Override
+    public void addError(Test test, Throwable t) {
+        Log.d(LOG_TAG, "addError: " + test);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/tests/BrowserTestCase.java
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browser.tests;
+
+import org.mozilla.gecko.AppConstants;
+
+import android.app.Activity;
+import android.content.Context;
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ * BrowserTestCase provides helper methods for testing.
+ */
+public class BrowserTestCase extends ActivityInstrumentationTestCase2<Activity> {
+    private static String LOG_TAG = "BrowserTestCase";
+
+    private static final String LAUNCHER_ACTIVITY = AppConstants.ANDROID_PACKAGE_NAME + ".App";
+
+    private final static Class<Activity> sLauncherActivityClass;
+
+    static {
+        try {
+            sLauncherActivityClass = (Class<Activity>) Class.forName(LAUNCHER_ACTIVITY);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public BrowserTestCase() {
+        super(sLauncherActivityClass);
+    }
+
+    public Context getApplicationContext() {
+        return this.getInstrumentation().getTargetContext().getApplicationContext();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/junit3/src/tests/TestJarReader.java
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.browser.tests;
+
+import java.io.InputStream;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.util.GeckoJarReader;
+
+/**
+ * A basic jar reader test. Tests reading a png from fennec's apk, as well as
+ * loading some invalid jar urls.
+ */
+public class TestJarReader extends BrowserTestCase {
+    public void testJarReader() {
+        String appPath = getActivity().getApplication().getPackageResourcePath();
+        assertNotNull(appPath);
+
+        // Test reading a file from a jar url that looks correct.
+        String url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+        InputStream stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+        assertNotNull(stream);
+
+        // Test looking for an non-existent file in a jar.
+        url = "jar:file://" + appPath + "!/" + AppConstants.OMNIJAR_NAME;
+        stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+        assertNull(stream);
+
+        // Test looking for a file that doesn't exist in the APK.
+        url = "jar:file://" + appPath + "!/" + "BAD" + AppConstants.OMNIJAR_NAME;
+        stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+        assertNull(stream);
+
+        // Test looking for an jar with an invalid url.
+        url = "jar:file://" + appPath + "!" + "!/" + AppConstants.OMNIJAR_NAME;
+        stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/nonexistent_file.png");
+        assertNull(stream);
+
+        // Test looking for a file that doesn't exist on disk.
+        url = "jar:file://" + appPath + "BAD" + "!/" + AppConstants.OMNIJAR_NAME;
+        stream = GeckoJarReader.getStream("jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
+        assertNull(stream);
+    }
+}
copy from mobile/android/tests/moz.build
copy to mobile/android/tests/browser/moz.build
--- a/mobile/android/tests/moz.build
+++ b/mobile/android/tests/browser/moz.build
@@ -1,9 +1,9 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += [
-    'background',
+    'junit3',
 ]
--- a/mobile/android/tests/moz.build
+++ b/mobile/android/tests/moz.build
@@ -1,9 +1,10 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += [
     'background',
+    'browser',
 ]
--- a/python/mozbuild/mozbuild/backend/android_eclipse.py
+++ b/python/mozbuild/mozbuild/backend/android_eclipse.py
@@ -117,16 +117,22 @@ class AndroidEclipseBackend(CommonBacken
     def _manifest_for_project(self, srcdir, project):
         manifest = InstallManifest()
 
         if project.manifest:
             manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml')
 
         if project.res:
             manifest.add_symlink(mozpath.join(srcdir, project.res), 'res')
+        else:
+            # Eclipse expects a res directory no matter what, so we
+            # make an empty directory if the project doesn't specify.
+            res = os.path.abspath(mozpath.join(os.path.dirname(__file__),
+                'templates', 'android_eclipse_empty_resource_directory'))
+            manifest.add_pattern_copy(res, '.**', 'res')
 
         if project.assets:
             manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets')
 
         for cpe in project._classpathentries:
             manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir)
 
         # JARs and native libraries go in the same place. For now,
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PostBuilder.launch
@@ -0,0 +1,18 @@
+#filter substitution
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType">
+<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_CLEAN_TARGETS" value="post_build,"/>
+<booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/>
+<booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
+<booleanAttribute key="org.eclipse.debug.core.capture_output" value="true"/>
+<booleanAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON" value="true"/>
+<stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${project}"/>
+<booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="@IDE_PROJECT_NAME@"/>
+<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/post_build.xml}"/>
+<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="clean"/>
+<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
+<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-Dbuild_type=&quot;${build_type}&quot;&#10;-Dbuild_files=&quot;DUMMY ${build_files}&quot;"/>
+</launchConfiguration>
rename from python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/Builder.launch
rename to python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PreBuilder.launch
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/Builder.launch
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.externalToolBuilders/PreBuilder.launch
@@ -1,19 +1,19 @@
 #filter substitution
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType">
-<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="compile,"/>
-<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="compile,"/>
+<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="pre_build,"/>
+<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_MANUAL_TARGETS" value="pre_build,"/>
 <booleanAttribute key="org.eclipse.ant.ui.ATTR_TARGETS_UPDATED" value="true"/>
 <booleanAttribute key="org.eclipse.ant.ui.DEFAULT_VM_INSTALL" value="false"/>
 <booleanAttribute key="org.eclipse.debug.core.capture_output" value="true"/>
 <booleanAttribute key="org.eclipse.debug.ui.ATTR_CONSOLE_OUTPUT_ON" value="true"/>
 <stringAttribute key="org.eclipse.debug.core.ATTR_REFRESH_SCOPE" value="${project}"/>
 <booleanAttribute key="org.eclipse.debug.ui.ATTR_LAUNCH_IN_BACKGROUND" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.ant.ui.AntClasspathProvider"/>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="true"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="@IDE_PROJECT_NAME@"/>
-<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/build.xml}"/>
+<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/@IDE_PROJECT_NAME@/pre_build.xml}"/>
 <stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="incremental,auto"/>
 <booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
 <stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="-Dbuild_type=&quot;${build_type}&quot;&#10;-Dbuild_files=&quot;DUMMY ${build_files}&quot;"/>
 </launchConfiguration>
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.project
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.project
@@ -6,17 +6,17 @@
 	<projects>
 	</projects>
 	<buildSpec>
 		<buildCommand>
 			<name>org.eclipse.ui.externaltools.ExternalToolBuilder</name>
 			<arguments>
 				<dictionary>
 					<key>LaunchConfigHandle</key>
-					<value>&lt;project&gt;/.externalToolBuilders/Builder.launch</value>
+					<value>&lt;project&gt;/.externalToolBuilders/PreBuilder.launch</value>
 				</dictionary>
 			</arguments>
 		</buildCommand>
 		<buildCommand>
 			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
 			<arguments>
 			</arguments>
 		</buildCommand>
@@ -30,14 +30,23 @@
 			<arguments>
 			</arguments>
 		</buildCommand>
 		<buildCommand>
 			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
 			<arguments>
 			</arguments>
 		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.ui.externaltools.ExternalToolBuilder</name>
+			<arguments>
+				<dictionary>
+					<key>LaunchConfigHandle</key>
+					<value>&lt;project&gt;/.externalToolBuilders/PostBuilder.launch</value>
+				</dictionary>
+			</arguments>
+		</buildCommand>
 	</buildSpec>
 	<natures>
 		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
 		<nature>org.eclipse.jdt.core.javanature</nature>
 	</natures>
 </projectDescription>
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.core.prefs
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.core.prefs
@@ -5,8 +5,288 @@ org.eclipse.jdt.core.compiler.codegen.ta
 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
 org.eclipse.jdt.core.compiler.compliance=1.6
 org.eclipse.jdt.core.compiler.debug.lineNumber=generate
 org.eclipse.jdt.core.compiler.debug.localVariable=generate
 org.eclipse.jdt.core.compiler.debug.sourceFile=generate
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
 org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=0
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=false
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=false
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=4
+org.eclipse.jdt.core.formatter.use_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=false
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=false
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,57 @@
+#filter substitution
+cleanup.add_default_serial_version_id=true
+cleanup.add_generated_serial_version_id=false
+cleanup.add_missing_annotations=true
+cleanup.add_missing_deprecated_annotations=true
+cleanup.add_missing_methods=false
+cleanup.add_missing_nls_tags=false
+cleanup.add_missing_override_annotations=true
+cleanup.add_missing_override_annotations_interface_methods=true
+cleanup.add_serial_version_id=false
+cleanup.always_use_blocks=true
+cleanup.always_use_parentheses_in_expressions=false
+cleanup.always_use_this_for_non_static_field_access=false
+cleanup.always_use_this_for_non_static_method_access=false
+cleanup.convert_to_enhanced_for_loop=false
+cleanup.correct_indentation=false
+cleanup.format_source_code=false
+cleanup.format_source_code_changes_only=false
+cleanup.make_local_variable_final=true
+cleanup.make_parameters_final=false
+cleanup.make_private_fields_final=true
+cleanup.make_type_abstract_if_missing_method=false
+cleanup.make_variable_declarations_final=false
+cleanup.never_use_blocks=false
+cleanup.never_use_parentheses_in_expressions=true
+cleanup.organize_imports=true
+cleanup.qualify_static_field_accesses_with_declaring_class=false
+cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+cleanup.qualify_static_member_accesses_with_declaring_class=false
+cleanup.qualify_static_method_accesses_with_declaring_class=false
+cleanup.remove_private_constructors=true
+cleanup.remove_trailing_whitespaces=true
+cleanup.remove_trailing_whitespaces_all=true
+cleanup.remove_trailing_whitespaces_ignore_empty=false
+cleanup.remove_unnecessary_casts=true
+cleanup.remove_unnecessary_nls_tags=true
+cleanup.remove_unused_imports=true
+cleanup.remove_unused_local_variables=false
+cleanup.remove_unused_private_fields=true
+cleanup.remove_unused_private_members=false
+cleanup.remove_unused_private_methods=true
+cleanup.remove_unused_private_types=true
+cleanup.sort_members=false
+cleanup.sort_members_all=false
+cleanup.use_blocks=false
+cleanup.use_blocks_only_for_return_and_throw=false
+cleanup.use_parentheses_in_expressions=false
+cleanup.use_this_for_non_static_field_access=false
+cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+cleanup.use_this_for_non_static_method_access=false
+cleanup.use_this_for_non_static_method_access_only_if_necessary=true
+cleanup_profile=_Fennec
+cleanup_settings_version=2
+eclipse.preferences.version=1
+formatter_profile=_Fennec
+formatter_settings_version=12
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/post_build.xml
@@ -0,0 +1,45 @@
+#filter substitution
+<?xml version="1.0" encoding="UTF-8"?>
+<project>
+    <property name="topsrcdir" value="@IDE_TOPSRCDIR@"/>
+    <property name="topobjdir" value="@IDE_TOPOBJDIR@"/>
+    <property name="objdir" value="@IDE_OBJDIR@"/>
+    <property name="project_name" value="@IDE_PROJECT_NAME@"/>
+
+    <!-- This file can get large (!), but for a short time we want to
+         log as much information for debugging build loops as possible. -->
+    <record name="${topobjdir}/android_eclipse/build.log" append="yes" />
+
+    <target name="build_needed" >
+
+        <script language="javascript" >
+<![CDATA[
+  importClass(java.io.File);
+
+  var build_files = project.getProperty("build_files").split(" ");
+  var after = [];
+
+  var echo = project.createTask("echo");
+  var info = Packages.org.apache.tools.ant.taskdefs.Echo.EchoLevel();
+  info.setValue("info");
+  echo.setLevel(info);
+
+  // Timestamp.
+  echo.addText(project.getProperty("project_name") + " build type " + project.getProperty("build_type") + " started at: " + new Date());
+  echo.addText(project.getProperty("line.separator"));
+
+  echo.perform();
+
+  // The if below checks for the property being defined, not its value.
+  project.setProperty("build_needed", build_needed);
+]]>
+        </script>
+    </target>
+
+    <target name="post_build" depends="build_needed" if="build_needed">
+        <exec executable="${topsrcdir}/mach" dir="${topobjdir}" failonerror="true">
+            <arg value="build"/>
+            <arg value="${objdir}/ANDROID_ECLIPSE_PROJECT_${project_name}"/>
+        </exec>
+    </target>
+</project>
rename from python/mozbuild/mozbuild/backend/templates/android_eclipse/build.xml
rename to python/mozbuild/mozbuild/backend/templates/android_eclipse/pre_build.xml
--- a/python/mozbuild/mozbuild/backend/templates/android_eclipse/build.xml
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse/pre_build.xml
@@ -65,15 +65,15 @@
   // The if below checks for the property being defined, not its value.
   if (build_needed) {
     project.setProperty("build_needed", build_needed);
   }
 ]]>
         </script>
     </target>
 
-    <target name="compile" depends="build_needed" if="build_needed">
+    <target name="pre_build" depends="build_needed" if="build_needed">
         <exec executable="${topsrcdir}/mach" dir="${topobjdir}" failonerror="true">
             <arg value="build"/>
             <arg value="${objdir}/ANDROID_ECLIPSE_PROJECT_${project_name}"/>
         </exec>
     </target>
 </project>
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/backend/templates/android_eclipse_empty_resource_directory/.not_an_android_resource
@@ -0,0 +1,5 @@
+This file is named such that it is ignored by Android aapt. The file
+itself ensures that the AndroidEclipse build backend can create an
+empty res/ directory for projects explicitly specifying that it has no
+resource directory.  This is necessary because the Android Eclipse
+plugin requires that each project have a res/ directory.
--- a/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py
+++ b/python/mozbuild/mozbuild/test/backend/test_android_eclipse.py
@@ -42,17 +42,18 @@ class TestAndroidEclipseBackend(BackendT
 
     def test_main_project_files(self):
         """Ensure we generate reasonable files for main (non-library) projects."""
         self.env = self._consume('android_eclipse', AndroidEclipseBackend)
         for f in ['.classpath',
                   '.externalToolBuilders',
                   '.project',
                   '.settings',
-                  'build.xml',
+                  'pre_build.xml',
+                  'post_build.xml',
                   'gen',
                   'lint.xml',
                   'project.properties']:
             self.assertExists('main1', f)
 
     def test_library_manifest(self):
         """Ensure we generate manifest for library projects."""
         self.env = self._consume('android_eclipse', AndroidEclipseBackend)
--- a/services/fxaccounts/FxAccountsManager.jsm
+++ b/services/fxaccounts/FxAccountsManager.jsm
@@ -16,19 +16,16 @@ this.EXPORTED_SYMBOLS = ["FxAccountsMana
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
-XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
-  "resource://gre/modules/FxAccountsClient.jsm");
-
 this.FxAccountsManager = {
 
   init: function() {
     Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
   },
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic !== ONLOGOUT_NOTIFICATION) {
@@ -78,20 +75,23 @@ this.FxAccountsManager = {
     return error;
   },
 
   _serverError: function(aServerResponse) {
     let error = this._getError({ error: aServerResponse });
     return this._error(error ? error : ERROR_SERVER_ERROR, aServerResponse);
   },
 
-  // As we do with _fxAccounts, we don't really need this factory, but this way
-  // we allow tests to mock FxAccountsClient.
-  _createFxAccountsClient: function() {
-    return new FxAccountsClient();
+  // As with _fxAccounts, we don't really need this method, but this way we
+  // allow tests to mock FxAccountsClient.  By default, we want to return the
+  // client used by the fxAccounts object because deep down they should have
+  // access to the same hawk request object which will enable them to share
+  // local clock skeq data.
+  _getFxAccountsClient: function() {
+    return this._fxAccounts.getAccountsClient();
   },
 
   _signInSignUp: function(aMethod, aAccountId, aPassword) {
     if (Services.io.offline) {
       return this._error(ERROR_OFFLINE);
     }
 
     if (!aAccountId) {
@@ -104,17 +104,17 @@ this.FxAccountsManager = {
 
     // Check that there is no signed in account first.
     if (this._activeSession) {
       return this._error(ERROR_ALREADY_SIGNED_IN_USER, {
         user: this._user
       });
     }
 
-    let client = this._createFxAccountsClient();
+    let client = this._getFxAccountsClient();
     return this._fxAccounts.getSignedInUser().then(
       user => {
         if (user) {
           return this._error(ERROR_ALREADY_SIGNED_IN_USER, {
             user: this._user
           });
         }
         return client[aMethod](aAccountId, aPassword);
@@ -123,19 +123,21 @@ this.FxAccountsManager = {
       user => {
         let error = this._getError(user);
         if (!user || !user.uid || !user.sessionToken || error) {
           return this._error(error ? error : ERROR_INTERNAL_INVALID_USER, {
             user: user
           });
         }
 
-        // Save the credentials of the signed in user.
-        user.email = aAccountId;
-        return this._fxAccounts.setSignedInUser(user, false).then(
+        // If the user object includes an email field, it may differ in
+        // capitalization from what we sent down.  This is the server's
+        // canonical capitalization and should be used instead.
+        user.email = user.email || aAccountId;
+        return this._fxAccounts.setSignedInUser(user).then(
           () => {
             this._activeSession = user;
             log.debug("User signed in: " + JSON.stringify(this._user) +
                       " - Account created " + (aMethod == "signUp"));
             return Promise.resolve({
               accountCreated: aMethod === "signUp",
               user: this._user
             });
@@ -166,17 +168,17 @@ this.FxAccountsManager = {
         // At this point the local session should already be removed.
 
         // The client can create new sessions up to the limit (100?).
         // Orphaned tokens on the server will eventually be garbage collected.
         if (Services.io.offline) {
           return Promise.resolve();
         }
         // Otherwise, we try to remove the remote session.
-        let client = this._createFxAccountsClient();
+        let client = this._getFxAccountsClient();
         return client.signOut(sessionToken).then(
           result => {
             let error = this._getError(result);
             if (error) {
               return this._error(error, result);
             }
             log.debug("Signed out");
             return Promise.resolve();
@@ -288,17 +290,17 @@ this.FxAccountsManager = {
     }
 
     let deferred = Promise.defer();
 
     if (!aAccountId) {
       return this._error(ERROR_INVALID_ACCOUNTID);
     }
 
-    let client = this._createFxAccountsClient();
+    let client = this._getFxAccountsClient();
     return client.accountExists(aAccountId).then(
       result => {
         log.debug("Account " + result ? "" : "does not" + " exists");
         let error = this._getError(result);
         if (error) {
           return this._error(error, result);
         }
 
@@ -322,17 +324,17 @@ this.FxAccountsManager = {
       log.debug("Account already verified");
       return Promise.resolve(this._user);
     }
 
     if (Services.io.offline) {
       return this._error(ERROR_OFFLINE);
     }
 
-    let client = this._createFxAccountsClient();
+    let client = this._getFxAccountsClient();
     return client.recoveryEmailStatus(this._activeSession.sessionToken).then(
       data => {
         let error = this._getError(data);
         if (error) {
           return this._error(error, data);
         }
 
         // If the verification status is different from the one that we have
--- a/services/fxaccounts/tests/xpcshell/test_manager.js
+++ b/services/fxaccounts/tests/xpcshell/test_manager.js
@@ -135,17 +135,18 @@ FxAccountsManager._fxAccounts = {
     this._signedInUser = null;
     Services.obs.notifyObservers(null, ONLOGOUT_NOTIFICATION, null);
     deferred.resolve();
     return deferred.promise;
   }
 };
 
 // Save original FxAccountsClient factory from FxAccountsManager.
-const kFxAccountsClient = FxAccountsManager._createFxAccountsClient;
+const kFxAccountsClient = FxAccountsManager._getFxAccountsClient;
+
 // and change it for a fake client factory.
 let FakeFxAccountsClient = {
   _reject: false,
   _recoveryEmailStatusCalled: false,
   _signInCalled: false,
   _signUpCalled: false,
   _signOutCalled: false,
 
@@ -196,19 +197,21 @@ let FakeFxAccountsClient = {
 
   accountExists: function() {
     let deferred = Promise.defer();
     this._reject ? deferred.reject()
                  : deferred.resolve(this._accountExists);
     return deferred.promise;
   }
 };
-FxAccountsManager._createFxAccountsClient = function() {
+
+FxAccountsManager._getFxAccountsClient = function() {
   return FakeFxAccountsClient;
-}
+};
+
 
 // === Global cleanup ===
 
 // Unregister mocks and restore original code.
 do_register_cleanup(function() {
   // Unregister the factory so we do not leak
   Cm.QueryInterface(Ci.nsIComponentRegistrar)
     .unregisterFactory(Components.ID(kFxAccountsUIGlueUUID),
@@ -220,17 +223,17 @@ do_register_cleanup(function() {
                      "FxAccountsUIGlue",
                      kFxAccountsUIGlueContractID,
                      kFxAccountsUIGlueFactory);
 
   // Restore the original FxAccounts instance from FxAccountsManager.
   FxAccountsManager._fxAccounts = kFxAccounts;
 
   // Restore the FxAccountsClient getter from FxAccountsManager.
-  FxAccountsManager._createFxAccountsClient = kFxAccountsClient;
+  FxAccountsManager._getFxAccountsClient = kFxAccountsClient;
 });
 
 
 // === Tests ===
 
 function run_test() {
   run_next_test();
 }
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -102,17 +102,17 @@ function lazyPathGetter(constProp, dirKe
       delete SharedAll.Constants.Path[constProp];
       SharedAll.Constants.Path[constProp] = path;
     } catch (ex) {
       // Ignore errors if the value still isn't available. Hopefully
       // the next access will return it.
     }
 
     return path;
-  }
+  };
 }
 
 for (let [constProp, dirKey] of [
   ["localProfileDir", "ProfLD"],
   ["profileDir", "ProfD"],
   ["userApplicationDataDir", "UAppData"],
   ["winAppDataDir", "AppData"],
   ["winStartMenuProgsDir", "Progs"],
@@ -151,26 +151,27 @@ let Scheduler = {
   /**
    * A promise resolved once all operations are complete.
    *
    * This promise is never rejected and the result is always undefined.
    */
   queue: Promise.resolve(),
 
   /**
-   * The latest message sent and still waiting for a reply. This
-   * field is stored only in DEBUG builds, to avoid hoarding memory in
-   * release builds.
+   * The latest message sent and still waiting for a reply. In DEBUG
+   * builds, the entire message is stored, which may be memory-consuming.
+   * In non-DEBUG builds, only the method name is stored.
    */
   latestSent: undefined,
 
   /**
    * The latest reply received, or null if we are waiting for a reply.
-   * This field is stored only in DEBUG builds, to avoid hoarding
-   * memory in release builds.
+   * In DEBUG builds, the entire response is stored, which may be
+   * memory-consuming.  In non-DEBUG builds, only exceptions and
+   * method names are stored.
    */
   latestReceived: undefined,
 
   /**
    * A timer used to automatically shut down the worker after some time.
    */
   resetTimer: null,
 
@@ -236,41 +237,50 @@ let Scheduler = {
 
     // By convention, the last argument of any message may be an |options| object.
     let options;
     let methodArgs = args[0];
     if (methodArgs) {
       options = methodArgs[methodArgs.length - 1];
     }
     return this.push(() => Task.spawn(function*() {
+      Scheduler.latestReceived = null;
       if (OS.Constants.Sys.DEBUG) {
         // Update possibly memory-expensive debugging information
-        Scheduler.latestReceived = null;
-        Scheduler.latestSent = [method, ...args];
+        Scheduler.latestSent = [Date.now(), method, ...args];
+      } else {
+        Scheduler.latestSent = [Date.now(), method];
       }
       let data;
       let reply;
+      let isError = false;
       try {
         data = yield worker.post(method, ...args);
         reply = data;
       } catch (error if error instanceof PromiseWorker.WorkerError) {
         reply = error;
+        isError = true;
         throw EXCEPTION_CONSTRUCTORS[error.data.exn || "OSError"](error.data);
       } catch (error if error instanceof ErrorEvent) {
         reply = error;
         let message = error.message;
         if (message == "uncaught exception: [object StopIteration]") {
           throw StopIteration;
         }
+        isError = true;
         throw new Error(message, error.filename, error.lineno);
       } finally {
+        Scheduler.latestSent = Scheduler.latestSent.slice(0, 2);
         if (OS.Constants.Sys.DEBUG) {
           // Update possibly memory-expensive debugging information
-          Scheduler.latestSent = null;
-          Scheduler.latestReceived = reply;
+          Scheduler.latestReceived = [Date.now(), reply];
+        } else if (isError) {
+          Scheduler.latestReceived = [Date.now(), reply.message, reply.fileName, reply.lineNumber];
+        } else {
+          Scheduler.latestReceived = [Date.now()];
         }
         if (firstLaunch) {
           Scheduler._updateTelemetry();
         }
 
         // Don't restart the timer when reseting the worker, since that will
         // lead to an endless "resetWorker()" loop.
         if (method != "Meta_reset") {
@@ -1294,22 +1304,40 @@ this.OS.Path = Path;
 
 // Auto-flush OS.File during profile-before-change. This ensures that any I/O
 // that has been queued *before* profile-before-change is properly completed.
 // To ensure that I/O queued *during* profile-before-change is completed,
 // clients should register using AsyncShutdown.addBlocker.
 AsyncShutdown.profileBeforeChange.addBlocker(
   "OS.File: flush I/O queued before profile-before-change",
   // Wait until the latest currently enqueued promise is satisfied/rejected
-  (() => Scheduler.queue),
+  function() {
+    let DEBUG = false;
+    try {
+      DEBUG = Services.prefs.getBoolPref("toolkit.osfile.debug.failshutdown");
+    } catch (ex) {
+      // Ignore
+    }
+    if (DEBUG) {
+      // Return a promise that will never be satisfied
+      return Promise.defer().promise;
+    } else {
+      return Scheduler.queue;
+    }
+  },
   function getDetails() {
     let result = {
       launched: Scheduler.launched,
       shutdown: Scheduler.shutdown,
+      worker: !!worker,
       pendingReset: !!Scheduler.resetTimer,
+      latestSent: Scheduler.latestSent,
+      latestReceived: Scheduler.latestReceived
     };
-    if (OS.Constants.Sys.DEBUG) {
-      result.latestSent = Scheduler.latestSent;
-      result.latestReceived - Scheduler.latestReceived;
-    };
+    // Convert dates to strings for better readability
+    for (let key of ["latestSent", "latestReceived"]) {
+      if (result[key] && typeof result[key][0] == "number") {
+        result[key][0] = Date(result[key][0]);
+      }
+    }
     return result;
   }
 );
--- a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
+++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
@@ -1233,22 +1233,25 @@ exports.normalizeToPointer = normalizeTo
 /**
  * An OS error.
  *
  * This class is provided mostly for type-matching. If you need more
  * details about an error, you should use the platform-specific error
  * codes provided by subclasses of |OS.Shared.Error|.
  *
  * @param {string} operation The operation that failed.
+ * @param {string=} path The path of the file on which the operation failed,
+ * or nothing if there was no file involved in the failure.
  *
  * @constructor
  */
-function OSError(operation) {
+function OSError(operation, path = "") {
   Error.call(this);
   this.operation = operation;
+  this.path = path;
 }
 exports.OSError = OSError;
 
 
 ///////////////////// Temporary boilerplate
 // Boilerplate, to simplify the transition to require()
 // Do not rely upon this symbol, it will disappear with
 // bug 883050.
--- a/toolkit/components/osfile/modules/osfile_shared_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -20,33 +20,38 @@ let Lz4 =
   require("resource://gre/modules/workers/lz4.js");
 let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
 let clone = SharedAll.clone;
 
 /**
  * Code shared by implementations of File.
  *
  * @param {*} fd An OS-specific file handle.
+ * @param {string} path File path of the file handle, used for error-reporting.
  * @constructor
  */
-let AbstractFile = function AbstractFile(fd) {
+let AbstractFile = function AbstractFile(fd, path) {
   this._fd = fd;
+  if (!path) {
+    throw new TypeError("path is expected");
+  }
+  this._path = path;
 };
 
 AbstractFile.prototype = {
   /**
    * Return the file handle.
    *
    * @throw OS.File.Error if the file has been closed.
    */
   get fd() {
     if (this._fd) {
       return this._fd;
     }
-    throw OS.File.Error.closed();
+    throw OS.File.Error.closed("accessing file", this._path);
   },
   /**
    * Read bytes from this file to a new buffer.
    *
    * @param {number=} bytes If unspecified, read all the remaining bytes from
    * this file. If specified, read |bytes| bytes, or less if the file does notclone
    * contain that many bytes.
    * @param {JSON} options
@@ -178,17 +183,17 @@ AbstractFile.openUnique = function openU
         return {
           path: uniquePath,
           file: OS.File.open(uniquePath, mode)
         };
       } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
         // keep trying ...
       }
     }
-    throw OS.File.Error.exists("could not find an unused file name.");
+    throw OS.File.Error.exists("could not find an unused file name.", path);
   }
 };
 
 /**
  * Code shared by iterators.
  */
 AbstractFile.AbstractIterator = function AbstractIterator() {
 };
@@ -390,17 +395,17 @@ AbstractFile.writeAtomic =
      function writeAtomic(path, buffer, options = {}) {
 
   // Verify that path is defined and of the correct type
   if (typeof path != "string" || path == "") {
     throw new TypeError("File path should be a (non-empty) string");
   }
   let noOverwrite = options.noOverwrite;
   if (noOverwrite && OS.File.exists(path)) {
-    throw OS.File.Error.exists("writeAtomic");
+    throw OS.File.Error.exists("writeAtomic", path);
   }
 
   if (typeof buffer == "string") {
     // Normalize buffer to a C buffer by encoding it
     let encoding = options.encoding || "utf-8";
     buffer = new TextEncoder(encoding).encode(buffer);
   }
 
--- a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
@@ -68,29 +68,33 @@ libc.declareLazy(LazyBindings, "strerror
  * of this field against the error constants of |OS.Constants.libc|.
  *
  * @param {string=} operation The operation that failed. If unspecified,
  * the name of the calling function is taken to be the operation that
  * failed.
  * @param {number=} lastError The OS-specific constant detailing the
  * reason of the error. If unspecified, this is fetched from the system
  * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
  *
  * @constructor
  * @extends {OS.Shared.Error}
  */
-let OSError = function OSError(operation, errno) {
-  operation = operation || "unknown operation";
-  SharedAll.OSError.call(this, operation);
-  this.unixErrno = errno || ctypes.errno;
+let OSError = function OSError(operation = "unknown operation",
+                               errno = ctypes.errno, path = "") {
+  operation = operation;
+  SharedAll.OSError.call(this, operation, path);
+  this.unixErrno = errno;
 };
 OSError.prototype = Object.create(SharedAll.OSError.prototype);
 OSError.prototype.toString = function toString() {
   return "Unix error " + this.unixErrno +
     " during operation " + this.operation +
+    (this.path? " on file " + this.path : "") +
     " (" + LazyBindings.strerror(this.unixErrno).readString() + ")";
 };
 
 /**
  * |true| if the error was raised because a file or directory
  * already exists, |false| otherwise.
  */
 Object.defineProperty(OSError.prototype, "becauseExists", {
@@ -138,49 +142,59 @@ Object.defineProperty(OSError.prototype,
 
 /**
  * Serialize an instance of OSError to something that can be
  * transmitted across threads (not necessarily a string).
  */
 OSError.toMsg = function toMsg(error) {
   return {
     operation: error.operation,
-    unixErrno: error.unixErrno
+    unixErrno: error.unixErrno,
+    path: error.path
   };
 };
 
 /**
  * Deserialize a message back to an instance of OSError
  */
 OSError.fromMsg = function fromMsg(msg) {
-  return new OSError(msg.operation, msg.unixErrno);
+  return new OSError(msg.operation, msg.unixErrno, msg.path);
 };
 exports.Error = OSError;
 
 /**
  * Code shared by implementations of File.Info on Unix
  *
  * @constructor
 */
-let AbstractInfo = function AbstractInfo(isDir, isSymLink, size, lastAccessDate,
+let AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size, lastAccessDate,
                                          lastModificationDate, unixLastStatusChangeDate,
                                          unixOwner, unixGroup, unixMode) {
+  this._path = path;
   this._isDir = isDir;
   this._isSymlLink = isSymLink;
   this._size = size;
   this._lastAccessDate = lastAccessDate;
   this._lastModificationDate = lastModificationDate;
   this._unixLastStatusChangeDate = unixLastStatusChangeDate;
   this._unixOwner = unixOwner;
   this._unixGroup = unixGroup;
   this._unixMode = unixMode;
 };
 
 AbstractInfo.prototype = {
   /**
+   * The path of the file, used for error-reporting.
+   *
+   * @type {string}
+   */
+  get path() {
+    return this._path;
+  },
+  /**
    * |true| if this file is a directory, |false| otherwise
    */
   get isDir() {
     return this._isDir;
   },
   /**
    * |true| if this file is a symbolink link, |false| otherwise
    */
@@ -300,26 +314,26 @@ exports.Type = Type;
  * Native paths
  *
  * Under Unix, expressed as C strings
  */
 Type.path = Type.cstring.withName("[in] path");
 Type.out_path = Type.out_cstring.withName("[out] path");
 
 // Special constructors that need to be defined on all threads
-OSError.closed = function closed(operation) {
-  return new OSError(operation, Const.EBADF);
+OSError.closed = function closed(operation, path) {
+  return new OSError(operation, Const.EBADF, path);
 };
 
-OSError.exists = function exists(operation) {
-  return new OSError(operation, Const.EEXIST);
+OSError.exists = function exists(operation, path) {
+  return new OSError(operation, Const.EEXIST, path);
 };
 
-OSError.noSuchFile = function noSuchFile(operation) {
-  return new OSError(operation, Const.ENOENT);
+OSError.noSuchFile = function noSuchFile(operation, path) {
+  return new OSError(operation, Const.ENOENT, path);
 };
 
 let EXPORTED_SYMBOLS = [
   "declareFFI",
   "libc",
   "Error",
   "AbstractInfo",
   "AbstractEntry",
--- a/toolkit/components/osfile/modules/osfile_unix_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -35,20 +35,21 @@
 
      /**
       * Representation of a file.
       *
       * You generally do not need to call this constructor yourself. Rather,
       * to open a file, use function |OS.File.open|.
       *
       * @param fd A OS-specific file descriptor.
+      * @param {string} path File path of the file handle, used for error-reporting.
       * @constructor
       */
-     let File = function File(fd) {
-       exports.OS.Shared.AbstractFile.call(this, fd);
+     let File = function File(fd, path) {
+       exports.OS.Shared.AbstractFile.call(this, fd, path);
        this._closeResult = null;
      };
      File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
 
      /**
       * Close the file.
       *
       * This method has no effect if the file is already closed. However,
@@ -65,17 +66,17 @@
         // Call |close(fd)|, detach finalizer if any
          // (|fd| may not be a CDataFinalizer if it has been
          // instantiated from a controller thread).
          let result = UnixFile._close(fd);
          if (typeof fd == "object" && "forget" in fd) {
            fd.forget();
          }
          if (result == -1) {
-           this._closeResult = new File.Error("close");
+           this._closeResult = new File.Error("close", ctypes.errno, this._path);
          }
        }
        if (this._closeResult) {
          throw this._closeResult;
        }
        return;
      };
 
@@ -98,17 +99,18 @@
       // Populate the page cache with data from a file so the subsequent reads
       // from that file will not block on disk I/O.
        if (typeof(UnixFile.posix_fadvise) === 'function' &&
            (options.sequential || !("sequential" in options))) {
          UnixFile.posix_fadvise(this.fd, 0, nbytes,
           OS.Constants.libc.POSIX_FADV_SEQUENTIAL);
        }
        return throw_on_negative("read",
-         UnixFile.read(this.fd, buffer, nbytes)
+         UnixFile.read(this.fd, buffer, nbytes),
+         this._path
        );
      };
 
      /**
       * Write some bytes to a file.
       *
       * @param {C pointer} buffer A buffer holding the data that must be
       * written.
@@ -117,17 +119,18 @@
       * @param {*=} options Additional options for writing. Ignored in
       * this implementation.
       *
       * @return {number} The number of bytes effectively written.
       * @throws {OS.File.Error} In case of I/O error.
       */
      File.prototype._write = function _write(buffer, nbytes, options = {}) {
        return throw_on_negative("write",
-         UnixFile.write(this.fd, buffer, nbytes)
+         UnixFile.write(this.fd, buffer, nbytes),
+         this._path
        );
      };
 
      /**
       * Return the current position in the file.
       */
      File.prototype.getPosition = function getPosition(pos) {
          return this.setPosition(0, File.POS_CURRENT);
@@ -149,28 +152,30 @@
       *
       * @return The new position in the file.
       */
      File.prototype.setPosition = function setPosition(pos, whence) {
        if (whence === undefined) {
          whence = Const.SEEK_SET;
        }
        return throw_on_negative("setPosition",
-         UnixFile.lseek(this.fd, pos, whence)
+         UnixFile.lseek(this.fd, pos, whence),
+         this._path
        );
      };
 
      /**
       * Fetch the information on the file.
       *
       * @return File.Info The information on |this| file.
       */
      File.prototype.stat = function stat() {
-       throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr));
-         return new File.Info(gStatData);
+       throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr),
+                         this._path);
+       return new File.Info(gStatData, this._path);
      };
 
      /**
       * Set the last access and modification date of the file.
       * The time stamp resolution is 1 second at best, but might be worse
       * depending on the platform.
       *
       * @param {Date,number=} accessDate The last access date. If numeric,
@@ -187,32 +192,33 @@
        accessDate = normalizeDate("File.prototype.setDates", accessDate);
        modificationDate = normalizeDate("File.prototype.setDates",
                                         modificationDate);
        gTimevals[0].tv_sec = (accessDate / 1000) | 0;
        gTimevals[0].tv_usec = 0;
        gTimevals[1].tv_sec = (modificationDate / 1000) | 0;
        gTimevals[1].tv_usec = 0;
        throw_on_negative("setDates",
-                         UnixFile.futimes(this.fd, gTimevalsPtr));
+                         UnixFile.futimes(this.fd, gTimevalsPtr),
+                         this._path);
      };
 
      /**
       * Flushes the file's buffers and causes all buffered data
       * to be written.
       * Disk flushes are very expensive and therefore should be used carefully,
       * sparingly and only in scenarios where it is vital that data survives
       * system crashes. Even though the function will be executed off the
       * main-thread, it might still affect the overall performance of any
       * running application.
       *
       * @throws {OS.File.Error} In case of I/O error.
       */
      File.prototype.flush = function flush() {
-       throw_on_negative("flush", UnixFile.fsync(this.fd));
+       throw_on_negative("flush", UnixFile.fsync(this.fd), this._path);
      };
 
      // The default unix mode for opening (0600)
      const DEFAULT_UNIX_MODE = 384;
 
      /**
       * Open a file
       *
@@ -288,17 +294,17 @@
            // flags are sufficient
          } else if (!mode.existing) {
            flags |= Const.O_CREAT;
          }
          if (mode.append) {
            flags |= Const.O_APPEND;
          }
        }
-       return error_or_file(UnixFile.open(path, flags, omode));
+       return error_or_file(UnixFile.open(path, flags, omode), path);
      };
 
      /**
       * Checks if a file exists
       *
       * @param {string} path The path to the file.
       *
       * @return {bool} true if the file exists, false otherwise.
@@ -323,17 +329,17 @@
       */
      File.remove = function remove(path, options = {}) {
        let result = UnixFile.unlink(path);
        if (result == -1) {
          if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
              ctypes.errno == Const.ENOENT) {
            return;
          }
-         throw new File.Error("remove");
+         throw new File.Error("remove", ctypes.errno, path);
        }
      };
 
      /**
       * Remove an empty directory.
       *
       * @param {string} path The name of the directory to remove.
       * @param {*=} options Additional options.
@@ -342,17 +348,17 @@
       */
      File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
        let result = UnixFile.rmdir(path);
        if (result == -1) {
          if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
              ctypes.errno == Const.ENOENT) {
            return;
          }
-         throw new File.Error("removeEmptyDir");
+         throw new File.Error("removeEmptyDir", ctypes.errno, path);
        }
      };
 
      /**
       * Gets the number of bytes available on disk to the current user.
       *
       * @param {string} sourcePath Platform-specific path to a directory on 
       * the disk to query for free available bytes.
@@ -394,17 +400,17 @@
      File.makeDir = function makeDir(path, options = {}) {
        let omode = options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE_DIR;
        let result = UnixFile.mkdir(path, omode);
        if (result == -1) {
          if ((!("ignoreExisting" in options) || options.ignoreExisting) &&
              (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR)) {
            return;
          }
-         throw new File.Error("makeDir");
+         throw new File.Error("makeDir", ctypes.errno, path);
        }
      };
 
      /**
       * Copy a file to a destination.
       *
       * @param {string} sourcePath The platform-specific path at which
       * the file may currently be found.
@@ -461,17 +467,18 @@
        // Adding copying of hierarchies and/or attributes is just a flag
        // away.
        File.copy = function copyfile(sourcePath, destPath, options = {}) {
          let flags = Const.COPYFILE_DATA;
          if (options.noOverwrite) {
            flags |= Const.COPYFILE_EXCL;
          }
          throw_on_negative("copy",
-           UnixFile.copyfile(sourcePath, destPath, null, flags)
+           UnixFile.copyfile(sourcePath, destPath, null, flags),
+           sourcePath
          );
        };
      } else {
        // If the OS does not implement file copying for us, we need to
        // implement it ourselves. For this purpose, we need to define
        // a pumping function.
 
        /**
@@ -639,34 +646,34 @@
        // across file systems
 
        // If necessary, fail if the destination file exists
        if (options.noOverwrite) {
          let fd = UnixFile.open(destPath, Const.O_RDONLY, 0);
          if (fd != -1) {
            fd.dispose();
            // The file exists and we have access
-           throw new File.Error("move", Const.EEXIST);
+           throw new File.Error("move", Const.EEXIST, sourcePath);
          } else if (ctypes.errno == Const.EACCESS) {
            // The file exists and we don't have access
-           throw new File.Error("move", Const.EEXIST);
+           throw new File.Error("move", Const.EEXIST, sourcePath);
          }
        }
 
        // If we can, rename the file
        let result = UnixFile.rename(sourcePath, destPath);
        if (result != -1)
          return;
 
        // If the error is not EXDEV ("not on the same device"),
        // or if the error is EXDEV and we have passed an option
        // that prevents us from crossing devices, throw the
        // error.
        if (ctypes.errno != Const.EXDEV || options.noCopy) {
-         throw new File.Error("move");
+         throw new File.Error("move", ctypes.errno, sourcePath);
        }
 
        // Otherwise, copy and remove.
        File.copy(sourcePath, destPath, options);
        // FIXME: Clean-up in case of copy error?
        File.remove(sourcePath);
      };
 
@@ -684,17 +691,17 @@
       */
      File.DirectoryIterator = function DirectoryIterator(path, options) {
        exports.OS.Shared.AbstractFile.AbstractIterator.call(this);
        this._path = path;
        this._dir = UnixFile.opendir(this._path);
        if (this._dir == null) {
          let error = ctypes.errno;
          if (error != Const.ENOENT) {
-           throw new File.Error("DirectoryIterator", error);
+           throw new File.Error("DirectoryIterator", error, path);
          }
          this._exists = false;
          this._closed = true;
        } else {
          this._exists = true;
          this._closed = false;
        }
      };
@@ -707,17 +714,17 @@
       * Skip special directories "." and "..".
       *
       * @return {File.Entry} The next entry in the directory.
       * @throws {StopIteration} Once all files in the directory have been
       * encountered.
       */
      File.DirectoryIterator.prototype.next = function next() {
        if (!this._exists) {
-         throw File.Error.noSuchFile("DirectoryIterator.prototype.next");
+         throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
        }
        if (this._closed) {
          throw StopIteration;
        }
        for (let entry = UnixFile.readdir(this._dir);
             entry != null && !entry.isNull();
             entry = UnixFile.readdir(this._dir)) {
          let contents = entry.contents;
@@ -725,17 +732,17 @@
          if (name == "." || name == "..") {
            continue;
          }
 
          let isDir, isSymLink;
          if (!("d_type" in contents)) {
            // |dirent| doesn't have d_type on some platforms (e.g. Solaris).
            let path = Path.join(this._path, name);
-           throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr));
+           throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path);
            isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR;
            isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK;
          } else {
            isDir = contents.d_type == Const.DT_DIR;
            isSymLink = contents.d_type == Const.DT_LNK;
          }
 
          return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path);
@@ -763,18 +770,18 @@
      File.DirectoryIterator.prototype.exists = function exists() {
        return this._exists;
      };
 
      /**
       * Return directory as |File|
       */
      File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() {
-       if (!this._dir) throw File.Error.closed();
-       return error_or_file(UnixFile.dirfd(this._dir));
+       if (!this._dir) throw File.Error.closed("unixAsFile", this._path);
+       return error_or_file(UnixFile.dirfd(this._dir), this._path);
      };
 
      /**
       * An entry in a directory.
       */
      File.DirectoryIterator.Entry = function Entry(isDir, isSymLink, name, parent) {
        // Copy the relevant part of |unix_entry| to ensure that
        // our data is not overwritten prematurely.
@@ -805,31 +812,31 @@
        return serialized;
      };
 
      let gStatData = new Type.stat.implementation();
      let gStatDataPtr = gStatData.address();
      let gTimevals = new Type.timevals.implementation();
      let gTimevalsPtr = gTimevals.address();
      let MODE_MASK = 4095 /*= 07777*/;
-     File.Info = function Info(stat) {
+     File.Info = function Info(stat, path) {
        let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR;
        let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK;
        let size = Type.off_t.importFromC(stat.st_size);
 
        let lastAccessDate = new Date(stat.st_atime * 1000);
        let lastModificationDate = new Date(stat.st_mtime * 1000);
        let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000);
 
        let unixOwner = Type.uid_t.importFromC(stat.st_uid);
        let unixGroup = Type.gid_t.importFromC(stat.st_gid);
        let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK);
 
-       SysAll.AbstractInfo.call(this, isDir, isSymLink, size, lastAccessDate,
-           lastModificationDate, unixLastStatusChangeDate,
+       SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+           lastAccessDate, lastModificationDate, unixLastStatusChangeDate,
            unixOwner, unixGroup, unixMode);
 
        // Some platforms (e.g. MacOS X, some BSDs) store a file creation date
        if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) {
          let date = new Date(stat.st_birthtime * 1000);
 
         /**
          * The date of creation of this file.
@@ -880,21 +887,21 @@
       * - {bool} unixNoFollowingLinks If set and |true|, if |path|
       * represents a symbolic link, the call will return the information
       * of the link itself, rather than that of the target file.
       *
       * @return {File.Information}
       */
      File.stat = function stat(path, options = {}) {
        if (options.unixNoFollowingLinks) {
-         throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr));
+         throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path);
        } else {
-         throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr));
+         throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path);
        }
-       return new File.Info(gStatData);
+       return new File.Info(gStatData, path);
      };
 
      /**
       * Set the last access and modification date of the file.
       * The time stamp resolution is 1 second at best, but might be worse
       * depending on the platform.
       *
       * @param {string} path The full name of the file to set the dates for.
@@ -911,40 +918,42 @@
      File.setDates = function setDates(path, accessDate, modificationDate) {
        accessDate = normalizeDate("File.setDates", accessDate);
        modificationDate = normalizeDate("File.setDates", modificationDate);
        gTimevals[0].tv_sec = (accessDate / 1000) | 0;
        gTimevals[0].tv_usec = 0;
        gTimevals[1].tv_sec = (modificationDate / 1000) | 0;
        gTimevals[1].tv_usec = 0;
        throw_on_negative("setDates",
-                         UnixFile.utimes(path, gTimevalsPtr));
+                         UnixFile.utimes(path, gTimevalsPtr),
+                         path);
      };
 
      File.read = exports.OS.Shared.AbstractFile.read;
      File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
      File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
      File.removeDir = exports.OS.Shared.AbstractFile.removeDir;
 
      /**
       * Get the current directory by getCurrentDirectory.
       */
      File.getCurrentDirectory = function getCurrentDirectory() {
        let path = UnixFile.get_current_dir_name?UnixFile.get_current_dir_name():
          UnixFile.getwd_auto(null);
-       throw_on_null("getCurrentDirectory",path);
+       throw_on_null("getCurrentDirectory", path);
        return path.readString();
      };
 
      /**
       * Set the current directory by setCurrentDirectory.
       */
      File.setCurrentDirectory = function setCurrentDirectory(path) {
        throw_on_negative("setCurrentDirectory",
-         UnixFile.chdir(path)
+         UnixFile.chdir(path),
+         path
        );
      };
 
      /**
       * Get/set the current directory.
       */
      Object.defineProperty(File, "curDir", {
          set: function(path) {
@@ -955,43 +964,58 @@
          }
        }
      );
 
      // Utility functions
 
      /**
       * Turn the result of |open| into an Error or a File
+      * @param {number} maybe The result of the |open| operation that may
+      * represent either an error or a success. If -1, this function raises
+      * an error holding ctypes.errno, otherwise it returns the opened file.
+      * @param {string=} path The path of the file.
       */
-     function error_or_file(maybe) {
+     function error_or_file(maybe, path) {
        if (maybe == -1) {
-         throw new File.Error("open");
+         throw new File.Error("open", ctypes.errno, path);
        }
-       return new File(maybe);
+       return new File(maybe, path);
      }
 
      /**
       * Utility function to sort errors represented as "-1" from successes.
       *
+      * @param {string=} operation The name of the operation. If unspecified,
+      * the name of the caller function.
       * @param {number} result The result of the operation that may
       * represent either an error or a success. If -1, this function raises
       * an error holding ctypes.errno, otherwise it returns |result|.
-      * @param {string=} operation The name of the operation. If unspecified,
-      * the name of the caller function.
+      * @param {string=} path The path of the file.
       */
-     function throw_on_negative(operation, result) {
+     function throw_on_negative(operation, result, path) {
        if (result < 0) {
-         throw new File.Error(operation);
+         throw new File.Error(operation, ctypes.errno, path);
        }
        return result;
      }
 
-     function throw_on_null(operation, result) {
+     /**
+      * Utility function to sort errors represented as |null| from successes.
+      *
+      * @param {string=} operation The name of the operation. If unspecified,
+      * the name of the caller function.
+      * @param {pointer} result The result of the operation that may
+      * represent either an error or a success. If |null|, this function raises
+      * an error holding ctypes.errno, otherwise it returns |result|.
+      * @param {string=} path The path of the file.
+      */
+     function throw_on_null(operation, result, path) {
        if (result == null || (result.isNull && result.isNull())) {
-         throw new File.Error(operation);
+         throw new File.Error(operation, ctypes.errno, path);
        }
        return result;
      }
 
      /**
       * Normalize and verify a Date or numeric date value.
       *
       * @param {string} fn Function name of the calling function.
--- a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
@@ -74,24 +74,27 @@ libc.declareLazy(Scope, "FormatMessage",
  * of this field against the error constants of |OS.Constants.Win|.
  *
  * @param {string=} operation The operation that failed. If unspecified,
  * the name of the calling function is taken to be the operation that
  * failed.
  * @param {number=} lastError The OS-specific constant detailing the
  * reason of the error. If unspecified, this is fetched from the system
  * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
  *
  * @constructor
  * @extends {OS.Shared.Error}
  */
-let OSError = function OSError(operation, lastError) {
-  operation = operation || "unknown operation";
-  SharedAll.OSError.call(this, operation);
-  this.winLastError = lastError || ctypes.winLastError;
+let OSError = function OSError(operation = "unknown operation",
+                               lastError = ctypes.winLastError, path = "") {
+  operation = operation;
+  SharedAll.OSError.call(this, operation, path);
+  this.winLastError = lastError;
 };
 OSError.prototype = Object.create(SharedAll.OSError.prototype);
 OSError.prototype.toString = function toString() {
   let buf = new (ctypes.ArrayType(ctypes.jschar, 1024))();
   let result = Scope.FormatMessage(
     Const.FORMAT_MESSAGE_FROM_SYSTEM |
     Const.FORMAT_MESSAGE_IGNORE_INSERTS,
     null,
@@ -102,17 +105,18 @@ OSError.prototype.toString = function to
     /* Format args*/       null
   );
   if (!result) {
     buf = "additional error " +
       ctypes.winLastError +
       " while fetching system error message";
   }
   return "Win error " + this.winLastError + " during operation "
-    + this.operation + " (" + buf.readString() + ")";
+    + this.operation + (this.path? " on file " + this.path : "") +
+    " (" + buf.readString() + ")";
 };
 
 /**
  * |true| if the error was raised because a file or directory
  * already exists, |false| otherwise.
  */
 Object.defineProperty(OSError.prototype, "becauseExists", {
   get: function becauseExists() {
@@ -160,45 +164,56 @@ Object.defineProperty(OSError.prototype,
 
 /**
  * Serialize an instance of OSError to something that can be
  * transmitted across threads (not necessarily a string).
  */
 OSError.toMsg = function toMsg(error) {
   return {
     operation: error.operation,
-    winLastError: error.winLastError
+    winLastError: error.winLastError,
+    path: error.path
   };
 };
 
 /**
  * Deserialize a message back to an instance of OSError
  */
 OSError.fromMsg = function fromMsg(msg) {
-  return new OSError(msg.operation, msg.winLastError);
+  return new OSError(msg.operation, msg.winLastError, msg.path);
 };
 exports.Error = OSError;
 
 /**
  * Code shared by implementation of File.Info on Windows
  *
  * @constructor
  */
-let AbstractInfo = function AbstractInfo(isDir, isSymLink, size, winBirthDate,
+let AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size,
+                                         winBirthDate,
                                          lastAccessDate, lastWriteDate) {
+  this._path = path;
   this._isDir = isDir;
   this._isSymLink = isSymLink;
   this._size = size;
   this._winBirthDate = winBirthDate;
   this._lastAccessDate = lastAccessDate;
   this._lastModificationDate = lastWriteDate;
 };
 
 AbstractInfo.prototype = {
   /**
+   * The path of the file, used for error-reporting.
+   *
+   * @type {string}
+   */
+  get path() {
+    return this._path;
+  },
+  /**
    * |true| if this file is a directory, |false| otherwise
    */
   get isDir() {
     return this._isDir;
   },
   /**
    * |true| if this file is a symbolic link, |false| otherwise
    */
@@ -336,26 +351,26 @@ exports.Type = Type;
  * Native paths
  *
  * Under Windows, expressed as wide strings
  */
 Type.path = Type.wstring.withName("[in] path");
 Type.out_path = Type.out_wstring.withName("[out] path");
 
 // Special constructors that need to be defined on all threads
-OSError.closed = function closed(operation) {
-  return new OSError(operation, Const.ERROR_INVALID_HANDLE);
+OSError.closed = function closed(operation, path) {
+  return new OSError(operation, Const.ERROR_INVALID_HANDLE, path);
 };
 
-OSError.exists = function exists(operation) {
-  return new OSError(operation, Const.ERROR_FILE_EXISTS);
+OSError.exists = function exists(operation, path) {
+  return new OSError(operation, Const.ERROR_FILE_EXISTS, path);
 };
 
-OSError.noSuchFile = function noSuchFile(operation) {
-  return new OSError(operation, Const.ERROR_FILE_NOT_FOUND);
+OSError.noSuchFile = function noSuchFile(operation, path) {
+  return new OSError(operation, Const.ERROR_FILE_NOT_FOUND, path);
 };
 
 let EXPORTED_SYMBOLS = [
   "declareFFI",
   "libc",
   "Error",
   "AbstractInfo",
   "AbstractEntry",
--- a/toolkit/components/osfile/modules/osfile_win_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_front.jsm
@@ -53,20 +53,21 @@
 
      /**
       * Representation of a file.
       *
       * You generally do not need to call this constructor yourself. Rather,
       * to open a file, use function |OS.File.open|.
       *
       * @param fd A OS-specific file descriptor.
+      * @param {string} path File path of the file handle, used for error-reporting.
       * @constructor
       */
-     let File = function File(fd) {
-       exports.OS.Shared.AbstractFile.call(this, fd);
+     let File = function File(fd, path) {
+       exports.OS.Shared.AbstractFile.call(this, fd, path);
        this._closeResult = null;
      };
      File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
 
      /**
       * Close the file.
       *
       * This method has no effect if the file is already closed. However,
@@ -83,17 +84,17 @@
          // Call |close(fd)|, detach finalizer if any
          // (|fd| may not be a CDataFinalizer if it has been
          // instantiated from a controller thread).
          let result = WinFile._CloseHandle(fd);
          if (typeof fd == "object" && "forget" in fd) {
            fd.forget();
          }
          if (result == -1) {
-           this._closeResult = new File.Error("close");
+           this._closeResult = new File.Error("close", ctypes.winLastError, this._path);
          }
        }
        if (this._closeResult) {
          throw this._closeResult;
        }
        return;
      };
 
@@ -110,17 +111,18 @@
       *
       * @return {number} The number of bytes effectively read. If zero,
       * the end of the file has been reached.
       * @throws {OS.File.Error} In case of I/O error.
       */
      File.prototype._read = function _read(buffer, nbytes, options) {
        // |gBytesReadPtr| is a pointer to |gBytesRead|.
        throw_on_zero("read",
-         WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null)
+         WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null),
+         this._path
        );
        return gBytesRead.value;
      };
 
      /**
       * Write some bytes to a file.
       *
       * @param {C pointer} buffer A buffer holding the data that must be
@@ -136,17 +138,18 @@
      File.prototype._write = function _write(buffer, nbytes, options) {
        if (this._appendMode) {
          // Need to manually seek on Windows, as O_APPEND is not supported.
          // This is, of course, a race, but there is no real way around this.
          this.setPosition(0, File.POS_END);
        }
        // |gBytesWrittenPtr| is a pointer to |gBytesWritten|.
        throw_on_zero("write",
-         WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null)
+         WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null),
+         this._path
        );
        return gBytesWritten.value;
      };
 
      /**
       * Return the current position in the file.
       */
      File.prototype.getPosition = function getPosition(pos) {
@@ -169,28 +172,30 @@
       *
       * @return The new position in the file.
       */
      File.prototype.setPosition = function setPosition(pos, whence) {
        if (whence === undefined) {
          whence = Const.FILE_BEGIN;
        }
        return throw_on_negative("setPosition",
-         WinFile.SetFilePointer(this.fd, pos, null, whence));
+         WinFile.SetFilePointer(this.fd, pos, null, whence),
+         this._path);
      };
 
      /**
       * Fetch the information on the file.
       *
       * @return File.Info The information on |this| file.
       */
      File.prototype.stat = function stat() {
        throw_on_zero("stat",
-         WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr));
-       return new File.Info(gFileInfo);
+         WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr),
+         this._path);
+       return new File.Info(gFileInfo, this._path);
      };
 
      /**
       * Set the last access and modification date of the file.
       * The time stamp resolution is 1 second at best, but might be worse
       * depending on the platform.
       *
       * @param {Date,number=} accessDate The last access date. If numeric,
@@ -199,37 +204,39 @@
       * @param {Date,number=} modificationDate The last modification date. If
       * numeric, milliseconds since epoch. If omitted or null, then the current
       * date will be used.
       *
       * @throws {TypeError} In case of invalid parameters.
       * @throws {OS.File.Error} In case of I/O error.
       */
      File.prototype.setDates = function setDates(accessDate, modificationDate) {
-       accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate);
+       accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate, this._path);
        modificationDate = Date_to_FILETIME("File.prototype.setDates",
-                                           modificationDate);
+                                           modificationDate,
+                                           this._path);
        throw_on_zero("setDates",
                      WinFile.SetFileTime(this.fd, null, accessDate.address(),
-                                         modificationDate.address()));
+                                         modificationDate.address()),
+                     this._path);
      };
 
      /**
       * Flushes the file's buffers and causes all buffered data
       * to be written.
       * Disk flushes are very expensive and therefore should be used carefully,
       * sparingly and only in scenarios where it is vital that data survives
       * system crashes. Even though the function will be executed off the
       * main-thread, it might still affect the overall performance of any
       * running application.
       *
       * @throws {OS.File.Error} In case of I/O error.
       */
      File.prototype.flush = function flush() {
-       throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd));
+       throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd), this._path);
      };
 
      // The default sharing mode for opening files: files are not
      // locked against being reopened for reading/writing or against
      // being deleted by the same process or another process.
      // This is consistent with the default Unix policy.
      const DEFAULT_SHARE = Const.FILE_SHARE_READ |
        Const.FILE_SHARE_WRITE | Const.FILE_SHARE_DELETE;
@@ -335,27 +342,28 @@
          } else if (mode.existing) {
            disposition = Const.OPEN_EXISTING;
          } else {
            disposition = Const.OPEN_ALWAYS;
          }
        }
 
        let file = error_or_file(WinFile.CreateFile(path,
-         access, share, security, disposition, flags, template));
+         access, share, security, disposition, flags, template), path);
 
        file._appendMode = !!mode.append;
 
        if (!(mode.trunc && mode.existing)) {
          return file;
        }
        // Now, perform manual truncation
        file.setPosition(0, File.POS_START);
        throw_on_zero("open",
-         WinFile.SetEndOfFile(file.fd));
+         WinFile.SetEndOfFile(file.fd),
+         path);
        return file;
      };
 
      /**
       * Checks if a file or directory exists
       *
       * @param {string} path The path to the file.
       *
@@ -397,17 +405,17 @@
            let newAttributes = attributes & ~Const.FILE_ATTRIBUTE_READONLY;
            if (WinFile.SetFileAttributes(path, newAttributes) &&
                WinFile.DeleteFile(path)) {
              return;
            }
          }
        }
 
-       throw new File.Error("remove");
+       throw new File.Error("remove", ctypes.winLastError, path);
      };
 
      /**
       * Remove an empty directory.
       *
       * @param {string} path The name of the directory to remove.
       * @param {*=} options Additional options.
       *   - {bool} ignoreAbsent If |false|, throw an error if the directory
@@ -415,17 +423,17 @@
       */
      File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
        let result = WinFile.RemoveDirectory(path);
        if (!result) {
          if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
              ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) {
            return;
          }
-         throw new File.Error("removeEmptyDir");
+         throw new File.Error("removeEmptyDir", ctypes.winLastError, path);
        }
      };
 
      /**
       * Create a directory.
       *
       * @param {string} path The name of the directory.
       * @param {*=} options Additional options. This
@@ -442,17 +450,17 @@
        let security = options.winSecurity || null;
        let result = WinFile.CreateDirectory(path, security);
 
        if (result) {
          return;
        }
 
        if (("ignoreExisting" in options) && !options.ignoreExisting) {
-         throw new File.Error("makeDir");
+         throw new File.Error("makeDir", ctypes.winLastError, path);
        }
 
        if (ctypes.winLastError == Const.ERROR_ALREADY_EXISTS) {
          return;
        }
 
        // If the user has no access, but it's a root directory, no error should be thrown
        let splitPath = OS.Path.split(path);
@@ -464,17 +472,17 @@
        }
        // One component consisting of a drive letter implies a directory root.
        if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED &&
            splitPath.winDrive &&
            splitPath.components.length === 1 ) {
          return;
        }
 
-       throw new File.Error("makeDir");
+       throw new File.Error("makeDir", ctypes.winLastError, path);
      };
 
      /**
       * Copy a file to a destination.
       *
       * @param {string} sourcePath The platform-specific path at which
       * the file may currently be found.
       * @param {string} destPath The platform-specific path at which the
@@ -492,17 +500,18 @@
       * behavior is undefined and may not be the same across all platforms.
       *
       * General note: The behavior of this function with respect to metadata
       * is unspecified. Metadata may or may not be copied with the file. The
       * behavior may not be the same across all platforms.
      */
      File.copy = function copy(sourcePath, destPath, options = {}) {
        throw_on_zero("copy",
-         WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false)
+         WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false),
+         sourcePath
        );
      };
 
      /**
       * Move a file to a destination.
       *
       * @param {string} sourcePath The platform-specific path at which
       * the file may currently be found.
@@ -531,17 +540,18 @@
        let flags = 0;
        if (!options.noCopy) {
          flags = Const.MOVEFILE_COPY_ALLOWED;
        }
        if (!options.noOverwrite) {
          flags = flags | Const.MOVEFILE_REPLACE_EXISTING;
        }
        throw_on_zero("move",
-         WinFile.MoveFileEx(sourcePath, destPath, flags)
+         WinFile.MoveFileEx(sourcePath, destPath, flags),
+         sourcePath
        );
 
        // Inherit NTFS permissions from the destination directory
        // if possible.
        if (Path.dirname(sourcePath) === Path.dirname(destPath)) {
          // Skip if the move operation was the simple rename,
          return;
        }
@@ -595,23 +605,24 @@
       * A global value used to receive data during time conversions.
       */
      let gSystemTime = new Type.SystemTime.implementation();
      let gSystemTimePtr = gSystemTime.address();
 
      /**
       * Utility function: convert a FILETIME to a JavaScript Date.
       */
-     let FILETIME_to_Date = function FILETIME_to_Date(fileTime) {
+     let FILETIME_to_Date = function FILETIME_to_Date(fileTime, path) {
        if (fileTime == null) {
          throw new TypeError("Expecting a non-null filetime");
        }
        throw_on_zero("FILETIME_to_Date",
                      WinFile.FileTimeToSystemTime(fileTime.address(),
-                                                  gSystemTimePtr));
+                                                  gSystemTimePtr),
+                     path);
        // Windows counts hours, minutes, seconds from UTC,
        // JS counts from local time, so we need to go through UTC.
        let utc = Date.UTC(gSystemTime.wYear,
                           gSystemTime.wMonth - 1
                           /*Windows counts months from 1, JS from 0*/,
                           gSystemTime.wDay, gSystemTime.wHour,
                           gSystemTime.wMinute, gSystemTime.wSecond,
                           gSystemTime.wMilliSeconds);
@@ -621,17 +632,17 @@
      /**
       * Utility function: convert Javascript Date to FileTime.
       *
       * @param {string} fn Name of the calling function.
       * @param {Date,number} date The date to be converted. If omitted or null,
       * then the current date will be used. If numeric, assumed to be the date
       * in milliseconds since epoch.
       */
-     let Date_to_FILETIME = function Date_to_FILETIME(fn, date) {
+     let Date_to_FILETIME = function Date_to_FILETIME(fn, date, path) {
        if (typeof date === "number") {
          date = new Date(date);
        } else if (!date) {
          date = new Date();
        } else if (typeof date.getUTCFullYear !== "function") {
          throw new TypeError("|date| parameter of " + fn + " must be a " +
                              "|Date| instance or number");
        }
@@ -641,17 +652,18 @@
        gSystemTime.wDay = date.getUTCDate();
        gSystemTime.wHour = date.getUTCHours();
        gSystemTime.wMinute = date.getUTCMinutes();
        gSystemTime.wSecond = date.getUTCSeconds();
        gSystemTime.wMilliseconds = date.getUTCMilliseconds();
        let result = new OS.Shared.Type.FILETIME.implementation();
        throw_on_zero("Date_to_FILETIME",
                      WinFile.SystemTimeToFileTime(gSystemTimePtr,
-                                                  result.address()));
+                                                  result.address()),
+                     path);
        return result;
      };
 
      /**
       * Iterate on one directory.
       *
       * This iterator will not enter subdirectories.
       *
@@ -688,17 +700,17 @@
            this._closed = true;
            this._exists = true;
          } else if (error == Const.ERROR_PATH_NOT_FOUND) {
            // Directory does not exist, let's throw if we attempt to walk it
            SharedAll.LOG("Directory does not exist");
            this._closed = true;
            this._exists = false;
          } else {
-           throw new File.Error("DirectoryIterator", error);
+           throw new File.Error("DirectoryIterator", error, this._path);
          }
        } else {
          this._closed = false;
          this._exists = true;
        }
      };
 
      File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
@@ -707,17 +719,17 @@
      /**
       * Fetch the next entry in the directory.
       *
       * @return null If we have reached the end of the directory.
       */
      File.DirectoryIterator.prototype._next = function _next() {
        // Bailout if the directory does not exist
        if (!this._exists) {
-         throw File.Error.noSuchFile("DirectoryIterator.prototype.next");
+         throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
        }
        // Bailout if the iterator is closed.
        if (this._closed) {
          return null;
        }
        // If this is the first entry, we have obtained it already
        // during construction.
        if (this._first) {
@@ -728,17 +740,17 @@
        if (WinFile.FindNextFile(this._handle, this._findDataPtr)) {
          return this._findData;
        } else {
          let error = ctypes.winLastError;
          this.close();
          if (error == Const.ERROR_NO_MORE_FILES) {
             return null;
          } else {
-            throw new File.Error("iter (FindNextFile)", error);
+            throw new File.Error("iter (FindNextFile)", error, this._path);
          }
        }
      },
 
      /**
       * Return the next entry in the directory, if any such entry is
       * available.
       *
@@ -766,17 +778,18 @@
        if (this._closed) {
          return;
        }
        this._closed = true;
        if (this._handle) {
          // We might not have a handle if the iterator is closed
          // before being used.
          throw_on_zero("FindClose",
-           WinFile.FindClose(this._handle));
+           WinFile.FindClose(this._handle),
+           this._path);
          this._handle = null;
        }
      };
 
     /**
      * Determine whether the directory exists.
      *
      * @return {boolean}
@@ -790,19 +803,19 @@
            !win_entry.ftLastAccessTime || !win_entry.ftLastWriteTime)
         throw new TypeError();
 
        // Copy the relevant part of |win_entry| to ensure that
        // our data is not overwritten prematurely.
        let isDir = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
        let isSymLink = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
 
-       let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime);
-       let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime);
-       let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime);
+       let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime, this._path);
+       let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime, this._path);
+       let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime, this._path);
 
        let name = win_entry.cFileName.readString();
        if (!name) {
          throw new TypeError("Empty name");
        }
 
        if (!parent) {
          throw new TypeError("Empty parent");
@@ -842,30 +855,29 @@
       * Information on a file.
       *
       * To obtain the latest information on a file, use |File.stat|
       * (for an unopened file) or |File.prototype.stat| (for an
       * already opened file).
       *
       * @constructor
       */
-     File.Info = function Info(stat) {
+     File.Info = function Info(stat, path) {
        let isDir = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
        let isSymLink = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
-       
-       let winBirthDate = FILETIME_to_Date(stat.ftCreationTime);
-       let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime);
-       let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime);
+
+       let winBirthDate = FILETIME_to_Date(stat.ftCreationTime, this._path);
+       let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime, this._path);
+       let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime, this._path);
 
        let value = ctypes.UInt64.join(stat.nFileSizeHigh, stat.nFileSizeLow);
        let size = Type.uint64_t.importFromC(value);
 
-       SysAll.AbstractInfo.call(this, isDir, isSymLink, size,
-         winBirthDate, lastAccessDate,
-         lastWriteDate);
+       SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+         winBirthDate, lastAccessDate, lastWriteDate);
      };
      File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype);
 
      /**
       * Return a version of an instance of File.Info that can be sent
       * from a worker thread to the main thread. Note that deserialization
       * is asymmetric and returns an object with a different implementation.
       */
@@ -999,54 +1011,96 @@
        }
      };
 
      /**
       * Set the current directory by setCurrentDirectory.
       */
      File.setCurrentDirectory = function setCurrentDirectory(path) {
        throw_on_zero("setCurrentDirectory",
-         WinFile.SetCurrentDirectory(path));
+         WinFile.SetCurrentDirectory(path),
+         path);
      };
 
      /**
       * Get/set the current directory by |curDir|.
       */
      Object.defineProperty(File, "curDir", {
          set: function(path) {
            this.setCurrentDirectory(path);
          },
          get: function() {
            return this.getCurrentDirectory();
          }
        }
      );
 
      // Utility functions, used for error-handling
-     function error_or_file(maybe) {
+
+     /**
+      * Turn the result of |open| into an Error or a File
+      * @param {number} maybe The result of the |open| operation that may
+      * represent either an error or a success. If -1, this function raises
+      * an error holding ctypes.winLastError, otherwise it returns the opened file.
+      * @param {string=} path The path of the file.
+      */
+     function error_or_file(maybe, path) {
        if (maybe == Const.INVALID_HANDLE_VALUE) {
-         throw new File.Error("open");
+         throw new File.Error("open", ctypes.winLastError, path);
        }
-       return new File(maybe);
+       return new File(maybe, path);
      }
-     function throw_on_zero(operation, result) {
+
+     /**
+      * Utility function to sort errors represented as "0" from successes.
+      *
+      * @param {string=} operation The name of the operation. If unspecified,
+      * the name of the caller function.
+      * @param {number} result The result of the operation that may
+      * represent either an error or a success. If 0, this function raises
+      * an error holding ctypes.winLastError, otherwise it returns |result|.
+      * @param {string=} path The path of the file.
+      */
+     function throw_on_zero(operation, result, path) {
        if (result == 0) {
-         throw new File.Error(operation);
+         throw new File.Error(operation, ctypes.winLastError, path);
        }
        return result;
      }
-     function throw_on_negative(operation, result) {
+
+     /**
+      * Utility function to sort errors represented as "-1" from successes.
+      *
+      * @param {string=} operation The name of the operation. If unspecified,
+      * the name of the caller function.
+      * @param {number} result The result of the operation that may
+      * represent either an error or a success. If -1, this function raises
+      * an error holding ctypes.winLastError, otherwise it returns |result|.
+      * @param {string=} path The path of the file.
+      */
+     function throw_on_negative(operation, result, path) {
        if (result < 0) {
-         throw new File.Error(operation);
+         throw new File.Error(operation, ctypes.winLastError, path);
        }
        return result;
      }
-     function throw_on_null(operation, result) {
+
+     /**
+      * Utility function to sort errors represented as |null| from successes.
+      *
+      * @param {string=} operation The name of the operation. If unspecified,
+      * the name of the caller function.
+      * @param {pointer} result The result of the operation that may
+      * represent either an error or a success. If |null|, this function raises
+      * an error holding ctypes.winLastError, otherwise it returns |result|.
+      * @param {string=} path The path of the file.
+      */
+     function throw_on_null(operation, result, path) {
        if (result == null || (result.isNull && result.isNull())) {
-         throw new File.Error(operation);
+         throw new File.Error(operation, ctypes.winLastError, path);
        }
        return result;
      }
 
      File.Win = exports.OS.Win.File;
      File.Error = SysAll.Error;
      exports.OS.File = File;
      exports.OS.Shared.Type = Type;
new file mode 100644
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+add_task(function* testFileError_with_writeAtomic() {
+  let DEFAULT_CONTENTS = "default contents" + Math.random();
+  let path = Path.join(Constants.Path.tmpDir,
+                       "testFileError.tmp");
+  yield File.remove(path);
+  yield File.writeAtomic(path, DEFAULT_CONTENTS);
+  let exception;
+  try {
+    yield File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true });
+  } catch (ex) {
+    exception = ex;
+  }
+  do_check_true(exception instanceof File.Error);
+  do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_makeDir() {
+  let path = Path.join(Constants.Path.tmpDir,
+                       "directory");
+  yield File.removeDir(path);
+  yield File.makeDir(path);
+  let exception;
+  try {
+    yield File.makeDir(path, { ignoreExisting: false });
+  } catch (ex) {
+    exception = ex;
+  }
+  do_check_true(exception instanceof File.Error);
+  do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_move() {
+  let DEFAULT_CONTENTS = "default contents" + Math.random();
+  let sourcePath = Path.join(Constants.Path.tmpDir,
+                             "src.tmp");
+  let destPath = Path.join(Constants.Path.tmpDir,
+                           "dest.tmp");
+  yield File.remove(sourcePath);
+  yield File.remove(destPath);
+  yield File.writeAtomic(sourcePath, DEFAULT_CONTENTS);
+  yield File.writeAtomic(destPath, DEFAULT_CONTENTS);
+  let exception;
+  try {
+    yield File.move(sourcePath, destPath, { noOverwrite: true });
+  } catch (ex) {
+    exception = ex;
+  }
+  do_print(exception);
+  do_check_true(exception instanceof File.Error);
+  do_check_true(exception.path == sourcePath);
+});
--- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
@@ -22,8 +22,9 @@ tail =
 [test_reset.js]
 [test_shutdown.js]
 [test_unique.js]
 [test_open.js]
 [test_telemetry.js]
 [test_duration.js]
 [test_compression.js]
 [test_osfile_writeAtomic_backupTo_option.js]
+[test_osfile_error.js]
--- a/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
+++ b/toolkit/crashreporter/test/unit/test_crash_AsyncShutdown.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+// Test that AsyncShutdown report errors correctly
+
 function setup_crash() {
   Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this);
   Components.utils.import("resource://gre/modules/Services.jsm", this);
   Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
   Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
   Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10);
 
@@ -24,11 +26,69 @@ function setup_crash() {
 function after_crash(mdump, extra) {
   do_print("after crash: " + extra.AsyncShutdownTimeout);
   let info = JSON.parse(extra.AsyncShutdownTimeout);
   do_check_eq(info.phase, "testing-async-shutdown-crash");
   do_print("Condition: " + JSON.stringify(info.conditions));
   do_check_true(JSON.stringify(info.conditions).indexOf("A blocker that is never satisfied") != -1);
 }
 
+// Test that AsyncShutdown + OS.File reports errors correctly, in a case in which
+// the latest operation succeeded
+
+function setup_osfile_crash_noerror() {
+  Components.utils.import("resource://gre/modules/Services.jsm", this);
+  Components.utils.import("resource://gre/modules/osfile.jsm", this);
+
+  Services.prefs.setBoolPref("toolkit.osfile.debug.failshutdown", true);
+  Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1);
+
+  OS.File.getCurrentDirectory();
+  Services.obs.notifyObservers(null, "profile-before-change", null);
+  dump("Waiting for crash\n");
+};
+
+function after_osfile_crash_noerror(mdump, extra) {
+  do_print("after OS.File crash: " + JSON.stringify(extra.AsyncShutdownTimeout));
+  let info = JSON.parse(extra.AsyncShutdownTimeout);
+  let state = info.conditions[0].state;
+  do_print("Keys: " + Object.keys(state).join(", "));
+  do_check_eq(info.phase, "profile-before-change");
+  do_check_true(state.launched);
+  do_check_false(state.shutdown);
+  do_check_true(state.worker);
+  do_check_true(!!state.latestSent);
+  do_check_eq(state.latestSent[1], "getCurrentDirectory");
+}
+
+// Test that AsyncShutdown + OS.File reports errors correctly, in a case in which
+// the latest operation failed
+
+function setup_osfile_crash_exn() {
+  Components.utils.import("resource://gre/modules/Services.jsm", this);
+  Components.utils.import("resource://gre/modules/osfile.jsm", this);
+
+  Services.prefs.setBoolPref("toolkit.osfile.debug.failshutdown", true);
+  Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 1);
+
+  OS.File.read("I do not exist");
+  Services.obs.notifyObservers(null, "profile-before-change", null);
+  dump("Waiting for crash\n");
+};
+
+function after_osfile_crash_exn(mdump, extra) {
+  do_print("after OS.File crash: " + JSON.stringify(extra.AsyncShutdownTimeout));
+  let info = JSON.parse(extra.AsyncShutdownTimeout);
+  let state = info.conditions[0].state;
+  do_print("Keys: " + Object.keys(state).join(", "));
+  do_check_eq(info.phase, "profile-before-change");
+  do_check_true(state.launched);
+  do_check_false(state.shutdown);
+  do_check_true(state.worker);
+  do_check_true(!!state.latestSent);
+  do_check_eq(state.latestSent[1], "read");
+}
+
 function run_test() {
   do_crash(setup_crash, after_crash);
+  do_crash(setup_osfile_crash_noerror, after_osfile_crash_noerror);
+  do_crash(setup_osfile_crash_exn, after_osfile_crash_exn);
 }
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -676,22 +676,17 @@ var NodeListActor = exports.NodeListActo
       length: this.nodeList.length
     }
   },
 
   /**
    * Get a single node from the node list.
    */
   item: method(function(index) {
-    let node = this.walker._ref(this.nodeList[index]);
-    let newParents = [node for (node of this.walker.ensurePathToRoot(node))];
-    return {
-      node: node,
-      newParents: newParents
-    }
+    return this.walker.attachElement(this.nodeList[index]);
   }, {
     request: { item: Arg(0) },
     response: RetVal("disconnectedNode")
   }),
 
   /**
    * Get a range of the items from the node list.
    */
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -1038,17 +1038,22 @@ let DebuggerEnvironmentSupport = {
   getProperties: function(aObj)
   {
     return aObj.names();
   },
 
   getProperty: function(aObj, aName)
   {
     // TODO: we should use getVariableDescriptor() here - bug 725815.
-    let result = aObj.getVariable(aName);
+    let result = undefined;
+    try {
+      result = aObj.getVariable(aName);
+    } catch (ex) {
+      // getVariable() throws for invalid identifiers.
+    }
     return result === undefined ? null : { value: result };
   },
 };
 
 
 exports.JSPropertyProvider = JSPropertyProvider;
 })(WebConsoleUtils);
 
--- a/toolkit/modules/Log.jsm
+++ b/toolkit/modules/Log.jsm
@@ -452,35 +452,35 @@ Appender.prototype = {
 };
 
 /*
  * DumpAppender
  * Logs to standard out
  */
 
 function DumpAppender(formatter) {
+  Appender.call(this, formatter);
   this._name = "DumpAppender";
-  Appender.call(this, formatter);
 }
 DumpAppender.prototype = {
   __proto__: Appender.prototype,
 
   doAppend: function DApp_doAppend(message) {
     dump(message);
   }
 };
 
 /*
  * ConsoleAppender
  * Logs to the javascript console
  */
 
 function ConsoleAppender(formatter) {
+  Appender.call(this, formatter);
   this._name = "ConsoleAppender";
-  Appender.call(this, formatter);
 }
 ConsoleAppender.prototype = {
   __proto__: Appender.prototype,
 
   doAppend: function CApp_doAppend(message) {
     if (message.level > Log.Level.Warn) {
       Cu.reportError(message);
       return;
@@ -494,18 +494,18 @@ ConsoleAppender.prototype = {
  * Append to an nsIStorageStream
  *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
  * during logging. Instead, one can periodically consume the input stream and
  * e.g. write it to disk asynchronously.
  */
 function StorageStreamAppender(formatter) {
+  Appender.call(this, formatter);
   this._name = "StorageStreamAppender";
-  Appender.call(this, formatter);
 }
 
 StorageStreamAppender.prototype = {
   __proto__: Appender.prototype,
 
   _converterStream: null, // holds the nsIConverterOutputStream
   _outputStream: null,    // holds the underlying nsIOutputStream
 
@@ -576,25 +576,25 @@ StorageStreamAppender.prototype = {
 };
 
 /**
  * File appender
  *
  * Writes output to file using OS.File.
  */
 function FileAppender(path, formatter) {
+  Appender.call(this, formatter);
   this._name = "FileAppender";
   this._encoder = new TextEncoder();
   this._path = path;
   this._file = null;
   this._fileReadyPromise = null;
 
   // This is a promise exposed for testing/debugging the logger itself.
   this._lastWritePromise = null;
-  Appender.call(this, formatter);
 }
 
 FileAppender.prototype = {
   __proto__: Appender.prototype,
 
   _openFile: function () {
     return Task.spawn(function _openFile() {
       try {
@@ -649,21 +649,21 @@ FileAppender.prototype = {
 /**
  * Bounded File appender
  *
  * Writes output to file using OS.File. After the total message size
  * (as defined by message.length) exceeds maxSize, existing messages
  * will be discarded, and subsequent writes will be appended to a new log file.
  */
 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
+  FileAppender.call(this, path, formatter);
   this._name = "BoundedFileAppender";
   this._size = 0;
   this._maxSize = maxSize;
   this._closeFilePromise = null;
-  FileAppender.call(this, path, formatter);
 }
 
 BoundedFileAppender.prototype = {
   __proto__: FileAppender.prototype,
 
   doAppend: function (message) {
     if (!this._removeFilePromise) {
       if (this._size < this._maxSize) {
--- a/toolkit/mozapps/installer/packager.mk
+++ b/toolkit/mozapps/installer/packager.mk
@@ -375,21 +375,26 @@ ROBOCOP_PATH = $(abspath $(_ABS_DIST)/..
 # is used in a series of commands that run under a "cd something", while
 # $(NSINSTALL) is relative.
 INNER_ROBOCOP_PACKAGE= \
   cp $(GECKO_APP_AP_PATH)/fennec_ids.txt $(_ABS_DIST) && \
   $(call RELEASE_SIGN_ANDROID_APK,$(ROBOCOP_PATH)/robocop-debug-unsigned-unaligned.apk,$(_ABS_DIST)/robocop.apk)
 
 BACKGROUND_TESTS_PATH = $(abspath $(_ABS_DIST)/../mobile/android/tests/background/junit3)
 INNER_BACKGROUND_TESTS_PACKAGE= \
-  $(call RELEASE_SIGN_ANDROID_APK,$(BACKGROUND_TESTS_PATH)/background-debug-unsigned-unaligned.apk,$(_ABS_DIST)/background.apk)
+  $(call RELEASE_SIGN_ANDROID_APK,$(BACKGROUND_TESTS_PATH)/background-junit3-debug-unsigned-unaligned.apk,$(_ABS_DIST)/background-junit3.apk)
+
+BROWSER_TESTS_PATH = $(abspath $(_ABS_DIST)/../mobile/android/tests/browser/junit3)
+INNER_BROWSER_TESTS_PACKAGE= \
+  $(call RELEASE_SIGN_ANDROID_APK,$(BROWSER_TESTS_PATH)/browser-junit3-debug-unsigned-unaligned.apk,$(_ABS_DIST)/browser-junit3.apk)
 endif
 else
 INNER_ROBOCOP_PACKAGE=echo 'Testing is disabled - No Android Robocop for you'
-INNER_BACKGROUND_TESTS_PACKAGE=echo 'Testing is disabled - No Android Background tests for you'
+INNER_BACKGROUND_TESTS_PACKAGE=echo 'Testing is disabled - No Android Background JUnit 3 tests for you'
+INNER_BROWSER_TESTS_PACKAGE=echo 'Testing is disabled - No Android Browser JUnit 3tests for you'
 endif
 
 # Create geckoview_library/geckoview_{assets,library}.zip for third-party GeckoView consumers.
 ifdef NIGHTLY_BUILD
 ifndef MOZ_DISABLE_GECKOVIEW
 INNER_MAKE_GECKOVIEW_LIBRARY= \
   $(MAKE) -C ../mobile/android/geckoview_library package
 else
@@ -472,16 +477,17 @@ INNER_MAKE_PACKAGE	= \
   rm -f $(_ABS_DIST)/gecko.apk && \
   cp $(_ABS_DIST)/gecko.ap_ $(_ABS_DIST)/gecko.apk && \
   $(ZIP) -j0 $(_ABS_DIST)/gecko.apk $(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/classes.dex && \
   cp $(_ABS_DIST)/gecko.apk $(_ABS_DIST)/gecko-unsigned-unaligned.apk && \
   $(RELEASE_JARSIGNER) $(_ABS_DIST)/gecko.apk && \
   $(ZIPALIGN) -f -v 4 $(_ABS_DIST)/gecko.apk $(PACKAGE) && \
   $(INNER_ROBOCOP_PACKAGE) && \
   $(INNER_BACKGROUND_TESTS_PACKAGE) && \
+  $(INNER_BROWSER_TESTS_PACKAGE) && \
   $(INNER_MAKE_GECKOVIEW_LIBRARY)
 
 # Language repacks root the resources contained in assets/omni.ja
 # under assets/, but the repacks expect them to be rooted at /.
 # Therefore, we we move the omnijar back to / so the resources are
 # under the root here, in INNER_UNMAKE_PACKAGE. See comments about
 # OMNIJAR_NAME earlier in this file and in configure.in.
 
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -121,19 +121,16 @@
 #ifdef NECKO_PROTOCOL_rtsp
 #include "nsIScriptSecurityManager.h"
 #include "nsIMessageManager.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::ipc;
 
-// Buffer file writes in 32kb chunks
-#define BUFFERED_OUTPUT_SIZE (1024 * 32)
-
 // Download Folder location constants
 #define NS_PREF_DOWNLOAD_DIR        "browser.download.dir"
 #define NS_PREF_DOWNLOAD_FOLDERLIST "browser.download.folderList"
 enum {
   NS_FOLDER_VALUE_DESKTOP = 0
 , NS_FOLDER_VALUE_DOWNLOADS = 1
 , NS_FOLDER_VALUE_CUSTOM = 2
 };