Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Fri, 28 Mar 2014 16:48:17 -0700
changeset 175981 4f3443da36a19ef7a9e204df3e2a7a95832c610d
parent 175949 eb6da95b659ee3c2beefca983a8e11334cc41baf (current diff)
parent 175980 87148610fd2a57b52adf64dc9287e7078000da9d (diff)
child 175982 beea7a7f3fc3a1825b459a71210889b96423870e
child 176017 574ec49a35b8dbc8b6c6ff607e2a4c91018de1fc
child 176052 e0a4cb14e492886bf66bd212bbaa5675cc172630
child 176085 649d52ee8f9f51c39e7bda83745ed291a2c11dd0
push id26502
push userkwierso@gmail.com
push dateFri, 28 Mar 2014 23:48:25 +0000
treeherdermozilla-central@4f3443da36a1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone31.0a1
first release with
nightly linux32
4f3443da36a1 / 31.0a1 / 20140329030204 / files
nightly linux64
4f3443da36a1 / 31.0a1 / 20140329030204 / files
nightly mac
4f3443da36a1 / 31.0a1 / 20140329030204 / files
nightly win32
4f3443da36a1 / 31.0a1 / 20140329030204 / files
nightly win64
4f3443da36a1 / 31.0a1 / 20140329030204 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c
mobile/android/base/db/TransactionalProvider.java
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1412,8 +1412,10 @@ pref("ui.key.menuAccessKeyFocuses", true
 pref("browser.cache.auto_delete_cache_version", 1);
 
 // Telemetry experiments settings.
 pref("experiments.enabled", false);
 pref("experiments.manifest.fetchIntervalSeconds", 86400);
 pref("experiments.manifest.uri", "https://telemetry-experiment.cdn.mozilla.net/manifest/v1/firefox/%VERSION%/%CHANNEL%");
 pref("experiments.manifest.certs.1.commonName", "*.cdn.mozilla.net");
 pref("experiments.manifest.certs.1.issuerName", "CN=Cybertrust Public SureServer SV CA,O=Cybertrust Inc");
+// Whether experiments are supported by the current application profile.
+pref("experiments.supported", true);
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1377,29 +1377,24 @@ let BookmarkingUI = {
       this.notifier.style.transform = starIconTransform;
       this.dropmarkerNotifier.style.transform = dropmarkerTransform;
 
       let dropmarkerAnimationNode = this.dropmarkerNotifier.firstChild;
       dropmarkerAnimationNode.style.MozImageRegion = dropmarkerStyle.MozImageRegion;
       dropmarkerAnimationNode.style.listStyleImage = dropmarkerStyle.listStyleImage;
     }
 
-    let isInBookmarksToolbar = this.button.classList.contains("bookmark-item");
-    if (isInBookmarksToolbar)
-      this.notifier.setAttribute("in-bookmarks-toolbar", true);
-
     let isInOverflowPanel = this.button.getAttribute("overflowedItem") == "true";
     if (!isInOverflowPanel) {
       this.notifier.setAttribute("notification", "finish");
       this.button.setAttribute("notification", "finish");
       this.dropmarkerNotifier.setAttribute("notification", "finish");
     }
 
     this._notificationTimeout = setTimeout( () => {
-      this.notifier.removeAttribute("in-bookmarks-toolbar");
       this.notifier.removeAttribute("notification");
       this.dropmarkerNotifier.removeAttribute("notification");
       this.button.removeAttribute("notification");
 
       this.dropmarkerNotifier.style.transform = '';
       this.notifier.style.transform = '';
     }, 1000);
   },
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -26,17 +26,17 @@
                      value="&newtab.undo.removedLabel;" />
           <xul:button id="newtab-undo-button" tabindex="-1"
                       label="&newtab.undo.undoButton;"
                       class="newtab-undo-button" />
           <xul:button id="newtab-undo-restore-button" tabindex="-1"
                       label="&newtab.undo.restoreButton;"
                       class="newtab-undo-button" />
           <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1"
-                             class="close-icon"
+                             class="close-icon tabbable"
                              tooltiptext="&newtab.undo.closeTooltip;" />
         </div>
       </div>
 
       <xul:spacer flex="1"/>
 
       <div id="newtab-horizontal-margin">
         <div class="newtab-side-margin"/>
--- a/browser/branding/aurora/branding.nsi
+++ b/browser/branding/aurora/branding.nsi
@@ -5,18 +5,18 @@
 # NSIS branding defines for Aurora builds.
 # The official release build branding.nsi is located in other-license/branding/firefox/
 # The unofficial build branding.nsi is located in browser/branding/unofficial/
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Aurora"
 !define CompanyName           "mozilla.org"
-!define URLInfoAbout          "http://www.mozilla.org"
-!define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
+!define URLInfoAbout          "https://www.mozilla.org"
+!define HelpLink              "https://support.mozilla.org"
 
 !define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-aurora-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=aurora&installer_lang=${AB_CD}"
 !define Channel "aurora"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "DigiCert Assured ID Code Signing CA-1"
--- a/browser/branding/nightly/branding.nsi
+++ b/browser/branding/nightly/branding.nsi
@@ -5,18 +5,18 @@
 # NSIS branding defines for nightly builds.
 # The official release build branding.nsi is located in other-license/branding/firefox/
 # The unofficial build branding.nsi is located in browser/branding/unofficial/
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Nightly"
 !define CompanyName           "mozilla.org"
-!define URLInfoAbout          "http://www.mozilla.org"
-!define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
+!define URLInfoAbout          "https://www.mozilla.org"
+!define HelpLink              "https://support.mozilla.org"
 
 !define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-nightly-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=nightly&installer_lang=${AB_CD}"
 !define Channel "nightly"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "DigiCert Assured ID Code Signing CA-1"
--- a/browser/branding/official/branding.nsi
+++ b/browser/branding/official/branding.nsi
@@ -5,18 +5,19 @@
 # NSIS branding defines for official release builds.
 # The nightly build branding.nsi is located in browser/installer/windows/nsis/
 # The unofficial build branding.nsi is located in browser/branding/unofficial/
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Mozilla Firefox"
 !define CompanyName           "Mozilla Corporation"
-!define URLInfoAbout          "https://www.mozilla.org/${AB_CD}/"
-!define URLUpdateInfo         "https://www.mozilla.org/${AB_CD}/firefox/"
+!define URLInfoAbout          "https://www.mozilla.org"
+!define URLUpdateInfo         "https://www.mozilla.org/firefox/${AppVersion}/releasenotes"
+!define HelpLink              "https://support.mozilla.org"
 
 ; The OFFICIAL define is a workaround to support different urls for Release and
 ; Beta since they share the same branding when building with other branches that
 ; set the update channel to beta.
 !define OFFICIAL
 !define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
 !define Channel "release"
--- a/browser/branding/unofficial/branding.nsi
+++ b/browser/branding/unofficial/branding.nsi
@@ -5,18 +5,18 @@
 # NSIS branding defines for unofficial builds.
 # The official release build branding.nsi is located in other-license/branding/firefox/
 # The nightly build branding.nsi is located in browser/installer/windows/nsis/
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Mozilla Developer Preview"
 !define CompanyName           "mozilla.org"
-!define URLInfoAbout          "http://www.mozilla.org"
-!define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
+!define URLInfoAbout          "https://www.mozilla.org"
+!define HelpLink              "https://support.mozilla.org"
 
 !define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
 !define Channel "unofficial"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "Thawte Code Signing CA - G2"
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -805,16 +805,18 @@ let CustomizableUIInternal = {
   },
 
   registerBuildWindow: function(aWindow) {
     if (!gBuildWindows.has(aWindow)) {
       gBuildWindows.set(aWindow, new Set());
 
       aWindow.addEventListener("unload", this);
       aWindow.addEventListener("command", this, true);
+
+      this.notifyListeners("onWindowOpened", aWindow);
     }
   },
 
   unregisterBuildWindow: function(aWindow) {
     aWindow.removeEventListener("unload", this);
     aWindow.removeEventListener("command", this, true);
     gPanelsForWindow.delete(aWindow);
     gBuildWindows.delete(aWindow);
@@ -845,16 +847,18 @@ let CustomizableUIInternal = {
         if (areaNode.ownerDocument == document) {
           toDelete.push(areaNode);
         }
       }
       for (let areaNode of toDelete) {
         areaMap.delete(toDelete);
       }
     }
+
+    this.notifyListeners("onWindowClosed", aWindow);
   },
 
   setLocationAttributes: function(aNode, aArea) {
     let props = gAreas.get(aArea);
     if (!props) {
       throw new Error("Expected area " + aArea + " to have a properties Map " +
                       "associated with it.");
     }
@@ -2472,16 +2476,28 @@ this.CustomizableUI = {
    */
   get WIDE_PANEL_CLASS() "panel-wide-item",
   /**
    * The (constant) number of columns in the menu panel.
    */
   get PANEL_COLUMN_COUNT() 3,
 
   /**
+   * An iteratable property of windows managed by CustomizableUI.
+   * Note that this can *only* be used as an iterator. ie:
+   *     for (let window of CustomizableUI.windows) { ... }
+   */
+  windows: {
+    "@@iterator": function*() {
+      for (let [window,] of gBuildWindows)
+        yield window;
+    }
+  },
+
+  /**
    * Add a listener object that will get fired for various events regarding
    * customization.
    *
    * @param aListener the listener object to add
    *
    * Not all event handler methods need to be defined.
    * CustomizableUI will catch exceptions. Events are dispatched
    * synchronously on the UI thread, so if you can delay any/some of your
@@ -2554,16 +2570,22 @@ this.CustomizableUI = {
    *     Fired when exiting customize mode in aWindow.
    *
    *   - onWidgetOverflow(aNode, aContainer)
    *     Fired when a widget's DOM node is overflowing its container, a toolbar,
    *     and will be displayed in the overflow panel.
    *   - onWidgetUnderflow(aNode, aContainer)
    *     Fired when a widget's DOM node is *not* overflowing its container, a
    *     toolbar, anymore.
+   *   - onWindowOpened(aWindow)
+   *     Fired when a window has been opened that is managed by CustomizableUI,
+   *     once all of the prerequisite setup has been done.
+   *   - onWindowClosed(aWindow)
+   *     Fired when a window that has been managed by CustomizableUI has been
+   *     closed.
    */
   addListener: function(aListener) {
     CustomizableUIInternal.addListener(aListener);
   },
   /**
    * Remove a listener added with addListener
    * @param aListener the listener object to remove
    */
@@ -3269,17 +3291,17 @@ this.CustomizableUI = {
         place = "palette";
 
       node = node.parentNode;
     }
     return place;
   }
 };
 Object.freeze(this.CustomizableUI);
-
+Object.freeze(this.CustomizableUI.windows);
 
 /**
  * All external consumers of widgets are really interacting with these wrappers
  * which provide a common interface.
  */
 
 /**
  * WidgetGroupWrapper is the common interface for interacting with an entire
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -88,16 +88,65 @@ function addShortcut(aNode, aDocument, a
   }
   let shortcut = aDocument.getElementById(shortcutId);
   if (!shortcut) {
     return;
   }
   aItem.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
 }
 
+function fillSubviewFromMenuItems(aMenuItems, aSubview) {
+  let attrs = ["oncommand", "onclick", "label", "key", "disabled",
+               "command", "observes", "hidden", "class", "origin",
+               "image", "checked"];
+
+  let doc = aSubview.ownerDocument;
+  let fragment = doc.createDocumentFragment();
+  for (let menuChild of aMenuItems) {
+    if (menuChild.hidden)
+      continue;
+
+    let subviewItem;
+    if (menuChild.localName == "menuseparator") {
+      // Don't insert duplicate or leading separators. This can happen if there are
+      // menus (which we don't copy) above the separator.
+      if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
+        continue;
+      }
+      subviewItem = doc.createElementNS(kNSXUL, "menuseparator");
+    } else if (menuChild.localName == "menuitem") {
+      subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton");
+      subviewItem.setAttribute("class", "subviewbutton");
+      addShortcut(menuChild, doc, subviewItem);
+    } else {
+      continue;
+    }
+    for (let attr of attrs) {
+      let attrVal = menuChild.getAttribute(attr);
+      if (attrVal)
+        subviewItem.setAttribute(attr, attrVal);
+    }
+    fragment.appendChild(subviewItem);
+  }
+  aSubview.appendChild(fragment);
+}
+
+function clearSubview(aSubview) {
+  let parent = aSubview.parentNode;
+  // We'll take the container out of the document before cleaning it out
+  // to avoid reflowing each time we remove something.
+  parent.removeChild(aSubview);
+
+  while (aSubview.firstChild) {
+    aSubview.firstChild.remove();
+  }
+
+  parent.appendChild(aSubview);
+}
+
 const CustomizableWidgets = [{
     id: "history-panelmenu",
     type: "view",
     viewId: "PanelUI-history",
     shortcutId: "key_gotoHistory",
     tooltiptext: "history-panelmenu.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onViewShowing: function(aEvent) {
@@ -279,139 +328,56 @@ const CustomizableWidgets = [{
     defaultArea: CustomizableUI.AREA_PANEL,
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the developer
       // menu. We skip menu elements, because the menu panel has no way
       // of dealing with those right now.
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
 
-      let items = doc.getElementById("PanelUI-developerItems");
       let menu = doc.getElementById("menuWebDeveloperPopup");
-      let attrs = ["oncommand", "onclick", "label", "key", "disabled",
-                   "command", "observes"];
 
-      let fragment = doc.createDocumentFragment();
       let itemsToDisplay = [...menu.children];
       // Hardcode the addition of the "work offline" menuitem at the bottom:
       itemsToDisplay.push({localName: "menuseparator", getAttribute: () => {}});
       itemsToDisplay.push(doc.getElementById("goOfflineMenuitem"));
-      for (let node of itemsToDisplay) {
-        if (node.hidden)
-          continue;
-
-        let item;
-        if (node.localName == "menuseparator") {
-          // Don't insert duplicate or leading separators. This can happen if there are
-          // menus (which we don't copy) above the separator.
-          if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") {
-            continue;
-          }
-          item = doc.createElementNS(kNSXUL, "menuseparator");
-        } else if (node.localName == "menuitem") {
-          item = doc.createElementNS(kNSXUL, "toolbarbutton");
-          item.setAttribute("class", "subviewbutton");
-          addShortcut(node, doc, item);
-        } else {
-          continue;
-        }
-        for (let attr of attrs) {
-          let attrVal = node.getAttribute(attr);
-          if (attrVal)
-            item.setAttribute(attr, attrVal);
-        }
-        fragment.appendChild(item);
-      }
-      items.appendChild(fragment);
+      fillSubviewFromMenuItems(itemsToDisplay, doc.getElementById("PanelUI-developerItems"));
 
     },
     onViewHiding: function(aEvent) {
       let doc = aEvent.target.ownerDocument;
-      let win = doc.defaultView;
-      let items = doc.getElementById("PanelUI-developerItems");
-      let parent = items.parentNode;
-      // We'll take the container out of the document before cleaning it out
-      // to avoid reflowing each time we remove something.
-      parent.removeChild(items);
-
-      while (items.firstChild) {
-        items.firstChild.remove();
-      }
-
-      parent.appendChild(items);
+      clearSubview(doc.getElementById("PanelUI-developerItems"));
     }
   }, {
     id: "sidebar-button",
     type: "view",
     viewId: "PanelUI-sidebar",
     onViewShowing: function(aEvent) {
       // Largely duplicated from the developer-button above with a couple minor
       // alterations.
       // Populate the subview with whatever menuitems are in the
       // sidebar menu. We skip menu elements, because the menu panel has no way
       // of dealing with those right now.
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
-
-      let items = doc.getElementById("PanelUI-sidebarItems");
       let menu = doc.getElementById("viewSidebarMenu");
 
       // First clear any existing menuitems then populate. Social sidebar
       // options may not have been added yet, so we do that here. Add it to the
       // standard menu first, then copy all sidebar options to the panel.
       win.SocialSidebar.clearProviderMenus();
       let providerMenuSeps = menu.getElementsByClassName("social-provider-menu");
       if (providerMenuSeps.length > 0)
         win.SocialSidebar.populateProviderMenu(providerMenuSeps[0]);
 
-      let attrs = ["oncommand", "onclick", "label", "key", "disabled",
-                   "command", "observes", "hidden", "class", "origin",
-                   "image", "checked"];
-
-      let fragment = doc.createDocumentFragment();
-      let itemsToDisplay = [...menu.children];
-      for (let node of itemsToDisplay) {
-        if (node.hidden)
-          continue;
-
-        let item;
-        if (node.localName == "menuseparator") {
-          item = doc.createElementNS(kNSXUL, "menuseparator");
-        } else if (node.localName == "menuitem") {
-          item = doc.createElementNS(kNSXUL, "toolbarbutton");
-        } else {
-          continue;
-        }
-        for (let attr of attrs) {
-          let attrVal = node.getAttribute(attr);
-          if (attrVal)
-            item.setAttribute(attr, attrVal);
-        }
-        if (node.localName == "menuitem") {
-          item.classList.add("subviewbutton");
-          addShortcut(node, doc, item);
-        }
-        fragment.appendChild(item);
-      }
-
-      items.appendChild(fragment);
+      fillSubviewFromMenuItems([...menu.children], doc.getElementById("PanelUI-sidebarItems"));
     },
     onViewHiding: function(aEvent) {
       let doc = aEvent.target.ownerDocument;
-      let items = doc.getElementById("PanelUI-sidebarItems");
-      let parent = items.parentNode;
-      // We'll take the container out of the document before cleaning it out
-      // to avoid reflowing each time we remove something.
-      parent.removeChild(items);
-
-      while (items.firstChild) {
-        items.firstChild.remove();
-      }
-
-      parent.appendChild(items);
+      clearSubview(doc.getElementById("PanelUI-sidebarItems"));
     }
   }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -88,9 +88,10 @@ skip-if = os == "linux"
 
 [browser_978084_dragEnd_after_move.js]
 [browser_980155_add_overflow_toolbar.js]
 [browser_981418-widget-onbeforecreated-handler.js]
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_987177_destroyWidget_xul.js]
 [browser_987177_xul_wrapper_updating.js]
+[browser_987492_window_api.js]
 [browser_panel_toggle.js]
--- a/browser/components/customizableui/test/browser_981305_separator_insertion.js
+++ b/browser/components/customizableui/test/browser_981305_separator_insertion.js
@@ -1,51 +1,73 @@
 /* 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";
 
 let tempElements = [];
-// Shouldn't insert multiple separators into the developer tools subview
-add_task(function testMultipleDevtoolsSeparators() {
-  let devtoolsSubMenu = document.getElementById("menuWebDeveloperPopup");
+
+function insertTempItemsIntoMenu(parentMenu) {
   // Last element is null to insert at the end:
-  let beforeEls = [devtoolsSubMenu.firstChild, devtoolsSubMenu.lastChild, null];
+  let beforeEls = [parentMenu.firstChild, parentMenu.lastChild, null];
   for (let i = 0; i < beforeEls.length; i++) {
     let sep = document.createElement("menuseparator");
     tempElements.push(sep);
-    devtoolsSubMenu.insertBefore(sep, beforeEls[i]);
+    parentMenu.insertBefore(sep, beforeEls[i]);
     let menu = document.createElement("menu");
     tempElements.push(menu);
-    devtoolsSubMenu.insertBefore(menu, beforeEls[i]);
+    parentMenu.insertBefore(menu, beforeEls[i]);
     // And another separator for good measure:
     sep = document.createElement("menuseparator");
     tempElements.push(sep);
-    devtoolsSubMenu.insertBefore(sep, beforeEls[i]);
+    parentMenu.insertBefore(sep, beforeEls[i]);
   }
-  yield PanelUI.show();
+}
+
+function checkSeparatorInsertion(menuId, buttonId, subviewId) {
+  return function() {
+    info("Checking for duplicate separators in " + buttonId + " widget");
+    let menu = document.getElementById(menuId);
+    insertTempItemsIntoMenu(menu);
 
-  let devtoolsButton = document.getElementById("developer-button");
-  devtoolsButton.click();
-  yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning"));
-  let subview = document.getElementById("PanelUI-developerItems");
-  ok(subview.firstChild, "Subview should have a kid");
-  is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
+    let placement = CustomizableUI.getPlacementOfWidget(buttonId);
+    let changedPlacement = false;
+    if (!placement || placement.area != CustomizableUI.AREA_PANEL) {
+      CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_PANEL);
+      changedPlacement = true;
+    }
+    yield PanelUI.show();
+
+    let button = document.getElementById(buttonId);
+    button.click();
 
-  for (let kid of subview.children) {
-    if (kid.localName == "menuseparator") {
-      ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
-         "Separators should never have another separator next to them, and should never be the first node.");
+    yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning"));
+    let subview = document.getElementById(subviewId);
+    ok(subview.firstChild, "Subview should have a kid");
+    is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with");
+
+    for (let kid of subview.children) {
+      if (kid.localName == "menuseparator") {
+        ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator",
+           "Separators should never have another separator next to them, and should never be the first node.");
+      }
     }
-  }
+
+    let panelHiddenPromise = promisePanelHidden(window);
+    PanelUI.hide();
+    yield panelHiddenPromise;
 
-  let panelHiddenPromise = promisePanelHidden(window);
-  PanelUI.hide();
-  yield panelHiddenPromise;
-});
+    if (changedPlacement) {
+      CustomizableUI.reset();
+    }
+  };
+}
+
+add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems"));
+add_task(checkSeparatorInsertion("viewSidebarMenu", "sidebar-button", "PanelUI-sidebarItems"));
 
 registerCleanupFunction(function() {
   for (let el of tempElements) {
     el.remove();
   }
   tempElements = null;
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_987492_window_api.js
@@ -0,0 +1,54 @@
+/* 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";
+
+
+add_task(function* testOneWindow() {
+  let windows = [];
+  for (let win of CustomizableUI.windows)
+    windows.push(win);
+  is(windows.length, 1, "Should have one customizable window");
+});
+
+
+add_task(function* testOpenCloseWindow() {
+  let newWindow = null;
+  let openListener = {
+    onWindowOpened: function(window) {
+      newWindow = window;
+    }
+  }
+  CustomizableUI.addListener(openListener);
+  let win = yield openAndLoadWindow(null, true);
+  isnot(newWindow, null, "Should have gotten onWindowOpen event");
+  is(newWindow, win, "onWindowOpen event should have received expected window");
+  CustomizableUI.removeListener(openListener);
+
+  let windows = [];
+  for (let win of CustomizableUI.windows)
+    windows.push(win);
+  is(windows.length, 2, "Should have two customizable windows");
+  isnot(windows.indexOf(window), -1, "Current window should be in window collection.");
+  isnot(windows.indexOf(newWindow), -1, "New window should be in window collection.");
+
+  let closedWindow = null;
+  let closeListener = {
+    onWindowClosed: function(window) {
+      closedWindow = window;
+    }
+  }
+  CustomizableUI.addListener(closeListener);
+  yield promiseWindowClosed(newWindow);
+  isnot(closedWindow, null, "Should have gotten onWindowClosed event")
+  is(newWindow, closedWindow, "Closed window should match previously opened window");
+  CustomizableUI.removeListener(closeListener);
+
+  let windows = [];
+  for (let win of CustomizableUI.windows)
+    windows.push(win);
+  is(windows.length, 1, "Should have one customizable window");
+  isnot(windows.indexOf(window), -1, "Current window should be in window collection.");
+  is(windows.indexOf(closedWindow), -1, "Closed window should not be in window collection.");
+});
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -80,16 +80,17 @@ support-files =
   head.js
   sjs_random-javascript.sjs
   testactors.js
 
 [browser_dbg_aaa_run_first_leaktest.js]
 [browser_dbg_addonactor.js]
 [browser_dbg_addon-sources.js]
 [browser_dbg_addon-modules.js]
+[browser_dbg_addon-panels.js]
 [browser_dbg_auto-pretty-print-01.js]
 [browser_dbg_auto-pretty-print-02.js]
 [browser_dbg_bfcache.js]
 [browser_dbg_blackboxing-01.js]
 [browser_dbg_blackboxing-02.js]
 [browser_dbg_blackboxing-03.js]
 [browser_dbg_blackboxing-04.js]
 [browser_dbg_blackboxing-05.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_addon-panels.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that only panels that are relevant to the addon debugger
+// display in the toolbox
+
+const ADDON3_URL = EXAMPLE_URL + "addon3.xpi";
+
+let gAddon, gClient, gThreadClient, gDebugger, gSources;
+
+function test() {
+  Task.spawn(function () {
+    if (!DebuggerServer.initialized) {
+      DebuggerServer.init(() => true);
+      DebuggerServer.addBrowserActors();
+    }
+
+    gBrowser.selectedTab = gBrowser.addTab();
+    let iframe = document.createElement("iframe");
+    document.documentElement.appendChild(iframe);
+
+    let transport = DebuggerServer.connectPipe();
+    gClient = new DebuggerClient(transport);
+
+    let connected = promise.defer();
+    gClient.connect(connected.resolve);
+    yield connected.promise;
+
+    yield installAddon();
+    let debuggerPanel = yield initAddonDebugger(gClient, ADDON3_URL, iframe);
+    gDebugger = debuggerPanel.panelWin;
+    gThreadClient = gDebugger.gThreadClient;
+    gSources = gDebugger.DebuggerView.Sources;
+
+    testPanels(iframe);
+    yield uninstallAddon();
+    yield closeConnection();
+    yield debuggerPanel._toolbox.destroy();
+    iframe.remove();
+    finish();
+  });
+}
+
+function installAddon () {
+  return addAddon(ADDON3_URL).then(aAddon => {
+    gAddon = aAddon;
+  });
+}
+
+function testPanels(frame) {
+  let tabs = frame.contentDocument.getElementById("toolbox-tabs").children;
+  let expectedTabs = ["options", "jsdebugger"];
+
+  is(tabs.length, 2, "displaying only 2 tabs in addon debugger");
+  Array.forEach(tabs, (tab, i) => {
+    let toolName = expectedTabs[i];
+    is(tab.getAttribute("toolid"), toolName, "displaying " + toolName);
+  });
+}
+
+function uninstallAddon() {
+  return removeAddon(gAddon);
+}
+
+function closeConnection () {
+  let deferred = promise.defer();
+  gClient.close(deferred.resolve);
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gClient = null;
+  gAddon = null;
+  gThreadClient = null;
+  gDebugger = null;
+  gSources = null;
+  while (gBrowser.tabs.length > 1) {
+    gBrowser.removeCurrentTab();
+  }
+});
--- a/browser/devtools/framework/target.js
+++ b/browser/devtools/framework/target.js
@@ -237,16 +237,20 @@ TabTarget.prototype = {
     return this._tab ? this._tab.linkedBrowser.contentDocument.location.href :
                        this._form.url;
   },
 
   get isRemote() {
     return !this.isLocalTab;
   },
 
+  get isAddon() {
+    return !!(this._form && this._form.addonActor);
+  },
+
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isThreadPaused() {
     return !!this._isThreadPaused;
   },
 
--- a/browser/devtools/framework/toolbox-options.xul
+++ b/browser/devtools/framework/toolbox-options.xul
@@ -83,18 +83,18 @@
         <vbox id="context-options" class="options-groupbox">
           <checkbox id="devtools-disable-cache"
                     label="&options.disableCache.label;"
                     tooltiptext="&options.disableCache.tooltip;"/>
           <checkbox id="devtools-disable-javascript"
                     label="&options.disableJavaScript.label;"
                     tooltiptext="&options.disableJavaScript.tooltip;"/>
           <hbox class="hidden-labels-box">
-            <checkbox label="&options.enableChrome.label3;"
-                      tooltiptext="&options.enableChrome.tooltip;"
+            <checkbox label="&options.enableChrome.label4;"
+                      tooltiptext="&options.enableChrome.tooltip2;"
                       data-pref="devtools.chrome.enabled"/>
           </hbox>
           <hbox class="hidden-labels-box">
             <checkbox label="&options.enableRemote.label3;"
                       tooltiptext="&options.enableRemote.tooltip;"
                       data-pref="devtools.debugger.remote-enabled"/>
           </hbox>
           <label class="options-citation-label"
--- a/browser/devtools/main.js
+++ b/browser/devtools/main.js
@@ -92,17 +92,17 @@ Tools.webConsole = {
   onkey: function(panel, toolbox) {
     if (toolbox.splitConsole)
       return toolbox.focusConsoleInput();
 
     panel.focusInput();
   },
 
   isTargetSupported: function(target) {
-    return true;
+    return !target.isAddon;
   },
   build: function(iframeWindow, toolbox) {
     let panel = new WebConsolePanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
 Tools.inspector = {
@@ -119,17 +119,17 @@ Tools.inspector = {
   inMenu: true,
 
   preventClosingOnKey: true,
   onkey: function(panel) {
     panel.toolbox.highlighterUtils.togglePicker();
   },
 
   isTargetSupported: function(target) {
-    return true;
+    return !target.isAddon;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new InspectorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
@@ -166,17 +166,17 @@ Tools.styleEditor = {
   icon: "chrome://browser/skin/devtools/tool-styleeditor.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/styleeditor.xul",
   label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
   tooltip: l10n("ToolboxStyleEditor.tooltip2", styleEditorStrings),
   inMenu: true,
 
   isTargetSupported: function(target) {
-    return true;
+    return !target.isAddon;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new StyleEditorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
@@ -186,17 +186,17 @@ Tools.shaderEditor = {
   visibilityswitch: "devtools.shadereditor.enabled",
   icon: "chrome://browser/skin/devtools/tool-styleeditor.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/shadereditor.xul",
   label: l10n("ToolboxShaderEditor.label", shaderEditorStrings),
   tooltip: l10n("ToolboxShaderEditor.tooltip", shaderEditorStrings),
 
   isTargetSupported: function(target) {
-    return true;
+    return !target.isAddon;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new ShaderEditorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
@@ -210,17 +210,17 @@ Tools.jsprofiler = {
   icon: "chrome://browser/skin/devtools/tool-profiler.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
   tooltip: l10n("profiler.tooltip2", profilerStrings),
   inMenu: true,
 
   isTargetSupported: function (target) {
-    return true;
+    return !target.isAddon;
   },
 
   build: function (frame, target) {
     let panel = new ProfilerPanel(frame, target);
     return panel.open();
   }
 };
 
@@ -235,17 +235,17 @@ Tools.netMonitor = {
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/netmonitor.xul",
   label: l10n("netmonitor.label", netMonitorStrings),
   tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
   inMenu: true,
 
   isTargetSupported: function(target) {
     let root = target.client.mainRoot;
-    return root.traits.networkMonitor || !target.isApp;
+    return !target.isAddon && (root.traits.networkMonitor || !target.isApp);
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new NetMonitorPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
@@ -256,17 +256,17 @@ Tools.scratchpad = {
   icon: "chrome://browser/skin/devtools/tool-scratchpad.svg",
   invertIconForLightTheme: true,
   url: "chrome://browser/content/devtools/scratchpad.xul",
   label: l10n("scratchpad.label", scratchpadStrings),
   tooltip: l10n("scratchpad.tooltip", scratchpadStrings),
   inMenu: false,
 
   isTargetSupported: function(target) {
-    return target.isRemote;
+    return !target.isAddon && target.isRemote;
   },
 
   build: function(iframeWindow, toolbox) {
     let panel = new ScratchpadPanel(iframeWindow, toolbox);
     return panel.open();
   }
 };
 
--- a/browser/devtools/sourceeditor/autocomplete.js
+++ b/browser/devtools/sourceeditor/autocomplete.js
@@ -41,21 +41,23 @@ function setupAutoCompletion(ctx, walker
     "Tab": cycle,
     "Down": cycle,
     "Shift-Tab": cycle.bind(this, true),
     "Up": cycle.bind(this, true),
     "Enter": () => {
       if (popup && popup.isOpen) {
         if (!privates.get(ed).suggestionInsertedOnce) {
           privates.get(ed).insertingSuggestion = true;
-          let {label, preLabel} = popup.getItemAtIndex(0);
+          let {label, preLabel, text} = popup.getItemAtIndex(0);
           let cur = ed.getCursor();
-          ed.replaceText(label.slice(preLabel.length), cur, cur);
+          ed.replaceText(text.slice(preLabel.length), cur, cur);
         }
         popup.hidePopup();
+        // This event is used in tests
+        ed.emit("popup-hidden");
         return;
       }
 
       return win.CodeMirror.Pass;
     }
   };
   keyMap[Editor.accel("Space")] = cm => autoComplete(ctx);
   cm.addKeyMap(keyMap);
@@ -130,27 +132,27 @@ function cycleSuggestions(ed, reverse) {
       firstItem = popup.getItemAtIndex(0);
       if (firstItem.label == firstItem.preLabel && popup.itemCount > 1) {
         firstItem = popup.getItemAtIndex(1);
         popup.selectNextItem();
       }
     }
     if (popup.itemCount == 1)
       popup.hidePopup();
-    ed.replaceText(firstItem.label.slice(firstItem.preLabel.length), cur, cur);
+    ed.replaceText(firstItem.text.slice(firstItem.preLabel.length), cur, cur);
   } else {
     let fromCur = {
       line: cur.line,
-      ch  : cur.ch - popup.selectedItem.label.length
+      ch  : cur.ch - popup.selectedItem.text.length
     };
     if (reverse)
       popup.selectPreviousItem();
     else
       popup.selectNextItem();
-    ed.replaceText(popup.selectedItem.label, fromCur, cur);
+    ed.replaceText(popup.selectedItem.text, fromCur, cur);
   }
   // This event is used in tests.
   ed.emit("suggestion-entered");
 }
 
 /**
  * onkeydown handler for the editor instance to prevent autocompleting on some
  * keypresses.
--- a/browser/devtools/sourceeditor/css-autocompleter.js
+++ b/browser/devtools/sourceeditor/css-autocompleter.js
@@ -121,22 +121,24 @@ CSSCompleter.prototype = {
       case CSS_STATES.selector:
         return this.suggestSelectors();
 
       case CSS_STATES.media:
       case CSS_STATES.keyframes:
         if ("media".startsWith(this.completing)) {
           return Promise.resolve([{
             label: "media",
-            preLabel: this.completing
+            preLabel: this.completing,
+            text: "media"
           }]);
         } else if ("keyframes".startsWith(this.completing)) {
           return Promise.resolve([{
-            label: "keyrames",
-            preLabel: this.completing
+            label: "keyframes",
+            preLabel: this.completing,
+            text: "keyframes"
           }]);
         }
     }
     return Promise.resolve([]);
   },
 
   /**
    * Resolves the state of CSS at the cursor location. This method implements a
@@ -780,16 +782,17 @@ CSSCompleter.prototype = {
 
         default:
          value[0] = query.slice(0, query.length - this.completing.length) +
                     value[0];
       }
       completion.push({
         label: value[0],
         preLabel: query,
+        text: value[0],
         score: value[1]
       });
       if (completion.length > this.maxEntries - 1)
         break;
     }
     return completion;
   },
 
@@ -803,19 +806,21 @@ CSSCompleter.prototype = {
     if (!startProp)
       return Promise.resolve(finalList);
 
     let length = propertyNames.length;
     let i = 0, count = 0;
     for (; i < length && count < this.maxEntries; i++) {
       if (propertyNames[i].startsWith(startProp)) {
         count++;
+        let propName = propertyNames[i];
         finalList.push({
           preLabel: startProp,
-          label: propertyNames[i]
+          label: propName,
+          text: propName + ": "
         });
       } else if (propertyNames[i] > startProp) {
         // We have crossed all possible matches alphabetically.
         break;
       }
     }
     return Promise.resolve(finalList);
   },
@@ -835,19 +840,21 @@ CSSCompleter.prototype = {
     if (!startValue)
       list.splice(0, 1);
 
     let length = list.length;
     let i = 0, count = 0;
     for (; i < length && count < this.maxEntries; i++) {
       if (list[i].startsWith(startValue)) {
         count++;
+        let value = list[i];
         finalList.push({
           preLabel: startValue,
-          label: list[i]
+          label: value,
+          text: value
         });
       } else if (list[i] > startValue) {
         // We have crossed all possible matches alphabetically.
         break;
       }
     }
     return Promise.resolve(finalList);
   },
--- a/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_autocomplete.js
@@ -8,70 +8,81 @@ const MAX_SUGGESTIONS = 15;
 // Pref which decides if CSS autocompletion is enabled in Style Editor or not.
 const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
 
 const {CSSProperties, CSSValues} = getCSSKeywords();
 
 // Test cases to test that autocompletion works correctly when enabled.
 // Format:
 // [
-//   -1 for pressing Ctrl + Space or the particular key to press,
-//   Number of suggestions in the popup (-1 if popup is closed),
-//   Index of selected suggestion,
-//   1 to check whether the selected suggestion is inserted into the editor or not
+//   key,
+//   {
+//     total: Number of suggestions in the popup (-1 if popup is closed),
+//     current: Index of selected suggestion,
+//     inserted: 1 to check whether the selected suggestion is inserted into the editor or not,
+//     entered: 1 if the suggestion is inserted and finalized
+//   }
 // ]
 let TEST_CASES = [
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  [-1, 1, 0],
-  ['VK_LEFT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_DOWN', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  [-1, getSuggestionNumberFor("font"), 0],
-  ['VK_END', -1],
-  ['VK_RETURN', -1],
-  ['b', getSuggestionNumberFor("b"), 0],
-  ['a', getSuggestionNumberFor("ba"), 0],
-  ['VK_DOWN', getSuggestionNumberFor("ba"), 0, 1],
-  ['VK_TAB', getSuggestionNumberFor("ba"), 1, 1],
-  [':', getSuggestionNumberFor("background", ""), 0],
-  ['b', getSuggestionNumberFor("background", "b"), 0],
-  ['l', getSuggestionNumberFor("background", "bl"), 0],
-  ['VK_TAB', getSuggestionNumberFor("background", "bl"), 0, 1],
-  ['VK_DOWN', getSuggestionNumberFor("background", "bl"), 1, 1],
-  ['VK_UP', getSuggestionNumberFor("background", "bl"), 0, 1],
-  ['VK_TAB', getSuggestionNumberFor("background", "bl"), 1, 1],
-  ['VK_TAB', getSuggestionNumberFor("background", "bl"), 2, 1],
-  ['VK_LEFT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_DOWN', -1],
-  ['VK_RETURN', -1],
-  ['b', 2, 0],
-  ['u', 1, 0],
-  ['VK_RETURN', -1, 0, 1],
-  ['{', -1],
-  ['VK_HOME', -1],
-  ['VK_DOWN', -1],
-  ['VK_DOWN', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  ['VK_RIGHT', -1],
-  [-1, 1, 0],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['Ctrl+Space', {total: 1, current: 0}],
+  ['VK_LEFT'],
+  ['VK_RIGHT'],
+  ['VK_DOWN'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['Ctrl+Space', { total: getSuggestionNumberFor("font"), current: 0}],
+  ['VK_END'],
+  ['VK_RETURN'],
+  ['b', {total: getSuggestionNumberFor("b"), current: 0}],
+  ['a', {total: getSuggestionNumberFor("ba"), current: 0}],
+  ['VK_DOWN', {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
+  ['VK_TAB', {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
+  ['VK_RETURN', {current: 1, inserted: 1, entered: 1}],
+  ['b', {total: getSuggestionNumberFor("background", "b"), current: 0}],
+  ['l', {total: getSuggestionNumberFor("background", "bl"), current: 0}],
+  ['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 0, inserted: 1}],
+  ['VK_DOWN', {total: getSuggestionNumberFor("background", "bl"), current: 1, inserted: 1}],
+  ['VK_UP', {total: getSuggestionNumberFor("background", "bl"), current: 0, inserted: 1}],
+  ['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 1, inserted: 1}],
+  ['VK_TAB', {total: getSuggestionNumberFor("background", "bl"), current: 2, inserted: 1}],
+  [';'],
+  ['VK_RETURN'],
+  ['c', {total: getSuggestionNumberFor("c"), current: 0}],
+  ['o', {total: getSuggestionNumberFor("co"), current: 0}],
+  ['VK_RETURN', {current: 0, inserted: 1}],
+  ['r', {total: getSuggestionNumberFor("color", "r"), current: 0}],
+  ['VK_RETURN', {current: 0, inserted: 1}],
+  [';'],
+  ['VK_LEFT'],
+  ['VK_RIGHT'],
+  ['VK_DOWN'],
+  ['VK_RETURN'],
+  ['b', {total: 2, current: 0}],
+  ['u', {total: 1, current: 0}],
+  ['VK_RETURN', {current: 0, inserted: 1}],
+  ['{'],
+  ['VK_HOME'],
+  ['VK_DOWN'],
+  ['VK_DOWN'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['VK_RIGHT'],
+  ['Ctrl+Space', {total: 1, current: 0}],
 ];
 
 let gEditor;
 let gPopup;
 let index = 0;
 
 function test()
 {
@@ -95,70 +106,74 @@ function testEditorAdded(aEvent, aEditor
 }
 
 function testState() {
   if (index == TEST_CASES.length) {
     testAutocompletionDisabled();
     return;
   }
 
-  let [key] = TEST_CASES[index];
+  let [key, details] = TEST_CASES[index];
+  let entered;
+  if (details) {
+    entered = details.entered;
+  }
   let mods = {};
 
-  if (key == -1) {
-    info("pressing Ctrl + Space to get result: [" + TEST_CASES[index] +
-         "] for index " + index);
-    gEditor.once("after-suggest", checkState);
+  info("pressing key " + key + " to get result: " +
+                JSON.stringify(TEST_CASES[index]) + " for index " + index);
+
+  let evt = "after-suggest";
+
+  if (key == 'Ctrl+Space') {
     key = " ";
     mods.accelKey = true;
   }
+  else if (key == "VK_RETURN" && entered) {
+    evt = "popup-hidden";
+  }
   else if (/(left|right|return|home|end)/ig.test(key) ||
            (key == "VK_DOWN" && !gPopup.isOpen)) {
-    info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
-         "] for index " + index);
-    gEditor.once("cursorActivity", checkState);
+    evt = "cursorActivity";
   }
   else if (key == "VK_TAB" || key == "VK_UP" || key == "VK_DOWN") {
-    info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
-         "] for index " + index);
-    gEditor.once("suggestion-entered", checkState);
+    evt = "suggestion-entered";
   }
-  else {
-    info("pressing key " + key + " to get result: [" + TEST_CASES[index] +
-         "] for index " + index);
-    gEditor.once("after-suggest", checkState);
-  }
+
+  gEditor.once(evt, checkState);
   EventUtils.synthesizeKey(key, mods, gPanelWindow);
 }
 
 function checkState() {
   executeSoon(() => {
-    info("After keypress for index " + index);
-    let [key, total, current, inserted] = TEST_CASES[index];
-    if (total != -1) {
+    let [key, details] = TEST_CASES[index];
+    details = details || {};
+    let {total, current, inserted} = details;
+
+    if (total != undefined) {
       ok(gPopup.isOpen, "Popup is open for index " + index);
       is(total, gPopup.itemCount,
          "Correct total suggestions for index " + index);
       is(current, gPopup.selectedIndex,
          "Correct index is selected for index " + index);
       if (inserted) {
-        let { preLabel, label } = gPopup.getItemAtIndex(current);
+        let { preLabel, label, text } = gPopup.getItemAtIndex(current);
         let { line, ch } = gEditor.getCursor();
         let lineText = gEditor.getText(line);
-        is(lineText.substring(ch - label.length, ch), label,
+        is(lineText.substring(ch - text.length, ch), text,
            "Current suggestion from the popup is inserted into the editor.");
       }
     }
     else {
       ok(!gPopup.isOpen, "Popup is closed for index " + index);
       if (inserted) {
-        let { preLabel, label } = gPopup.getItemAtIndex(current);
+        let { preLabel, label, text } = gPopup.getItemAtIndex(current);
         let { line, ch } = gEditor.getCursor();
         let lineText = gEditor.getText(line);
-        is(lineText.substring(ch - label.length, ch), label,
+        is(lineText.substring(ch - text.length, ch), text,
            "Current suggestion from the popup is inserted into the editor.");
       }
     }
     index++;
     testState();
   });
 }
 
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -163,16 +163,61 @@ function loadJSONAsync(file, options) {
   });
 }
 
 function telemetryEnabled() {
   return gPrefsTelemetry.get(PREF_TELEMETRY_ENABLED, false) ||
          gPrefsTelemetry.get(PREF_TELEMETRY_PRERELEASE, false);
 }
 
+// Returns a promise that is resolved with the AddonInstall for that URL.
+function addonInstallForURL(url, hash) {
+  let deferred = Promise.defer();
+  AddonManager.getInstallForURL(url, install => deferred.resolve(install),
+                                "application/x-xpinstall", hash);
+  return deferred.promise;
+}
+
+// Returns a promise that is resolved with an Array<Addon> of the installed
+// experiment addons.
+function installedExperimentAddons() {
+  let deferred = Promise.defer();
+  AddonManager.getAddonsByTypes(["experiment"],
+                                addons => deferred.resolve(addons));
+  return deferred.promise;
+}
+
+// Takes an Array<Addon> and returns a promise that is resolved when the
+// addons are uninstalled.
+function uninstallAddons(addons) {
+  let ids = new Set([a.id for (a of addons)]);
+  let deferred = Promise.defer();
+
+  let listener = {};
+  listener.onUninstalled = addon => {
+    if (!ids.has(addon.id)) {
+      return;
+    }
+
+    ids.delete(addon.id);
+    if (ids.size == 0) {
+      AddonManager.removeAddonListener(listener);
+      deferred.resolve();
+    }
+  };
+
+  AddonManager.addAddonListener(listener);
+
+  for (let addon of addons) {
+    addon.uninstall();
+  }
+
+  return deferred.promise;
+}
+
 /**
  * The experiments module.
  */
 
 let Experiments = {
   /**
    * Provides access to the global `Experiments.Experiments` instance.
    */
@@ -1295,77 +1340,101 @@ Experiments.ExperimentEntry.prototype = 
   },
 
   /*
    * Start running the experiment.
    * @return Promise<> Resolved when the operation is complete.
    */
   start: function () {
     gLogger.trace("ExperimentEntry::start() for " + this.id);
+
+    return Task.spawn(function* ExperimentEntry_start_task() {
+      let addons = yield installedExperimentAddons();
+      if (addons.length > 0) {
+        gLogger.error("ExperimentEntry::start() - there are already "
+                      + addons.length + " experiment addons installed");
+        yield uninstallAddons(addons);
+      }
+
+      yield this._installAddon();
+    }.bind(this));
+  },
+
+  // Async install of the addon for this experiment, part of the start task above.
+  _installAddon: function* () {
     let deferred = Promise.defer();
 
-    let installCallback = install => {
-      let failureHandler = (install, handler) => {
-        let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
-                      (install.state || "?") + ", error=" + install.error;
-        gLogger.error("ExperimentEntry::start() - " + message);
-        this._failedStart = true;
+    let install = yield addonInstallForURL(this._manifestData.xpiURL,
+                                           this._manifestData.xpiHash);
+    let failureHandler = (install, handler) => {
+      let message = "AddonInstall " + handler + " for " + this.id + ", state=" +
+                   (install.state || "?") + ", error=" + install.error;
+      gLogger.error("ExperimentEntry::_installAddon() - " + message);
+      this._failedStart = true;
+
+      TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
+                      [TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
+
+      deferred.reject(new Error(message));
+    };
+
+    let listener = {
+      onDownloadEnded: install => {
+        gLogger.trace("ExperimentEntry::_installAddon() - onDownloadEnded for " + this.id);
+
+        if (install.existingAddon) {
+          gLogger.warn("ExperimentEntry::_installAddon() - onDownloadEnded, addon already installed");
+        }
+
+        if (install.addon.type !== "experiment") {
+          gLogger.error("ExperimentEntry::_installAddon() - onDownloadEnded, wrong addon type");
+          install.cancel();
+        }
+      },
+
+      onInstallStarted: install => {
+        gLogger.trace("ExperimentEntry::_installAddon() - onInstallStarted for " + this.id);
+
+        if (install.existingAddon) {
+          gLogger.warn("ExperimentEntry::_installAddon() - onInstallStarted, addon already installed");
+        }
+
+        if (install.addon.type !== "experiment") {
+          gLogger.error("ExperimentEntry::_installAddon() - onInstallStarted, wrong addon type");
+          return false;
+        }
+      },
+
+      onInstallEnded: install => {
+        gLogger.trace("ExperimentEntry::_installAddon() - install ended for " + this.id);
+        this._lastChangedDate = this._policy.now();
+        this._startDate = this._policy.now();
+        this._enabled = true;
 
         TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
-                         [TELEMETRY_LOG.ACTIVATION.INSTALL_FAILURE, this.id]);
-
-        deferred.reject(new Error(message));
-      };
-
-      let listener = {
-        onDownloadEnded: install => {
-          gLogger.trace("ExperimentEntry::start() - onDownloadEnded for " + this.id);
-        },
-
-        onInstallStarted: install => {
-          gLogger.trace("ExperimentEntry::start() - onInstallStarted for " + this.id);
-          // TODO: this check still needs changes in the addon manager
-          //if (install.addon.type !== "experiment") {
-          //  gLogger.error("ExperimentEntry::start() - wrong addon type");
-          //  failureHandler({state: -1, error: -1}, "onInstallStarted");
-          //}
+                       [TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
 
-          let addon = install.addon;
-          this._name = addon.name;
-          this._addonId = addon.id;
-          this._description = addon.description || "";
-          this._homepageURL = addon.homepageURL || "";
-        },
-
-        onInstallEnded: install => {
-          gLogger.trace("ExperimentEntry::start() - install ended for " + this.id);
-          this._lastChangedDate = this._policy.now();
-          this._startDate = this._policy.now();
-          this._enabled = true;
+        let addon = install.addon;
+        this._name = addon.name;
+        this._addonId = addon.id;
+        this._description = addon.description || "";
+        this._homepageURL = addon.homepageURL || "";
 
-          TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY,
-                           [TELEMETRY_LOG.ACTIVATION.ACTIVATED, this.id]);
-
-          deferred.resolve();
-        },
-      };
-
-      ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
-        .forEach(what => {
-          listener[what] = install => failureHandler(install, what)
-        });
-
-      install.addListener(listener);
-      install.install();
+        deferred.resolve();
+      },
     };
 
-    AddonManager.getInstallForURL(this._manifestData.xpiURL,
-                                  installCallback,
-                                  "application/x-xpinstall",
-                                  this._manifestData.xpiHash);
+    ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
+      .forEach(what => {
+        listener[what] = install => failureHandler(install, what)
+      });
+
+    install.addListener(listener);
+    install.install();
+
     return deferred.promise;
   },
 
   /*
    * Stop running the experiment if it is active.
    * @param terminationKind (optional) The termination kind, e.g. USERDISABLED or EXPIRED.
    * @param terminationReason (optional) The termination reason details for
    *                          termination kind RECHECK.
@@ -1390,35 +1459,19 @@ Experiments.ExperimentEntry.prototype = 
       if (!addon) {
         let message = "could not get Addon for " + this.id;
         gLogger.warn("ExperimentEntry::stop() - " + message);
         updateDates();
         deferred.resolve();
         return;
       }
 
-      let listener = {};
-      let handler = addon => {
-        if (addon.id !== this._addonId) {
-          return;
-        }
-
-        updateDates();
-        this._logTermination(terminationKind, terminationReason);
-
-        AddonManager.removeAddonListener(listener);
-        deferred.resolve();
-      };
-
-      listener.onUninstalled = handler;
-      listener.onDisabled = handler;
-
-      AddonManager.addAddonListener(listener);
-
-      addon.uninstall();
+      updateDates();
+      this._logTermination(terminationKind, terminationReason);
+      deferred.resolve(uninstallAddons([addon]));
     });
 
     return deferred.promise;
   },
 
   _logTermination: function (terminationKind, terminationReason) {
     if (terminationKind === undefined) {
       return;
index ede358c25878f81e6f7bdb9830873a9f6f2a3b8e..b336ff03a155107c128599f9de44304b8398a6cd
GIT binary patch
literal 522
zc$^FHW@Zs#U|`^2km)XSx%;SGI*yTn;R6!`12=;VLuOuaNn%cpUQtR~Xb2|*bNEi(
zU=S{?;AUWC`3h7C)?6BNKEKI8WZ&oTl8WdcuAFvHf!IfT-fq(IE?e{IO4=6Hx5Y31
z{hF%ORQT9szx?~^_gjDd(a`?HdyH#sTB6X=hkH1h{^agd>I*Crdnfq1XXCQWMb8v3
zr5&I2vgFjGDN8j?pH90q*{N>HDZfJvTjramDXiZU^uJ@(qkRri$NPCwUe9aM>zh!+
zS(fb5yN>tJ^<NE}KO}^dm8CjGJ_&VI4Nu}0o4eZglw;Z=oePR0eWntMj*7lwx=F`p
zTE1Q)e!KFS(np_qE<Kx~&CE6HUVG(ORGs55znaz0J2xY?mNVwh^gG^51LU8dxWVmq
zVCw|=>OcF0`5f*G?0$Jd`~Qpu%I`H)wHJ#{5)Yje?y&F43C^X5%S5IJ?Afh4DRJ$R
z=&8%~4i$;~<@DHbX$QOUwukfjPv5w>fA^)}C#$EKD_az-HH|Xaqjy2(z-zXQ9V>%*
zT_@R<I&WwHzi8d|NyguobUzRe@MdI^W5yLp5)2>!<T5O21hG(}j1>}PXwenm&B_MR
O#K;f`q!)v9GXMbWJ<xgp
index 4d67787e63e75e1e8b581ad3424cd412f1e2eab2..ad21c3bb7785c986c8a5a855cc3387ffdf4747bf
GIT binary patch
literal 525
zc$^FHW@Zs#U|`^25a}*+;WSLGOk`wW_`$@$z|A1TkeOFpl9-dDSCo<#8p6rI?7mYs
z7=%kJxEUB(z5-Q(HJ1h*ENU_k*!x*j^PZJ3)0XyA3a@&;Uh`glxq3>du`1`}HLSnC
z@&>XTpC(-S``*6ov!8qp3wt8nBN}z~kV@C#N<o&Nzq5Q}TyI8JIIJ-~HfveutmPiF
zZM~OW*6a?Rw(QCj>nwk+O7E$2TNq#3>7Q}9m$CGpVCd1>W|O}8k|+FZIp@kM{^WXd
ztwSo7=aBiYj?Eu3LdpVDog$xvx~hgJv5U=JReZ`ZZIR9eMUh)lJrfp8Q0wN~n0$WD
z+?C-sQ=chy3dO%Ze{gE!oi~irOJh5A-Qzz$zT5J4qhj~9tJ4o0t-og*%JsE!V~=ej
z%N?Q0+mHLxA2EDsyb-H({!emK@yEuXO=eMwm7%JY%ulrY<^)*1aWCXN+8(VeDdznv
z_2;9hQGI8oue;AVp<wAphBTdH%<|@&7uT0>U4Eim?>$@L1Gnrg85OY$ESsM5UMPrP
zF4d({YuSHK_Rr~EcYV*@xvbX|AK=Z%B*%;^o+KDR0LW!n(g<Rq#2PCk*3e=sz?+o~
PqKT0q5J)cr>1F@`QI5?%
index ff2a262687a1210438853db1406f97a4cb289f58..79cb1810e12e7d1599ca4409ac0b441107eb913d
GIT binary patch
literal 523
zc$^FHW@Zs#U|`^2u;?yxQ8~D}E}oHr;R_Q312=;VLuOuaNn%cpUQtR~Xb2|*bI(rQ
zU=S{?;AUWC`3h7C)?6CoTij$Iu=lfe(4JB?mJI%>j#ngKubu9<{JX~LG%q2wNbYaf
z&0Pd#qWzxKzPWu@W&ZX}O7|1iT*wkr3zRw6t#IOgc=jWS)bxjp8_!8(&r-{pr<D6m
zXQtnB9lyv~LBa1<*lIq}4J{L5Tk?C4A#>&9slVJqM8C6MvZzZ_u>P)6QrPlDdqcEx
zR!OeFzKia2THP0K>ed!MrL<m4Yp0O)h8HohL3~ESsf~OKFI-D#VQDpqE$NU=fA&Ua
z?=3xJ@y_&L!39qz&bEE9X`S}Qr!Oq+@A$5^H7i+E`c<{yV|<bRRHxcFrDS~x#_dk`
zzWsO{X~9tI{AQi<`aj7{#y=f2Bc<0)*t2TV9>!0i?tU7w+ZOL=d30EJB2TvJO8wU>
zS8tGU)sNjOtnkq5G2@vVUF>#xsqX*ZMfp!$A9Jtw;ep<385ck7awuk5FTLR5Ha}Al
z&tFAyd#rzim+qc9XJ>jW%l-gwMkYCCT(Km<00KZR!;(f23nk82A#sKlUjg2%Y!FS1
L41pkxAl(cAYoEps
--- a/browser/experiments/test/xpcshell/head.js
+++ b/browser/experiments/test/xpcshell/head.js
@@ -8,26 +8,26 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://services-sync/healthreport.jsm", this);
 Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
 
 const EXPERIMENT1_ID       = "test-experiment-1@tests.mozilla.org";
-const EXPERIMENT1_XPI_SHA1 = "sha1:08c4d3ef1d0fc74faa455e85106ef0bc8cf8ca90";
+const EXPERIMENT1_XPI_SHA1 = "sha1:0f15ee3677ffbf1e82367069fe4e8fe8e2ad838f";
 const EXPERIMENT1_XPI_NAME = "experiment-1.xpi";
 const EXPERIMENT1_NAME     = "Test experiment 1";
 
-const EXPERIMENT1A_XPI_SHA1 = "sha1:2b8d14e3e06a54d5ce628fe3598cbb364cff9e6b";
+const EXPERIMENT1A_XPI_SHA1 = "sha1:b938f1b4f0bf466a67257aff26d4305ac24231eb";
 const EXPERIMENT1A_XPI_NAME = "experiment-1a.xpi";
 const EXPERIMENT1A_NAME     = "Test experiment 1.1";
 
 const EXPERIMENT2_ID       = "test-experiment-2@tests.mozilla.org"
-const EXPERIMENT2_XPI_SHA1 = "sha1:81877991ec70360fb48db84c34a9b2da7aa41d6a";
+const EXPERIMENT2_XPI_SHA1 = "sha1:9d23425421941e1d1e2037232cf5aeae82dbd4e4";
 const EXPERIMENT2_XPI_NAME = "experiment-2.xpi";
 
 const EXPERIMENT3_ID       = "test-experiment-3@tests.mozilla.org";
 const EXPERIMENT4_ID       = "test-experiment-4@tests.mozilla.org";
 
 const DEFAULT_BUILDID      = "2014060601";
 
 const FAKE_EXPERIMENTS_1 = [
@@ -155,21 +155,21 @@ function uninstallAddon(id) {
 
     AddonManager.addAddonListener(listener);
     addon.uninstall();
   });
 
   return deferred.promise;
 }
 
-function createAppInfo(options) {
+function createAppInfo(optionsIn) {
   const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
   const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
 
-  let options = options || {};
+  let options = optionsIn || {};
   let id = options.id || "xpcshell@tests.mozilla.org";
   let name = options.name || "XPCShell";
   let version = options.version || "1.0";
   let platformVersion = options.platformVersion || "1.0";
   let date = options.date || new Date();
 
   let buildID = options.buildID || DEFAULT_BUILDID;
 
--- a/browser/experiments/test/xpcshell/test_api.js
+++ b/browser/experiments/test/xpcshell/test_api.js
@@ -258,16 +258,89 @@ add_task(function* test_getExperiments()
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
   yield experiments.uninit();
   yield removeCacheFile();
 });
 
+// Test that we handle the experiments addon already being
+// installed properly.
+// We should just pave over them.
+
+add_task(function* test_addonAlreadyInstalled() {
+  const OBSERVER_TOPIC = "experiments-changed";
+  let observerFireCount = 0;
+  let expectedObserverFireCount = 0;
+  let observer = () => ++observerFireCount;
+  Services.obs.addObserver(observer, OBSERVER_TOPIC, false);
+
+  // Dates the following tests are based on.
+
+  let baseDate   = new Date(2014, 5, 1, 12);
+  let startDate  = futureDate(baseDate,   100 * MS_IN_ONE_DAY);
+  let endDate    = futureDate(baseDate, 10000 * MS_IN_ONE_DAY);
+
+  // The manifest data we test with.
+
+  gManifestObject = {
+    "version": 1,
+    experiments: [
+      {
+        id:               EXPERIMENT1_ID,
+        xpiURL:           gDataRoot + EXPERIMENT1_XPI_NAME,
+        xpiHash:          EXPERIMENT1_XPI_SHA1,
+        startTime:        dateToSeconds(startDate),
+        endTime:          dateToSeconds(endDate),
+        maxActiveSeconds: 10 * SEC_IN_ONE_DAY,
+        appName:          ["XPCShell"],
+        channel:          ["nightly"],
+      },
+    ],
+  };
+
+  let experiments = new Experiments.Experiments(gPolicy);
+
+  // Trigger update, clock set to before any activation.
+
+  let now = baseDate;
+  defineNow(gPolicy, now);
+  yield experiments.updateManifest();
+  Assert.equal(observerFireCount, 0,
+               "Experiments observer should not have been called yet.");
+  let list = yield experiments.getExperiments();
+  Assert.equal(list.length, 0, "Experiment list should be empty.");
+
+  // Install conflicting addon.
+
+  let installed = yield installAddon(gDataRoot + EXPERIMENT1_XPI_NAME, EXPERIMENT1_XPI_SHA1);
+  Assert.ok(installed, "Addon should have been installed.");
+
+  // Trigger update, clock set for the experiment to start.
+
+  now = futureDate(startDate, 10 * MS_IN_ONE_DAY);
+  defineNow(gPolicy, now);
+  yield experiments.updateManifest();
+  Assert.equal(observerFireCount, ++expectedObserverFireCount,
+               "Experiments observer should have been called.");
+
+  list = yield experiments.getExperiments();
+  list = yield experiments.getExperiments();
+  Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
+  Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
+  Assert.equal(list[0].active, true, "Experiment 1 should be active.");
+
+  // Cleanup.
+
+  Services.obs.removeObserver(observer, OBSERVER_TOPIC);
+  yield experiments.uninit();
+  yield removeCacheFile();
+});
+
 add_task(function* test_lastActiveToday() {
   let experiments = new Experiments.Experiments(gPolicy);
 
   replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
 
   let e = yield experiments.getExperiments();
   Assert.equal(e.length, 1, "Monkeypatch successful.");
   Assert.equal(e[0].id, "id1", "ID looks sane");
--- a/browser/installer/windows/nsis/shared.nsh
+++ b/browser/installer/windows/nsis/shared.nsh
@@ -635,20 +635,29 @@ FunctionEnd
   ${If} $2 == ""
     ${GetLongPath} "$INSTDIR" $8
 
     ; Write the uninstall registry keys
     ${WriteRegStr2} $1 "$0" "Comments" "${BrandFullNameInternal} ${AppVersion}$3 (${ARCH} ${AB_CD})" 0
     ${WriteRegStr2} $1 "$0" "DisplayIcon" "$8\${FileMainEXE},0" 0
     ${WriteRegStr2} $1 "$0" "DisplayName" "${BrandFullNameInternal} ${AppVersion}$3 (${ARCH} ${AB_CD})" 0
     ${WriteRegStr2} $1 "$0" "DisplayVersion" "${AppVersion}" 0
+    ${WriteRegStr2} $1 "$0" "HelpLink" "${HelpLink}" 0
     ${WriteRegStr2} $1 "$0" "InstallLocation" "$8" 0
     ${WriteRegStr2} $1 "$0" "Publisher" "Mozilla" 0
     ${WriteRegStr2} $1 "$0" "UninstallString" "$\"$8\uninstall\helper.exe$\"" 0
+    DeleteRegValue SHCTX "$0" "URLInfoAbout"
+; Don't add URLInfoAbout which is the release notes url except for the release
+; and esr channels since nightly, aurora, and beta do not have release notes.
+; Note: URLInfoAbout is only defined in the official branding.nsi.
+!ifdef URLInfoAbout
+!ifndef BETA_UPDATE_CHANNEL
     ${WriteRegStr2} $1 "$0" "URLInfoAbout" "${URLInfoAbout}" 0
+!endif
+!endif
     ${WriteRegStr2} $1 "$0" "URLUpdateInfo" "${URLUpdateInfo}" 0
     ${WriteRegDWORD2} $1 "$0" "NoModify" 1 0
     ${WriteRegDWORD2} $1 "$0" "NoRepair" 1 0
 
     ${GetSize} "$8" "/S=0K" $R2 $R3 $R4
     ${WriteRegDWORD2} $1 "$0" "EstimatedSize" $R2 0
 
     ${If} "$TmpVal" == "HKLM"
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -25,16 +25,17 @@ RequestExecutionLevel user
   ManifestSupportedOS all
   ManifestDPIAware true
 !endif
 
 !addplugindir ./
 
 Var Dialog
 Var Progressbar
+Var ProgressbarMarqueeIntervalMS
 Var LabelDownloading
 Var LabelInstalling
 Var LabelFreeSpace
 Var CheckboxSetAsDefault
 Var CheckboxShortcutOnBar ; Used for Quicklaunch or Taskbar as appropriate
 Var CheckboxShortcutInStartMenu
 Var CheckboxShortcutOnDesktop
 Var CheckboxSendPing
@@ -79,24 +80,23 @@ Var FirefoxLaunchCode
 ; the display of individual installer pages.
 Var StartIntroPhaseTickCount
 Var StartOptionsPhaseTickCount
 Var StartDownloadPhaseTickCount
 ; Since the Intro and Options pages can be displayed multiple times the total
 ; seconds spent on each of these pages is reported.
 Var IntroPhaseSeconds
 Var OptionsPhaseSeconds
-; The tick count for the last download
+; The tick count for the last download.
 Var StartLastDownloadTickCount
 ; The number of seconds from the start of the download phase until the first
 ; bytes are received. This is only recorded for first request so it is possible
 ; to determine connection issues for the first request.
 Var DownloadFirstTransferSeconds
 ; The last four tick counts are for the end of a phase in the installation page.
-; the options phase when it isn't entered.
 Var EndDownloadPhaseTickCount
 Var EndPreInstallPhaseTickCount
 Var EndInstallPhaseTickCount
 Var EndFinishPhaseTickCount
 
 Var InitialInstallRequirementsCode
 Var ExistingProfile
 Var ExistingVersion
@@ -172,34 +172,35 @@ Var ControlRightPX
 
 ; Interval for the install timer
 !define InstallIntervalMS 100
 
 ; The first step for the install progress bar. By starting with a large step
 ; immediate feedback is given to the user.
 !define InstallProgressFirstStep 20
 
+; The finish step size to quickly increment the progress bar after the
+; installation has finished.
+!define InstallProgressFinishStep 40
+
 ; Number of steps for the install progress.
 ; This might not be enough when installing on a slow network drive so it will
 ; fallback to downloading the full installer if it reaches this number. The size
 ; of the install progress step is increased when the full installer finishes
 ; instead of waiting.
 
 ; Approximately 150 seconds with a 100 millisecond timer and a first step of 20
 ; as defined by InstallProgressFirstStep.
 !define /math InstallCleanTotalSteps ${InstallProgressFirstStep} + 1500
 
 ; Approximately 165 seconds (minus 0.2 seconds for each file that is removed)
 ; with a 100 millisecond timer and a first step of 20 as defined by
 ; InstallProgressFirstStep .
 !define /math InstallPaveOverTotalSteps ${InstallProgressFirstStep} + 1800
 
-; The interval in MS used for the progress bars set as marquee.
-!define ProgressbarMarqueeIntervalMS 10
-
 ; On Vista and above attempt to elevate Standard Users in addition to users that
 ; are a member of the Administrators group.
 !define NONADMIN_ELEVATE
 
 !define CONFIG_INI "config.ini"
 
 !ifndef FILE_SHARE_READ
   !define FILE_SHARE_READ 1
@@ -383,16 +384,23 @@ Function .onInit
   ${OrIf} ${AtLeastWin8}
     StrCpy $CanSetAsDefault "false"
     StrCpy $CheckboxSetAsDefault "0"
   ${Else}
     DeleteRegValue HKLM "Software\Mozilla" "${BrandShortName}InstallerTest"
     StrCpy $CanSetAsDefault "true"
   ${EndIf}
 
+  ; The interval in MS used for the progress bars set as marquee.
+  ${If} ${AtLeastWinVista}
+    StrCpy $ProgressbarMarqueeIntervalMS "10"
+  ${Else}
+    StrCpy $ProgressbarMarqueeIntervalMS "50"
+  ${EndIf}
+
   ; Initialize the majority of variables except those that need to be reset
   ; when a page is displayed.
   StrCpy $IntroPhaseSeconds "0"
   StrCpy $OptionsPhaseSeconds "0"
   StrCpy $EndPreInstallPhaseTickCount "0"
   StrCpy $EndInstallPhaseTickCount "0"
   StrCpy $InitialInstallRequirementsCode ""
   StrCpy $IsDownloadFinished ""
@@ -443,27 +451,27 @@ Function .onGUIEnd
   Delete "$PLUGINSDIR\${CONFIG_INI}"
 
   ${UnloadUAC}
 FunctionEnd
 
 Function .onUserAbort
   ${NSD_KillTimer} StartDownload
   ${NSD_KillTimer} OnDownload
-  ${NSD_KillTimer} StartInstall
   ${NSD_KillTimer} CheckInstall
   ${NSD_KillTimer} FinishInstall
+  ${NSD_KillTimer} FinishProgressBar
   ${NSD_KillTimer} DisplayDownloadError
 
   ${If} "$IsDownloadFinished" != ""
     Call DisplayDownloadError
     ; Aborting the abort will allow SendPing which is called by
     ; DisplayDownloadError to hide the installer window and close the installer
     ; after it sends the metrics ping.
-    Abort 
+    Abort
   ${EndIf}
 FunctionEnd
 
 Function SendPing
   HideWindow
   ; Try to send a ping if a download was attempted
   ${If} $CheckboxSendPing == 1
   ${AndIf} $IsDownloadFinished != ""
@@ -1162,17 +1170,17 @@ Function createInstall
   SetCtlColors $LabelBlurb3 ${INSTALL_BLURB_TEXT_COLOR} transparent
   ShowWindow $BitmapBlurb3 ${SW_HIDE}
   ShowWindow $LabelBlurb3 ${SW_HIDE}
 
   ${NSD_CreateProgressBar} 103u 166u 241u 9u ""
   Pop $Progressbar
   ${NSD_AddStyle} $Progressbar ${PBS_MARQUEE}
   SendMessage $Progressbar ${PBM_SETMARQUEE} 1 \
-              ${ProgressbarMarqueeIntervalMS} ; start=1|stop=0 interval(ms)=+N
+              $ProgressbarMarqueeIntervalMS ; start=1|stop=0 interval(ms)=+N
 
   ${NSD_CreateLabelCenter} 103u 180u 241u 20u "$(DOWNLOADING_LABEL)"
   Pop $LabelDownloading
   SendMessage $LabelDownloading ${WM_SETFONT} $FontNormal 0
   SetCtlColors $LabelDownloading ${INSTALL_PROGRESS_TEXT_COLOR_NORMAL} transparent
 
   ${If} ${FileExists} "$INSTDIR\${FileMainEXE}"
     ${NSD_CreateLabelCenter} 103u 180u 241u 20u "$(UPGRADING_LABEL)"
@@ -1301,17 +1309,17 @@ Function OnDownload
   StrCpy $DownloadServerIP "$5"
   ${If} $0 > 299
     ${NSD_KillTimer} OnDownload
     IntOp $DownloadRetryCount $DownloadRetryCount + 1
     ${If} "$DownloadReset" != "true"
       StrCpy $DownloadedBytes "0"
       ${NSD_AddStyle} $Progressbar ${PBS_MARQUEE}
       SendMessage $Progressbar ${PBM_SETMARQUEE} 1 \
-                  ${ProgressbarMarqueeIntervalMS} ; start=1|stop=0 interval(ms)=+N
+                  $ProgressbarMarqueeIntervalMS ; start=1|stop=0 interval(ms)=+N
     ${EndIf}
     InetBgDL::Get /RESET /END
     StrCpy $DownloadSizeBytes ""
     StrCpy $DownloadReset "true"
 
     ${If} $DownloadRetryCount >= ${DownloadMaxRetries}
       StrCpy $ExitCode "${ERR_DOWNLOAD_TOO_MANY_RETRIES}"
       ; Use a timer so the UI has a chance to update
@@ -1519,19 +1527,21 @@ Function OnDownload
       ; Delete the install.log and let the full installer create it. When the
       ; installer closes it we can detect that it has completed.
       Delete "$INSTDIR\install.log"
 
       ; Delete firefox.exe.moz-upgrade if it exists since it being present will
       ; require an OS restart for the full installer.
       Delete "$INSTDIR\${FileMainEXE}.moz-upgrade"
 
-      ; Flicker happens less often if a timer is used between updates of the
-      ; progress bar.
-      ${NSD_CreateTimer} StartInstall ${InstallIntervalMS}
+      System::Call "kernel32::GetTickCount()l .s"
+      Pop $EndPreInstallPhaseTickCount
+
+      Exec "$\"$PLUGINSDIR\download.exe$\" /INI=$PLUGINSDIR\${CONFIG_INI}"
+      ${NSD_CreateTimer} CheckInstall ${InstallIntervalMS}
     ${Else}
       ${If} $HalfOfDownload != "true"
       ${AndIf} $3 > $HalfOfDownload
         StrCpy $HalfOfDownload "true"
         LockWindow on
         ShowWindow $LabelBlurb1 ${SW_HIDE}
         ShowWindow $BitmapBlurb1 ${SW_HIDE}
         ShowWindow $LabelBlurb2 ${SW_SHOW}
@@ -1561,31 +1571,16 @@ Function OnPing
     ${EndIf}
     ; The following will exit the installer
     SetAutoClose true
     StrCpy $R9 "2"
     Call RelativeGotoPage
   ${EndIf}
 FunctionEnd
 
-Function StartInstall
-  ${NSD_KillTimer} StartInstall
-
-  System::Call "kernel32::GetTickCount()l .s"
-  Pop $EndPreInstallPhaseTickCount
-
-  IntOp $InstallCounterStep $InstallCounterStep + 1
-  LockWindow on
-  SendMessage $Progressbar ${PBM_STEPIT} 0 0
-  LockWindow off
-
-  Exec "$\"$PLUGINSDIR\download.exe$\" /INI=$PLUGINSDIR\${CONFIG_INI}"
-  ${NSD_CreateTimer} CheckInstall ${InstallIntervalMS}
-FunctionEnd
-
 Function CheckInstall
   IntOp $InstallCounterStep $InstallCounterStep + 1
   ${If} $InstallCounterStep >= $InstallTotalSteps
     ${NSD_KillTimer} CheckInstall
     ; Close the handle that prevents modification of the full installer
     System::Call 'kernel32::CloseHandle(i $HandleDownload)'
     StrCpy $ExitCode "${ERR_INSTALL_TIMEOUT}"
     ; Use a timer so the UI has a chance to update
@@ -1594,41 +1589,44 @@ Function CheckInstall
   ${EndIf}
 
   SendMessage $Progressbar ${PBM_STEPIT} 0 0
 
   ${If} ${FileExists} "$INSTDIR\install.log"
     Delete "$INSTDIR\install.tmp"
     CopyFiles /SILENT "$INSTDIR\install.log" "$INSTDIR\install.tmp"
 
+    ; The unfocus and refocus that happens approximately here is caused by the
+    ; installer calling SHChangeNotify to refresh the shortcut icons.
+
     ; When the full installer completes the installation the install.log will no
     ; longer be in use.
     ClearErrors
     Delete "$INSTDIR\install.log"
     ${Unless} ${Errors}
       ${NSD_KillTimer} CheckInstall
       ; Close the handle that prevents modification of the full installer
       System::Call 'kernel32::CloseHandle(i $HandleDownload)'
       Rename "$INSTDIR\install.tmp" "$INSTDIR\install.log"
       Delete "$PLUGINSDIR\download.exe"
       Delete "$PLUGINSDIR\${CONFIG_INI}"
       System::Call "kernel32::GetTickCount()l .s"
       Pop $EndInstallPhaseTickCount
-      System::Int64Op $InstallStepSize * 20
+      System::Int64Op $InstallStepSize * ${InstallProgressFinishStep}
       Pop $InstallStepSize
       SendMessage $Progressbar ${PBM_SETSTEP} $InstallStepSize 0
       ${NSD_CreateTimer} FinishInstall ${InstallIntervalMS}
     ${EndUnless}
   ${EndIf}
 FunctionEnd
 
 Function FinishInstall
   ; The full installer has completed but the progress bar still needs to finish
   ; so increase the size of the step.
-  IntOp $InstallCounterStep $InstallCounterStep + 40
+  IntOp $InstallCounterStep $InstallCounterStep + ${InstallProgressFinishStep}
   ${If} $InstallTotalSteps < $InstallCounterStep
     StrCpy $InstallCounterStep "$InstallTotalSteps"
   ${EndIf}
 
   ${If} $InstallTotalSteps != $InstallCounterStep
     SendMessage $Progressbar ${PBM_STEPIT} 0 0
     Return
   ${EndIf}
@@ -1826,20 +1824,19 @@ Function CheckSpace
       StrCpy $SpaceAvailableBytes "0"
       StrCpy $HasRequiredSpaceAvailable "false"
       Return
     ${EndIf}
   ${Loop}
 
   ${GetLongPath} "$ExistingTopDir" $ExistingTopDir
 
-  ; GetDiskFreeSpaceExW can require a backslash
+  ; GetDiskFreeSpaceExW requires a backslash.
   StrCpy $0 "$ExistingTopDir" "" -1 ; the last character
   ${If} "$0" != "\"
-    ; A backslash is required for 
     StrCpy $0 "\"
   ${Else}
     StrCpy $0 ""
   ${EndIf}
 
   System::Call 'kernel32::GetDiskFreeSpaceExW(w, *l, *l, *l) i("$ExistingTopDir$0", .r1, .r2, .r3) .'
   StrCpy $SpaceAvailableBytes "$1"
 
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -52,21 +52,21 @@
   -  -->
 <!ENTITY options.defaultColorUnit.name "Color Names">
 
 <!-- LOCALIZATION NOTE (options.context.triggersPageRefresh): This is the
   -  triggers page refresh footnote under the advanced settings group in the
   -  options panel and is used for settings that trigger page reload. -->
 <!ENTITY options.context.triggersPageRefresh  "* Current session only, reloads the page">
 
-<!-- LOCALIZATION NOTE (options.enableChrome.label3): This is the label for the
+<!-- LOCALIZATION NOTE (options.enableChrome.label4): This is the label for the
   -  checkbox that toggles chrome debugging, i.e. devtools.chrome.enabled
   -  boolean preference in about:config, in the options panel. -->
-<!ENTITY options.enableChrome.label3    "Enable chrome debugging">
-<!ENTITY options.enableChrome.tooltip   "Turning this option on will allow you to use various developer tools in browser context">
+<!ENTITY options.enableChrome.label4    "Enable chrome and addon debugging">
+<!ENTITY options.enableChrome.tooltip2  "Turning this option on will allow you to use various developer tools in browser context and debug addons from the Add-On Manager">
 
 <!-- LOCALIZATION NOTE (options.enableRemote.label3): This is the label for the
   -  checkbox that toggles remote debugging, i.e. devtools.debugger.remote-enabled
   -  boolean preference in about:config, in the options panel. -->
 <!ENTITY options.enableRemote.label3    "Enable remote debugging">
 <!ENTITY options.enableRemote.tooltip   "Turning this option on will allow the developer tools to debug remote Firefox instance like Firefox OS">
 
 <!-- LOCALIZATION NOTE (options.disableJavaScript.label,
--- a/browser/modules/UITour.jsm
+++ b/browser/modules/UITour.jsm
@@ -678,30 +678,30 @@ this.UITour = {
     let targetObject = this.targets.get(aTargetName);
     if (!targetObject) {
       deferred.reject("The specified target name is not in the allowed set");
       return deferred.promise;
     }
 
     let targetQuery = targetObject.query;
     aWindow.PanelUI.ensureReady().then(() => {
+      let node;
       if (typeof targetQuery == "function") {
-        deferred.resolve({
-          addTargetListener: targetObject.addTargetListener,
-          node: targetQuery(aWindow.document),
-          removeTargetListener: targetObject.removeTargetListener,
-          targetName: aTargetName,
-          widgetName: targetObject.widgetName,
-        });
-        return;
+        try {
+          node = targetQuery(aWindow.document);
+        } catch (ex) {
+          node = null;
+        }
+      } else {
+        node = aWindow.document.querySelector(targetQuery);
       }
 
       deferred.resolve({
         addTargetListener: targetObject.addTargetListener,
-        node: aWindow.document.querySelector(targetQuery),
+        node: node,
         removeTargetListener: targetObject.removeTargetListener,
         targetName: aTargetName,
         widgetName: targetObject.widgetName,
       });
     }).then(null, Cu.reportError);
     return deferred.promise;
   },
 
--- a/browser/modules/test/browser_UITour_availableTargets.js
+++ b/browser/modules/test/browser_UITour_availableTargets.js
@@ -59,16 +59,40 @@ let tests = [
       ok(UITour.availableTargetsCache.has(window),
          "Targets should now be cached again");
       CustomizableUI.reset();
       ok(!UITour.availableTargetsCache.has(window),
          "Targets should not be cached after reset");
       done();
     });
   },
+
+  function test_availableTargets_exceptionFromGetTarget(done) {
+    // The query function for the "search" target will throw if it's not found.
+    // Make sure the callback still fires with the other available targets.
+    CustomizableUI.removeWidgetFromArea("search-container");
+    gContentAPI.getConfiguration("availableTargets", (data) => {
+      // Default minus "search" and "searchProvider"
+      ok_targets(data, [
+        "accountStatus",
+        "addons",
+        "appMenu",
+        "backForward",
+        "bookmarks",
+        "customize",
+        "help",
+        "home",
+        "pinnedTab",
+        "quit",
+        "urlbar",
+      ]);
+      CustomizableUI.reset();
+      done();
+    });
+  },
 ];
 
 function ok_targets(actualData, expectedTargets) {
   // Depending on how soon after page load this is called, the selected tab icon
   // may or may not be showing the loading throbber.  Check for its presence and
   // insert it into expectedTargets if it's visible.
   let selectedTabIcon =
     document.getAnonymousElementByAttribute(gBrowser.selectedTab,
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -61,18 +61,22 @@
   position: relative;
   z-index: 1;
 }
 
 #nav-bar-overflow-button {
   -moz-image-region: rect(-5px, 12px, 11px, -4px);
 }
 
+/* This only has an effect when this element is placed on the bookmarks toolbar.
+ * It's 30px to make sure buttons with 18px icons fit along with the default 16px
+ * icons, without changing the size of the toolbar.
+ */
 #personal-bookmarks {
-  min-height: 29px;
+  min-height: 30px;
 }
 
 #browser-bottombox {
   /* opaque for layers optimization */
   background-color: -moz-Dialog;
 }
 
 #urlbar:-moz-lwtheme:not([focused="true"]),
@@ -90,17 +94,17 @@ toolbarbutton.bookmark-item:not(.subview
 toolbarbutton.bookmark-item:not(.subviewbutton):hover:active,
 toolbarbutton.bookmark-item[open="true"] {
   padding-top: 3px;
   padding-bottom: 1px;
   -moz-padding-start: 4px;
   -moz-padding-end: 2px;
 }
 
-.bookmark-item > .toolbarbutton-icon,
+.bookmark-item:not(#home-button) > .toolbarbutton-icon,
 #personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 /* Force the display of the label for bookmarks */
 .bookmark-item > .toolbarbutton-text,
 #personal-bookmarks[cui-areatype="toolbar"] > #bookmarks-toolbar-placeholder > .toolbarbutton-text {
@@ -129,23 +133,16 @@ toolbarpaletteitem[place="palette"] > #p
 
 @keyframes animation-bookmarkAdded {
   from { transform: rotate(0deg) translateX(-16px) rotate(0deg) scale(1); opacity: 0; }
   60%  { transform: rotate(180deg) translateX(-16px) rotate(-180deg) scale(2.2); opacity: 1; }
   80%  { opacity: 1; }
   to   { transform: rotate(180deg) translateX(-16px) rotate(-180deg) scale(1); opacity: 0; }
 }
 
-@keyframes animation-bookmarkAddedToBookmarksBar {
-  from { transform: rotate(0deg) translateX(-10px) rotate(0deg) scale(1); opacity: 0; }
-  60%  { transform: rotate(180deg) translateX(-10px) rotate(-180deg) scale(2.2); opacity: 1; }
-  80%  { opacity: 1; }
-  to   { transform: rotate(180deg) translateX(-10px) rotate(-180deg) scale(1); opacity: 0; }
-}
-
 @keyframes animation-bookmarkPulse {
   from { transform: scale(1); }
   50%  { transform: scale(1.3); }
   to   { transform: scale(1); }
 }
 
 #bookmarked-notification-container {
   min-height: 1px;
@@ -177,20 +174,16 @@ toolbarpaletteitem[place="palette"] > #p
 }
 
 #bookmarked-notification-anchor[notification="finish"] > #bookmarked-notification {
   background-image: url("chrome://browser/skin/places/bookmarks-notification-finish.png");
   animation: animation-bookmarkAdded 800ms;
   animation-timing-function: ease, ease, ease;
 }
 
-#bookmarked-notification-anchor[notification="finish"][in-bookmarks-toolbar=true] > #bookmarked-notification {
-  animation: animation-bookmarkAddedToBookmarksBar 800ms;
-}
-
 #bookmarks-menu-button[notification="finish"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   list-style-image: none !important;
 }
 
 #bookmarked-notification-dropmarker-anchor[notification="finish"] > #bookmarked-notification-dropmarker-icon {
   visibility: visible;
   animation: animation-bookmarkPulse 300ms;
   animation-delay: 600ms;
@@ -834,22 +827,16 @@ toolbarbutton[sdk-button="true"][cui-are
   list-style-image: url("moz-icon://stock/gtk-go-back-rtl?size=menu") !important;
 }
 .unified-nav-forward[_moz-menuactive] {
   list-style-image: url("moz-icon://stock/gtk-go-forward-ltr?size=menu") !important;
 }
 .unified-nav-forward[_moz-menuactive]:-moz-locale-dir(rtl) {
   list-style-image: url("moz-icon://stock/gtk-go-forward-rtl?size=menu") !important;
 }
-#home-button.bookmark-item {
-  list-style-image: url("moz-icon://stock/gtk-home?size=menu");
-}
-#home-button.bookmark-item[disabled="true"] {
-  list-style-image: url("moz-icon://stock/gtk-home?size=menu&state=disabled");
-}
 
 /* Menu panel buttons */
 
 %include ../shared/toolbarbuttons.inc.css
 %include ../shared/menupanel.inc.css
 
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-icon,
 #main-window:not([customizing]) .toolbarbutton-1[disabled=true] > .toolbarbutton-menu-dropmarker,
@@ -1627,40 +1614,24 @@ richlistitem[type~="action"][actiontype=
 #social-mark-button {
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
 /* bookmarks menu-button */
 
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker {
   -moz-appearance: none !important;
+  -moz-box-align: center;
 }
 
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   margin-top: 3px;
   margin-bottom: 3px;
 }
 
-#bookmarks-menu-button[cui-areatype="toolbar"].bookmark-item > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon,
-#bookmarks-menu-button.bookmark-item {
-  list-style-image: url("chrome://browser/skin/Toolbar-small.png");
-}
-
-#bookmarks-menu-button.bookmark-item {
-  -moz-image-region: rect(0px 144px 16px 128px);
-}
-
-#bookmarks-menu-button.bookmark-item[starred] {
-  -moz-image-region: rect(16px 144px 32px 128px);
-}
-
-#bookmarks-menu-button[cui-areatype="toolbar"].bookmark-item > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
-  -moz-image-region: rect(0px 160px 16px 144px);
-}
-
 #bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-icon,
 #bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-menu-dropmarker,
 #bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker,
 #bookmarks-menu-button[disabled][cui-areatype="toolbar"] > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-button[disabled] > .toolbarbutton-icon {
   opacity: .4;
 }
 
@@ -1715,19 +1686,19 @@ richlistitem[type~="action"][actiontype=
 
 .panel-promo-icon {
   list-style-image: url("chrome://browser/skin/sync-notification-24.png");
   -moz-margin-end: 10px;
   vertical-align: middle;
 }
 
 .panel-promo-closebutton {
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-  margin-top: 0;
-  margin-bottom: 0;
+  -moz-appearance: none;
+  height: 16px;
+  width: 16px;
 }
 
 .panel-promo-closebutton > .toolbarbutton-text {
   padding: 0;
   margin: 0;
 }
 
 /* Content area */
@@ -1812,35 +1783,56 @@ richlistitem[type~="action"][actiontype=
 
 /* In-tab close button */
 .tab-close-button > .toolbarbutton-icon {
   /* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must
      use evil CSS to give the impression of smaller content */
   margin: -4px;
 }
 
+/* Tabstrip close button */
+.tabs-closebutton,
 .tab-close-button {
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-  margin-top: 0;
-  margin-bottom: -1px;
-  -moz-margin-end: -4px;
+  -moz-appearance: none;
+  height: 16px;
+  width: 16px;
+}
+
+.tabs-closebutton:not([selected]):not(:hover),
+.tab-close-button:not([selected]):not(:hover) {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 64, 16, 48);
+}
+
+.tabs-closebutton:not([selected]):not(:hover):-moz-lwtheme-brighttext,
+.tab-close-button:not([selected]):not(:hover):-moz-lwtheme-brighttext {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 80, 16, 64);
+}
+
+.tabs-closebutton:not([selected]):not(:hover):-moz-lwtheme-darktext,
+.tab-close-button:not([selected]):not(:hover):-moz-lwtheme-darktext {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 96, 16, 80);
 }
 
 /* Tabstrip new tab button */
 .tabs-newtab-button,
 #TabsToolbar > #new-tab-button ,
 #TabsToolbar > #wrapper-new-tab-button > #new-tab-button {
   list-style-image: url("moz-icon://stock/gtk-add?size=menu");
   -moz-image-region: auto;
 }
 
-/* Tabstrip close button */
-.tabs-closebutton,
 .customization-tipPanel-closeBox > .close-icon {
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
+  -moz-appearance: none;
+  width: 16px;
+  height: 16px;
+}
+
+/* The :hover:active style from toolkit doesn't seem to work in this panel so just use :active. */
+.customization-tipPanel-closeBox > .close-icon:active {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
 }
 
 .tabs-closebutton > .toolbarbutton-icon {
   /* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must
      use evil CSS to give the impression of smaller content */
   margin: -2px;
 }
 
--- a/browser/themes/linux/newtab/newTab.css
+++ b/browser/themes/linux/newtab/newTab.css
@@ -44,24 +44,21 @@
   text-decoration: underline;
 }
 
 .newtab-undo-button:-moz-focusring {
   outline: 1px dotted;
 }
 
 #newtab-undo-close-button {
+  -moz-appearance: none;
   padding: 0;
   border: none;
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-  -moz-user-focus: normal;
-}
-
-#newtab-undo-close-button > .toolbarbutton-icon {
-  margin: -4px;
+  height: 16px;
+  width: 16px;
 }
 
 #newtab-undo-close-button > .toolbarbutton-text {
   display: none;
 }
 
 #newtab-undo-close-button:-moz-focusring {
   outline: 1px dotted;
--- a/browser/themes/linux/tabview/tabview.css
+++ b/browser/themes/linux/tabview/tabview.css
@@ -86,17 +86,27 @@ html[dir=rtl] .favicon {
 }
 
 .close {
   top: 6px;
   right: 6px;
   width: 16px;
   height: 16px;
   opacity: 0.2;
-  background: url("moz-icon://stock/gtk-close?size=menu") no-repeat;
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 16, 16, 0);
+  background-position: center center;
+  background-repeat: no-repeat;
+}
+
+.close:hover {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 32, 16, 16);
+}
+
+.close:hover:active {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
 }
 
 html[dir=rtl] .close {
   right: auto;
   left: 6px;
 }
 
 .close:hover,
--- a/browser/themes/osx/newtab/newTab.css
+++ b/browser/themes/osx/newtab/newTab.css
@@ -46,17 +46,16 @@
 
 .newtab-undo-button:-moz-focusring {
   outline: 1px dotted;
 }
 
 #newtab-undo-close-button {
   padding: 0;
   border: none;
-  -moz-user-focus: normal;
 }
 
 #newtab-undo-close-button > .toolbarbutton-text {
   display: none;
 }
 
 #newtab-undo-close-button:-moz-focusring {
   outline: 1px dotted;
--- a/browser/themes/shared/browser.inc
+++ b/browser/themes/shared/browser.inc
@@ -1,12 +1,13 @@
 %filter substitution
 
-%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #cut-button, #copy-button, #paste-button, #fullscreen-button, #zoom-out-button, #zoom-reset-button, #zoom-in-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button
+% Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none.
+%define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
+%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #webrtc-status-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@
 
 %ifdef XP_MACOSX
 % Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen
 % and want it to behave like other toolbar buttons.
 %define primaryToolbarButtons @primaryToolbarButtons@, #restore-button
 %endif
 
 %define inAnyPanel :-moz-any(:not([cui-areatype="toolbar"]), [overflowedItem=true])
-%define nestedButtons #zoom-out-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -1,8 +1,10 @@
+/* Note that this file isn't used for HiDPI on OS X. */
+
 :-moz-any(@primaryToolbarButtons@),
 #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   list-style-image: url("chrome://browser/skin/Toolbar.png");
 }
 
 :-moz-any(@primaryToolbarButtons@):not(@inAnyPanel@):-moz-lwtheme-brighttext,
 #bookmarks-menu-button:not(@inAnyPanel@):-moz-lwtheme-brighttext > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   list-style-image: url(chrome://browser/skin/Toolbar-inverted.png);
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -193,16 +193,22 @@
 
   #main-window[tabsintitlebar][sizemode="normal"] #titlebar-content:-moz-lwtheme {
     /* Render a window top border: */
     background-image: linear-gradient(to bottom,
           ThreeDLightShadow 0, ThreeDLightShadow 1px,
           ThreeDHighlight 1px, ThreeDHighlight 2px,
           ActiveBorder 2px, ActiveBorder 4px, transparent 4px);
   }
+
+  /* End classic titlebar gradient */
+
+  #main-window[tabsintitlebar]:not([inFullscreen]) :-moz-any(#TabsToolbar, #toolbar-menubar) toolbarbutton:not(:-moz-lwtheme) {
+    color: inherit;
+  }
 }
 
 /* Render a window top border for lwthemes on WinXP modern themes: */
 @media (-moz-windows-theme: luna-blue) {
   #main-window[tabsintitlebar][sizemode="normal"] #titlebar-content:-moz-lwtheme {
     background-image: linear-gradient(to bottom,
         rgb(8, 49, 216) 0, rgb(8, 49, 216) 1px,
         rgb(15, 77, 227) 1px, rgb(15, 77, 227) 2px,
--- a/browser/themes/windows/newtab/newTab.css
+++ b/browser/themes/windows/newtab/newTab.css
@@ -52,17 +52,16 @@
 .newtab-undo-button > .button-box {
   padding: 0;
 }
 
 #newtab-undo-close-button {
   -moz-appearance: none;
   padding: 0;
   border: none;
-  -moz-user-focus: normal;
 }
 
 #newtab-undo-close-button > .toolbarbutton-text {
   display: none;
 }
 
 #newtab-undo-close-button:-moz-focusring {
   outline: 1px dotted;
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -576,16 +576,17 @@ sync_java_files = [
     'fxa/login/FxAccountLoginTransition.java',
     'fxa/login/Married.java',
     'fxa/login/Separated.java',
     'fxa/login/State.java',
     'fxa/login/StateFactory.java',
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
+    'fxa/receivers/FxAccountUpgradeReceiver.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountNotificationManager.java',
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncService.java',
     'fxa/sync/SchedulePolicy.java',
     'sync/AlreadySyncingException.java',
     'sync/BackoffHandler.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/AbstractPerProfileDatabaseProvider.java
@@ -0,0 +1,79 @@
+/* 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.db;
+
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * The base class for ContentProviders that wish to use a different DB
+ * for each profile.
+ *
+ * This class has logic shared between ordinary per-profile CPs and
+ * those that wish to share DB connections between CPs.
+ */
+public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
+
+    /**
+     * Extend this to provide access to your own map of shared databases. This
+     * is a method so that your subclass doesn't collide with others!
+     */
+    protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
+
+    /*
+     * Fetches a readable database based on the profile indicated in the
+     * passed URI. If the URI does not contain a profile param, the default profile
+     * is used.
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a readable SQLiteDatabase
+     */
+    @Override
+    protected SQLiteDatabase getReadableDatabase(Uri uri) {
+        String profile = null;
+        if (uri != null) {
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        }
+
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
+    }
+
+    /*
+     * Fetches a writable database based on the profile indicated in the
+     * passed URI. If the URI does not contain a profile param, the default profile
+     * is used
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a writable SQLiteDatabase
+     */
+    @Override
+    protected SQLiteDatabase getWritableDatabase(Uri uri) {
+        String profile = null;
+        if (uri != null) {
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        }
+
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
+    }
+
+    protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
+    }
+
+    /**
+     * This method should ONLY be used for testing purposes.
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a writable SQLiteDatabase
+     */
+    @Override
+    @RobocopTarget
+    public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
+        return getWritableDatabase(uri);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/AbstractTransactionalProvider.java
@@ -0,0 +1,370 @@
+/* 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.db;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * This abstract class exists to capture some of the transaction-handling
+ * commonalities in Fennec's DB layer.
+ *
+ * In particular, this abstracts DB access, batching, and a particular
+ * transaction approach.
+ *
+ * That approach is: subclasses implement the abstract methods
+ * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
+ * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
+ * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
+ *
+ * These are all called expecting a transaction to be established, so failed
+ * modifications can be rolled-back, and work batched.
+ *
+ * If no transaction is established, that's not a problem. Transaction nesting
+ * can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
+ *
+ * The decision of when to begin a transaction is left to the subclasses,
+ * primarily to avoid the pattern of a transaction being begun, a read occurring,
+ * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
+ * which we don't handle well. Better to avoid starting a transaction too soon!
+ *
+ * You are probably interested in some subclasses:
+ *
+ * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
+ *   querying databases that are stored in the user's profile directory.
+ * * {@link PerProfileDatabaseProvider} is a simple version that only allows a
+ *   single ContentProvider to access each per-profile database.
+ * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
+ *   that allows for multiple providers to safely work with the same databases.
+ */
+@SuppressWarnings("javadoc")
+public abstract class AbstractTransactionalProvider extends ContentProvider {
+    private static final String LOGTAG = "GeckoTransProvider";
+
+    private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+
+    protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
+    protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
+
+    public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
+
+    protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
+    protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+    protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
+
+    /**
+     * Track whether we're in a batch operation.
+     *
+     * When we're in a batch operation, individual write steps won't even try
+     * to start a transaction... and neither will they attempt to finish one.
+     *
+     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
+     * a section of code in which {@link ContentProvider} methods will be
+     * called, but nested transactions should not be started. Callers are
+     * responsible for beginning and ending the enclosing transaction, and
+     * for setting this to <code>Boolean.FALSE</code> when done.
+     *
+     * This is a ThreadLocal separate from `db.inTransaction` because batched
+     * operations start transactions independent of individual ContentProvider
+     * operations. This doesn't work well with the entire concept of this
+     * abstract class -- that is, automatically beginning and ending transactions
+     * for each insert/delete/update operation -- and doing so without
+     * causing arbitrary nesting requires external tracking.
+     *
+     * Note that beginWrite takes a DB argument, but we don't differentiate
+     * between databases in this tracking flag. If your ContentProvider manages
+     * multiple database transactions within the same thread, you'll need to
+     * amend this scheme -- but then, you're already doing some serious wizardry,
+     * so rock on.
+     */
+    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
+
+    /**
+     * Return true if OS version and database parallelism support indicates
+     * that this provider should bundle writes into transactions.
+     */
+    @SuppressWarnings("static-method")
+    protected boolean shouldUseTransactions() {
+        return Build.VERSION.SDK_INT >= 11;
+    }
+
+    protected static String computeSQLInClause(int items, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        int i = 0;
+        for (; i < items - 1; ++i) {
+            builder.append("?, ");
+        }
+        if (i < items) {
+            builder.append("?");
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    private boolean isInBatch() {
+        final Boolean isInBatch = isInBatchOperation.get();
+        if (isInBatch == null) {
+            return false;
+        }
+        return isInBatch.booleanValue();
+    }
+
+    /**
+     * If we're not currently in a transaction, and we should be, start one.
+     */
+    protected void beginWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not bothering with an intermediate write transaction: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && !db.inTransaction()) {
+            trace("beginWrite: beginning transaction.");
+            db.beginTransaction();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction, mark it as
+     * successful.
+     */
+    protected void markWriteSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not marking write successful: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("Marking write transaction successful.");
+            db.setTransactionSuccessful();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction,
+     * end it.
+     *
+     * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
+     */
+    protected void endWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not ending write: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("endWrite: ending transaction.");
+            db.endTransaction();
+        }
+    }
+
+    protected void beginBatch(final SQLiteDatabase db) {
+        trace("Beginning batch.");
+        isInBatchOperation.set(Boolean.TRUE);
+        db.beginTransaction();
+    }
+
+    protected void markBatchSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Marking batch successful.");
+            db.setTransactionSuccessful();
+            return;
+        }
+        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
+        throw new IllegalStateException("Not in batch.");
+    }
+
+    protected void endBatch(final SQLiteDatabase db) {
+        trace("Ending batch.");
+        db.endTransaction();
+        isInBatchOperation.set(Boolean.FALSE);
+    }
+
+    /**
+     * Turn a single-column cursor of longs into a single SQL "IN" clause.
+     * We can do this without using selection arguments because Long isn't
+     * vulnerable to injection.
+     */
+    protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        final int commaLimit = cursor.getCount() - 1;
+        int i = 0;
+        while (cursor.moveToNext()) {
+            builder.append(cursor.getLong(0));
+            if (i++ < commaLimit) {
+                builder.append(", ");
+            }
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int deleted = 0;
+
+        try {
+            deleted = deleteInTransaction(uri, selection, selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
+        }
+
+        if (deleted > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return deleted;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        trace("Calling insert on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        Uri result = null;
+        try {
+            result = insertInTransaction(uri, values);
+            markWriteSuccessful(db);
+        } catch (SQLException sqle) {
+            Log.e(LOGTAG, "exception in DB operation", sqle);
+        } catch (UnsupportedOperationException uoe) {
+            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
+        } finally {
+            endWrite(db);
+        }
+
+        if (result != null) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return result;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int updated = 0;
+
+        try {
+            updated = updateInTransaction(uri, values, selection,
+                                          selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
+        }
+
+        if (updated > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return updated;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        if (values == null) {
+            return 0;
+        }
+
+        int numValues = values.length;
+        int successes = 0;
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        debug("bulkInsert: explicitly starting transaction.");
+        beginBatch(db);
+
+        try {
+            for (int i = 0; i < numValues; i++) {
+                insertInTransaction(uri, values[i]);
+                successes++;
+            }
+            trace("Flushing DB bulkinsert...");
+            markBatchSuccessful(db);
+        } finally {
+            debug("bulkInsert: explicitly ending transaction.");
+            endBatch(db);
+        }
+
+        if (successes > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return successes;
+    }
+
+    /**
+     * Indicates whether a query should include deleted fields
+     * based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean shouldShowDeleted(Uri uri) {
+        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
+        return !TextUtils.isEmpty(showDeleted);
+    }
+
+    /**
+     * Indicates whether an insertion should be made if a record doesn't
+     * exist, based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean shouldUpdateOrInsert(Uri uri) {
+        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
+        return Boolean.parseBoolean(insertIfNeeded);
+    }
+
+    /**
+     * Indicates whether query is a test based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean isTest(Uri uri) {
+        if (uri == null) {
+            return false;
+        }
+        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
+        return !TextUtils.isEmpty(isTest);
+    }
+
+    /**
+     * Return true of the query is from Firefox Sync.
+     * @param uri query URI
+     */
+    protected static boolean isCallerSync(Uri uri) {
+        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
+        return !TextUtils.isEmpty(isSync);
+    }
+
+    protected static void trace(String message) {
+        if (logVerbose) {
+            Log.v(LOGTAG, message);
+        }
+    }
+
+    protected static void debug(String message) {
+        if (logDebug) {
+            Log.d(LOGTAG, message);
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -14,38 +14,36 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.sync.Utils;
 
 import android.app.SearchManager;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.OperationApplicationException;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.MatrixCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 
-public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
+public class BrowserProvider extends SharedBrowserDatabaseProvider {
     private static final String LOGTAG = "GeckoBrowserProvider";
 
     // How many records to reposition in a single query.
     // This should be less than the SQLite maximum number of query variables
     // (currently 999) divided by the number of variables used per positioning
     // query (currently 3).
     static final int MAX_POSITION_UPDATES_PER_QUERY = 100;
 
@@ -810,31 +808,16 @@ public class BrowserProvider extends Tra
         Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
                 null, sortOrder, limit);
         cursor.setNotificationUri(getContext().getContentResolver(),
                 BrowserContract.AUTHORITY_URI);
 
         return cursor;
     }
 
-    private static int getUrlCount(SQLiteDatabase db, String table, String url) {
-        final Cursor c = db.query(table, new String[] { "COUNT(*)" },
-                                  URLColumns.URL + " = ?", new String[] { url },
-                                  null, null, null);
-        try {
-            if (c.moveToFirst()) {
-                return c.getInt(0);
-            }
-        } finally {
-            c.close();
-        }
-
-        return 0;
-    }
-
     /**
      * Update the positions of bookmarks in batches.
      *
      * Begins and ends its own transactions.
      *
      * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
      */
     int updateBookmarkPositions(Uri uri, String[] guids) {
@@ -1300,17 +1283,17 @@ public class BrowserProvider extends Tra
         // Doing this UPDATE (or the DELETE above) first ensures that the
         // first operation within a new enclosing transaction is a write.
         // The cleanup call below will do a SELECT first, and thus would
         // require the transaction to be upgraded from a reader to a writer.
         // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
         // it if we can.
         final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
         try {
-            cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
+            cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
         } catch (Exception e) {
             // We don't care.
             Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
         }
         return updated;
     }
 
     int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
@@ -1329,17 +1312,17 @@ public class BrowserProvider extends Tra
         values.put(Bookmarks.IS_DELETED, 1);
 
         // Doing this UPDATE (or the DELETE above) first ensures that the
         // first operation within this transaction is a write.
         // The cleanup call below will do a SELECT first, and thus would
         // require the transaction to be upgraded from a reader to a writer.
         final int updated = updateBookmarks(uri, values, selection, selectionArgs);
         try {
-            cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
+            cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
         } catch (Exception e) {
             // We don't care.
             Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
         }
         return updated;
     }
 
     int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
@@ -1456,20 +1439,9 @@ public class BrowserProvider extends Tra
         endBatch(db);
 
         if (failures) {
             throw new OperationApplicationException();
         }
 
         return results;
     }
-
-    @Override
-    protected BrowserDatabaseHelper createDatabaseHelper(
-            Context context, String databasePath) {
-         return new BrowserDatabaseHelper(context, databasePath);
-    }
-
-    @Override
-    protected String getDatabaseName() {
-        return BrowserDatabaseHelper.DATABASE_NAME;
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/PerProfileDatabaseProvider.java
@@ -0,0 +1,50 @@
+/* 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.db;
+
+import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * Abstract class containing methods needed to make a SQLite-based content
+ * provider with a database helper of type T, where one database helper is
+ * held per profile.
+ */
+public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
+    private PerProfileDatabases<T> databases;
+
+    @Override
+    protected PerProfileDatabases<T> getDatabases() {
+        return databases;
+    }
+
+    protected abstract String getDatabaseName();
+
+    /**
+     * Creates and returns an instance of the appropriate DB helper.
+     *
+     * @param  context       to use to create the database helper
+     * @param  databasePath  path to the DB file
+     * @return               instance of the database helper
+     */
+    protected abstract T createDatabaseHelper(Context context, String databasePath);
+
+    @Override
+    public boolean onCreate() {
+        synchronized (this) {
+            databases = new PerProfileDatabases<T>(
+                getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
+                    @Override
+                    public T makeDatabaseHelper(Context context, String databasePath) {
+                        return createDatabaseHelper(context, databasePath);
+                    }
+                });
+        }
+
+        return true;
+    }
+}
--- a/mobile/android/base/db/ReadingListProvider.java
+++ b/mobile/android/base/db/ReadingListProvider.java
@@ -4,27 +4,24 @@
 
 package org.mozilla.gecko.db;
 
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.sync.Utils;
 
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 
-public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
-    private static final String LOGTAG = "GeckoReadingListProv";
-
+public class ReadingListProvider extends SharedBrowserDatabaseProvider {
     static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
 
     static final int ITEMS = 101;
     static final int ITEMS_ID = 102;
     static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
 
     static {
         URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS);
@@ -98,17 +95,17 @@ public class ReadingListProvider extends
         if (isCallerSync(uri)) {
             return db.delete(TABLE_READING_LIST, selection, selectionArgs);
         }
 
         debug("Marking item entry as deleted for URI: " + uri);
         ContentValues values = new ContentValues();
         values.put(ReadingListItems.IS_DELETED, 1);
 
-        cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
+        cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
         return updateItems(uri, values, selection, selectionArgs);
     }
 
     @Override
     @SuppressWarnings("fallthrough")
     public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         trace("Calling update in transaction on URI: " + uri);
 
@@ -242,20 +239,9 @@ public class ReadingListProvider extends
             case ITEMS_ID:
                 trace("URI is ITEMS_ID: " + uri);
                 return ReadingListItems.CONTENT_ITEM_TYPE;
         }
 
         debug("URI has unrecognized type: " + uri);
         return null;
     }
-
-    @Override
-    protected BrowserDatabaseHelper createDatabaseHelper(Context context,
-            String databasePath) {
-        return new BrowserDatabaseHelper(context, databasePath);
-    }
-
-    @Override
-    protected String getDatabaseName() {
-        return BrowserDatabaseHelper.DATABASE_NAME;
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SharedBrowserDatabaseProvider.java
@@ -0,0 +1,115 @@
+/* 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.db;
+
+import org.mozilla.gecko.db.BrowserContract.CommonColumns;
+import org.mozilla.gecko.db.BrowserContract.SyncColumns;
+import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * A ContentProvider subclass that provides per-profile browser.db access
+ * that can be safely shared between multiple providers.
+ *
+ * If multiple ContentProvider classes wish to share a database, it's
+ * vitally important that they use the same SQLiteOpenHelpers for access.
+ *
+ * Failure to do so can cause accidental concurrent writes, with the result
+ * being unexpected SQLITE_BUSY errors.
+ *
+ * This class provides a static {@link PerProfileDatabases} instance, lazily
+ * initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
+ */
+public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
+    private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
+
+    private static PerProfileDatabases<BrowserDatabaseHelper> databases;
+
+    @Override
+    protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
+        return databases;
+    }
+
+    @Override
+    public boolean onCreate() {
+        // If necessary, do the shared DB work.
+        synchronized (SharedBrowserDatabaseProvider.class) {
+            if (databases != null) {
+                return true;
+            }
+
+            final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
+                @Override
+                public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
+                    return new BrowserDatabaseHelper(context, databasePath);
+                }
+            };
+
+            databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
+        }
+
+        return true;
+    }
+
+    /**
+     * Clean up some deleted records from the specified table.
+     *
+     * If called in an existing transaction, it is the caller's responsibility
+     * to ensure that the transaction is already upgraded to a writer, because
+     * this method issues a read followed by a write, and thus is potentially
+     * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
+     *
+     * If not called in an existing transaction, no new explicit transaction
+     * will be begun.
+     */
+    protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
+        Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
+
+        // We clean up records marked as deleted that are older than a
+        // predefined max age. It's important not be too greedy here and
+        // remove only a few old deleted records at a time.
+
+        // we cleanup records marked as deleted that are older than a
+        // predefined max age. It's important not be too greedy here and
+        // remove only a few old deleted records at a time.
+
+        // Maximum age of deleted records to be cleaned up (20 days in ms)
+        final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
+
+        // Number of records marked as deleted to be removed
+        final long DELETED_RECORDS_PURGE_LIMIT = 5;
+
+        // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
+        // IDs of matching rows, then delete them in one go.
+        final long now = System.currentTimeMillis();
+        final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
+                SyncColumns.DATE_MODIFIED + " <= " +
+                (now - MAX_AGE_OF_DELETED_RECORDS);
+
+        final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
+        final String[] ids;
+        final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
+        final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
+        try {
+            ids = new String[cursor.getCount()];
+            int i = 0;
+            while (cursor.moveToNext()) {
+                ids[i++] = Long.toString(cursor.getLong(0), 10);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        final String inClause = computeSQLInClause(ids.length,
+                CommonColumns._ID);
+        db.delete(tableName, inClause, ids);
+    }
+}
--- a/mobile/android/base/db/TabsProvider.java
+++ b/mobile/android/base/db/TabsProvider.java
@@ -1,45 +1,33 @@
 /* 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.db;
 
-import java.io.File;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserContract.Clients;
 import org.mozilla.gecko.db.BrowserContract.Tabs;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
 
-import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
-import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
-import android.os.Build;
 import android.text.TextUtils;
-import android.util.Log;
 
-public class TabsProvider extends ContentProvider {
-    private static final String LOGTAG = "GeckoTabsProvider";
-    private Context mContext;
-
-    private PerProfileDatabases<TabsDatabaseHelper> mDatabases;
-
+public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDatabaseHelper> {
     static final String DATABASE_NAME = "tabs.db";
 
     static final int DATABASE_VERSION = 2;
 
     static final String TABLE_TABS = "tabs";
     static final String TABLE_CLIENTS = "clients";
 
     static final int TABS = 600;
@@ -82,183 +70,104 @@ public class TabsProvider extends Conten
 
         map = new HashMap<String, String>();
         map.put(Clients.GUID, Clients.GUID);
         map.put(Clients.NAME, Clients.NAME);
         map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
         CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
     }
 
-    static final String selectColumn(String table, String column) {
+    private static final String selectColumn(String table, String column) {
         return table + "." + column + " = ?";
     }
 
-    // Calculate these once, at initialization. isLoggable is too expensive to
-    // have in-line in each log call.
-    private static boolean logDebug   = Log.isLoggable(LOGTAG, Log.DEBUG);
-    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-    protected static void trace(String message) {
-        if (logVerbose) {
-            Log.v(LOGTAG, message);
-        }
-    }
-
-    protected static void debug(String message) {
-        if (logDebug) {
-            Log.d(LOGTAG, message);
-        }
-    }
-
-    /**
-     * Return true of the query is from Firefox Sync.
-     * @param uri query URI
-     */
-    public static boolean isCallerSync(Uri uri) {
-        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
-        return !TextUtils.isEmpty(isSync);
-    }
-
     final class TabsDatabaseHelper extends SQLiteOpenHelper {
         public TabsDatabaseHelper(Context context, String databasePath) {
             super(context, databasePath, null, DATABASE_VERSION);
         }
 
         @Override
         public void onCreate(SQLiteDatabase db) {
             debug("Creating tabs.db: " + db.getPath());
             debug("Creating " + TABLE_TABS + " table");
 
             // Table for each tab on any client.
             db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
-                    Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
-                    Tabs.CLIENT_GUID + " TEXT," +
-                    Tabs.TITLE + " TEXT," +
-                    Tabs.URL + " TEXT," +
-                    Tabs.HISTORY + " TEXT," +
-                    Tabs.FAVICON + " TEXT," +
-                    Tabs.LAST_USED + " INTEGER," +
-                    Tabs.POSITION + " INTEGER" +
-                    ");");
+                       Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                       Tabs.CLIENT_GUID + " TEXT," +
+                       Tabs.TITLE + " TEXT," +
+                       Tabs.URL + " TEXT," +
+                       Tabs.HISTORY + " TEXT," +
+                       Tabs.FAVICON + " TEXT," +
+                       Tabs.LAST_USED + " INTEGER," +
+                       Tabs.POSITION + " INTEGER" +
+                       ");");
 
             // Indices on CLIENT_GUID and POSITION.
-            db.execSQL("CREATE INDEX " + INDEX_TABS_GUID + " ON " + TABLE_TABS + "("
-                    + Tabs.CLIENT_GUID + ")");
-
-            db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION + " ON " + TABLE_TABS + "("
-                    + Tabs.POSITION + ")");
+            db.execSQL("CREATE INDEX " + INDEX_TABS_GUID +
+                       " ON " + TABLE_TABS + "(" + Tabs.CLIENT_GUID + ")");
+            db.execSQL("CREATE INDEX " + INDEX_TABS_POSITION +
+                       " ON " + TABLE_TABS + "(" + Tabs.POSITION + ")");
 
             debug("Creating " + TABLE_CLIENTS + " table");
 
             // Table for client's name-guid mapping.
             db.execSQL("CREATE TABLE " + TABLE_CLIENTS + "(" +
-                    Clients.GUID + " TEXT PRIMARY KEY," +
-                    Clients.NAME + " TEXT," +
-                    Clients.LAST_MODIFIED + " INTEGER" +
-                    ");");
+                       Clients.GUID + " TEXT PRIMARY KEY," +
+                       Clients.NAME + " TEXT," +
+                       Clients.LAST_MODIFIED + " INTEGER" +
+                       ");");
 
             // Index on GUID.
-            db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID + " ON " + TABLE_CLIENTS + "("
-                    + Clients.GUID + ")");
+            db.execSQL("CREATE INDEX " + INDEX_CLIENTS_GUID +
+                       " ON " + TABLE_CLIENTS + "(" + Clients.GUID + ")");
 
             createLocalClient(db);
         }
 
         // Insert a client row for our local Fennec client.
         private void createLocalClient(SQLiteDatabase db) {
             debug("Inserting local Fennec client into " + TABLE_CLIENTS + " table");
 
             ContentValues values = new ContentValues();
             values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
             db.insertOrThrow(TABLE_CLIENTS, null, values);
         }
 
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             debug("Upgrading tabs.db: " + db.getPath() + " from " +
-                    oldVersion + " to " + newVersion);
+                  oldVersion + " to " + newVersion);
 
             // We have to do incremental upgrades until we reach the current
             // database schema version.
             for (int v = oldVersion + 1; v <= newVersion; v++) {
                 switch(v) {
                     case 2:
                         createLocalClient(db);
                         break;
                  }
              }
         }
 
         @Override
         public void onOpen(SQLiteDatabase db) {
             debug("Opening tabs.db: " + db.getPath());
+            db.rawQuery("PRAGMA synchronous=OFF", null).close();
 
-            Cursor cursor = null;
-            try {
-                cursor = db.rawQuery("PRAGMA synchronous=OFF", null);
-            } finally {
-                if (cursor != null)
-                    cursor.close();
-            }
-
-            // From Honeycomb on, it's possible to run several db
-            // commands in parallel using multiple connections.
-            if (Build.VERSION.SDK_INT >= 11) {
+            if (shouldUseTransactions()) {
                 db.enableWriteAheadLogging();
                 db.setLockingEnabled(false);
-            } else {
-                // Pre-Honeycomb, we can do some lesser optimizations.
-                cursor = null;
-                try {
-                    cursor = db.rawQuery("PRAGMA journal_mode=PERSIST", null);
-                } finally {
-                    if (cursor != null)
-                        cursor.close();
-                }
+                return;
             }
-        }
-    }
-
-    private SQLiteDatabase getReadableDatabase(Uri uri) {
-        trace("Getting readable database for URI: " + uri);
-
-        String profile = null;
-
-        if (uri != null)
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-
-        return mDatabases.getDatabaseHelperForProfile(profile).getReadableDatabase();
-    }
 
-    private SQLiteDatabase getWritableDatabase(Uri uri) {
-        trace("Getting writable database for URI: " + uri);
-
-        String profile = null;
-
-        if (uri != null)
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-
-        return mDatabases.getDatabaseHelperForProfile(profile).getWritableDatabase();
-    }
-
-    @Override
-    public boolean onCreate() {
-        debug("Creating TabsProvider");
-
-        synchronized (this) {
-            mContext = getContext();
-            mDatabases = new PerProfileDatabases<TabsDatabaseHelper>(
-                getContext(), DATABASE_NAME, new DatabaseHelperFactory<TabsDatabaseHelper>() {
-                    @Override
-                    public TabsDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
-                        return new TabsDatabaseHelper(context, databasePath);
-                    }
-                });
+            // If we're not using transactions (in particular, prior to
+            // Honeycomb), then we can do some lesser optimizations.
+            db.rawQuery("PRAGMA journal_mode=PERSIST", null).close();
         }
-
-        return true;
     }
 
     @Override
     public String getType(Uri uri) {
         final int match = URI_MATCHER.match(uri);
 
         trace("Getting URI type: " + uri);
 
@@ -280,45 +189,16 @@ public class TabsProvider extends Conten
                 return Clients.CONTENT_ITEM_TYPE;
         }
 
         debug("URI has unrecognized type: " + uri);
 
         return null;
     }
 
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        trace("Calling delete on URI: " + uri);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int deleted = 0;
-
-        if (Build.VERSION.SDK_INT >= 11) {
-            trace("Beginning delete transaction: " + uri);
-            db.beginTransaction();
-            try {
-                deleted = deleteInTransaction(uri, selection, selectionArgs);
-                db.setTransactionSuccessful();
-                trace("Successful delete transaction: " + uri);
-            } finally {
-                db.endTransaction();
-            }
-        } else {
-            deleted = deleteInTransaction(uri, selection, selectionArgs);
-        }
-
-        if (deleted > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return deleted;
-    }
-
     @SuppressWarnings("fallthrough")
     public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
         trace("Calling delete in transaction on URI: " + uri);
 
         final int match = URI_MATCHER.match(uri);
         int deleted = 0;
 
         switch (match) {
@@ -350,45 +230,16 @@ public class TabsProvider extends Conten
                 throw new UnsupportedOperationException("Unknown delete URI " + uri);
         }
 
         debug("Deleted " + deleted + " rows for URI: " + uri);
 
         return deleted;
     }
 
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        trace("Calling insert on URI: " + uri);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        Uri result = null;
-
-        if (Build.VERSION.SDK_INT >= 11) {
-            trace("Beginning insert transaction: " + uri);
-            db.beginTransaction();
-            try {
-                result = insertInTransaction(uri, values);
-                db.setTransactionSuccessful();
-                trace("Successful insert transaction: " + uri);
-            } finally {
-                db.endTransaction();
-            }
-        } else {
-            result = insertInTransaction(uri, values);
-        }
-
-        if (result != null) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return result;
-    }
-
     public Uri insertInTransaction(Uri uri, ContentValues values) {
         trace("Calling insert in transaction on URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         int match = URI_MATCHER.match(uri);
         long id = -1;
 
         switch (match) {
@@ -411,48 +262,17 @@ public class TabsProvider extends Conten
         debug("Inserted ID in database: " + id);
 
         if (id >= 0)
             return ContentUris.withAppendedId(uri, id);
 
         return null;
     }
 
-    @Override
-    public int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        trace("Calling update on URI: " + uri);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int updated = 0;
-
-        if (Build.VERSION.SDK_INT >= 11) {
-            trace("Beginning update transaction: " + uri);
-            db.beginTransaction();
-            try {
-                updated = updateInTransaction(uri, values, selection, selectionArgs);
-                db.setTransactionSuccessful();
-                trace("Successful update transaction: " + uri);
-            } finally {
-                db.endTransaction();
-            }
-        } else {
-            updated = updateInTransaction(uri, values, selection, selectionArgs);
-        }
-
-        if (updated > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return updated;
-    }
-
-    public int updateInTransaction(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
+    public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         trace("Calling update in transaction on URI: " + uri);
 
         int match = URI_MATCHER.match(uri);
         int updated = 0;
 
         switch (match) {
             case CLIENTS_ID:
                 trace("Update on CLIENTS_ID: " + uri);
@@ -532,71 +352,40 @@ public class TabsProvider extends Conten
                 qb.setTables(TABLE_CLIENTS);
                 break;
 
             default:
                 throw new UnsupportedOperationException("Unknown query URI " + uri);
         }
 
         trace("Running built query.");
-        Cursor cursor = qb.query(db, projection, selection, selectionArgs, null,
-                null, sortOrder, limit);
-        cursor.setNotificationUri(getContext().getContentResolver(),
-                BrowserContract.TABS_AUTHORITY_URI);
+        final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
+        cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
 
         return cursor;
     }
 
     int updateValues(Uri uri, ContentValues values, String selection, String[] selectionArgs, String table) {
         trace("Updating tabs on URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
-
+        beginWrite(db);
         return db.update(table, values, selection, selectionArgs);
     }
 
     int deleteValues(Uri uri, String selection, String[] selectionArgs, String table) {
         debug("Deleting tabs for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
-
+        beginWrite(db);
         return db.delete(table, selection, selectionArgs);
     }
 
     @Override
-    public int bulkInsert(Uri uri, ContentValues[] values) {
-        if (values == null)
-            return 0;
-
-        int numValues = values.length;
-        int successes = 0;
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
-        db.beginTransaction();
-        try {
-            for (int i = 0; i < numValues; i++) {
-                try {
-                    insertInTransaction(uri, values[i]);
-                    successes++;
-                } catch (SQLException e) {
-                    Log.e(LOGTAG, "SQLException in bulkInsert", e);
+    protected TabsDatabaseHelper createDatabaseHelper(Context context, String databasePath) {
+        return new TabsDatabaseHelper(context, databasePath);
+    }
 
-                    // Restart the transaction to continue insertions.
-                    db.setTransactionSuccessful();
-                    db.endTransaction();
-                    db.beginTransaction();
-                }
-            }
-            trace("Flushing DB bulkinsert...");
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-
-        if (successes > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return successes;
+    @Override
+    protected String getDatabaseName() {
+        return DATABASE_NAME;
     }
 }
deleted file mode 100644
--- a/mobile/android/base/db/TransactionalProvider.java
+++ /dev/null
@@ -1,517 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.db;
-
-import org.mozilla.gecko.db.BrowserContract.CommonColumns;
-import org.mozilla.gecko.db.BrowserContract.SyncColumns;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
-import org.mozilla.gecko.mozglue.RobocopTarget;
-
-import android.content.ContentProvider;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-/*
- * Abstract class containing methods needed to make a SQLite-based content provider with a
- * database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
- * updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
- */
-public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
-    private static final String LOGTAG = "GeckoTransProvider";
-    protected Context mContext;
-    protected PerProfileDatabases<T> mDatabases;
-
-    /*
-     * Returns the name of the database file. Used to get a path
-     * to the DB file.
-     *
-     * @return name of the database file
-     */
-    abstract protected String getDatabaseName();
-
-    /*
-     * Creates and returns an instance of a DB helper. Given a
-     * context and a path to the DB file
-     *
-     * @param  context       to use to create the database helper
-     * @param  databasePath  path to the DB  file
-     * @return               instance of the database helper
-     */
-    abstract protected T createDatabaseHelper(Context context, String databasePath);
-
-    /*
-     * Inserts an item into the database within a DB transaction.
-     *
-     * @param uri    query URI
-     * @param values column values to be inserted
-     * @return       a URI for the newly inserted item
-     */
-    abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
-
-    /*
-     * Deletes items from the database within a DB transaction.
-     *
-     * @param uri            Query URI.
-     * @param selection      An optional filter to match rows to delete.
-     * @param selectionArgs  An array of arguments to substitute into the selection.
-     *
-     * @return number of rows impacted by the deletion.
-     */
-    abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
-
-    /*
-     * Updates the database within a DB transaction.
-     *
-     * @param uri            Query URI.
-     * @param values         A set of column_name/value pairs to add to the database.
-     * @param selection      An optional filter to match rows to update.
-     * @param selectionArgs  An array of arguments to substitute into the selection.
-     *
-     * @return               number of rows impacted by the update.
-     */
-    abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
-
-    /*
-     * Fetches a readable database based on the profile indicated in the
-     * passed URI. If the URI does not contain a profile param, the default profile
-     * is used.
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a readable SQLiteDatabase
-     */
-    protected SQLiteDatabase getReadableDatabase(Uri uri) {
-        String profile = null;
-        if (uri != null) {
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        }
-
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
-    }
-
-    /*
-     * Fetches a writeable database based on the profile indicated in the
-     * passed URI. If the URI does not contain a profile param, the default profile
-     * is used
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a writeable SQLiteDatabase
-     */
-    protected SQLiteDatabase getWritableDatabase(Uri uri) {
-        String profile = null;
-        if (uri != null) {
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        }
-
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
-    }
-
-    /**
-     * Public version of {@link #getWritableDatabase(Uri) getWritableDatabase}.
-     * This method should ONLY be used for testing purposes.
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a writeable SQLiteDatabase
-     */
-    @RobocopTarget
-    public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
-        return getWritableDatabase(uri);
-    }
-
-    /**
-     * Return true of the query is from Firefox Sync.
-     * @param uri query URI
-     */
-    public static boolean isCallerSync(Uri uri) {
-        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
-        return !TextUtils.isEmpty(isSync);
-    }
-
-    /**
-     * Indicates whether a query should include deleted fields
-     * based on the URI.
-     * @param uri query URI
-     */
-    public static boolean shouldShowDeleted(Uri uri) {
-        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
-        return !TextUtils.isEmpty(showDeleted);
-    }
-
-    /**
-     * Indicates whether an insertion should be made if a record doesn't
-     * exist, based on the URI.
-     * @param uri query URI
-     */
-    public static boolean shouldUpdateOrInsert(Uri uri) {
-        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
-        return Boolean.parseBoolean(insertIfNeeded);
-    }
-
-    /**
-     * Indicates whether query is a test based on the URI.
-     * @param uri query URI
-     */
-    public static boolean isTest(Uri uri) {
-        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
-        return !TextUtils.isEmpty(isTest);
-    }
-
-    protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
-    }
-
-    @Override
-    public boolean onCreate() {
-        synchronized (this) {
-            mContext = getContext();
-            mDatabases = new PerProfileDatabases<T>(
-                getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
-                    @Override
-                    public T makeDatabaseHelper(Context context, String databasePath) {
-                        return createDatabaseHelper(context, databasePath);
-                    }
-                });
-        }
-
-        return true;
-    }
-
-    /**
-     * Return true if OS version and database parallelism support indicates
-     * that this provider should bundle writes into transactions.
-     */
-    @SuppressWarnings("static-method")
-    protected boolean shouldUseTransactions() {
-        return Build.VERSION.SDK_INT >= 11;
-    }
-
-    /**
-     * Track whether we're in a batch operation.
-     *
-     * When we're in a batch operation, individual write steps won't even try
-     * to start a transaction... and neither will they attempt to finish one.
-     *
-     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
-     * a section of code in which {@link ContentProvider} methods will be
-     * called, but nested transactions should not be started. Callers are
-     * responsible for beginning and ending the enclosing transaction, and
-     * for setting this to <code>Boolean.FALSE</code> when done.
-     *
-     * This is a ThreadLocal separate from `db.inTransaction` because batched
-     * operations start transactions independent of individual ContentProvider
-     * operations. This doesn't work well with the entire concept of this
-     * abstract class -- that is, automatically beginning and ending transactions
-     * for each insert/delete/update operation -- and doing so without
-     * causing arbitrary nesting requires external tracking.
-     *
-     * Note that beginWrite takes a DB argument, but we don't differentiate
-     * between databases in this tracking flag. If your ContentProvider manages
-     * multiple database transactions within the same thread, you'll need to
-     * amend this scheme -- but then, you're already doing some serious wizardry,
-     * so rock on.
-     */
-    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
-
-    private boolean isInBatch() {
-        final Boolean isInBatch = isInBatchOperation.get();
-        if (isInBatch == null) {
-            return false;
-        }
-        return isInBatch.booleanValue();
-    }
-
-    /**
-     * If we're not currently in a transaction, and we should be, start one.
-     */
-    protected void beginWrite(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not bothering with an intermediate write transaction: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && !db.inTransaction()) {
-            trace("beginWrite: beginning transaction.");
-            db.beginTransaction();
-        }
-    }
-
-    /**
-     * If we're not in a batch, but we are in a write transaction, mark it as
-     * successful.
-     */
-    protected void markWriteSuccessful(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not marking write successful: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && db.inTransaction()) {
-            trace("Marking write transaction successful.");
-            db.setTransactionSuccessful();
-        }
-    }
-
-    /**
-     * If we're not in a batch, but we are in a write transaction,
-     * end it.
-     *
-     * @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
-     */
-    protected void endWrite(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not ending write: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && db.inTransaction()) {
-            trace("endWrite: ending transaction.");
-            db.endTransaction();
-        }
-    }
-
-    protected void beginBatch(final SQLiteDatabase db) {
-        trace("Beginning batch.");
-        isInBatchOperation.set(Boolean.TRUE);
-        db.beginTransaction();
-    }
-
-    protected void markBatchSuccessful(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Marking batch successful.");
-            db.setTransactionSuccessful();
-            return;
-        }
-        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
-        throw new IllegalStateException("Not in batch.");
-    }
-
-    protected void endBatch(final SQLiteDatabase db) {
-        trace("Ending batch.");
-        db.endTransaction();
-        isInBatchOperation.set(Boolean.FALSE);
-    }
-
-    /*
-     * This utility is replicated from RepoUtils, which is managed by android-sync.
-     */
-    protected static String computeSQLInClause(int items, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        int i = 0;
-        for (; i < items - 1; ++i) {
-            builder.append("?, ");
-        }
-        if (i < items) {
-            builder.append("?");
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
-    /**
-     * Turn a single-column cursor of longs into a single SQL "IN" clause.
-     * We can do this without using selection arguments because Long isn't
-     * vulnerable to injection.
-     */
-    protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        final int commaLimit = cursor.getCount() - 1;
-        int i = 0;
-        while (cursor.moveToNext()) {
-            builder.append(cursor.getLong(0));
-            if (i++ < commaLimit) {
-                builder.append(", ");
-            }
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int deleted = 0;
-
-        try {
-            deleted = deleteInTransaction(uri, selection, selectionArgs);
-            markWriteSuccessful(db);
-        } finally {
-            endWrite(db);
-        }
-
-        if (deleted > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return deleted;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        trace("Calling insert on URI: " + uri);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        Uri result = null;
-        try {
-            result = insertInTransaction(uri, values);
-            markWriteSuccessful(db);
-        } catch (SQLException sqle) {
-            Log.e(LOGTAG, "exception in DB operation", sqle);
-        } catch (UnsupportedOperationException uoe) {
-            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
-        } finally {
-            endWrite(db);
-        }
-
-        if (result != null) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return result;
-    }
-
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int updated = 0;
-
-        try {
-            updated = updateInTransaction(uri, values, selection,
-                                          selectionArgs);
-            markWriteSuccessful(db);
-        } finally {
-            endWrite(db);
-        }
-
-        if (updated > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return updated;
-    }
-
-    @Override
-    public int bulkInsert(Uri uri, ContentValues[] values) {
-        if (values == null) {
-            return 0;
-        }
-
-        int numValues = values.length;
-        int successes = 0;
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
-        debug("bulkInsert: explicitly starting transaction.");
-        beginBatch(db);
-
-        try {
-            for (int i = 0; i < numValues; i++) {
-                insertInTransaction(uri, values[i]);
-                successes++;
-            }
-            trace("Flushing DB bulkinsert...");
-            markBatchSuccessful(db);
-        } finally {
-            debug("bulkInsert: explicitly ending transaction.");
-            endBatch(db);
-        }
-
-        if (successes > 0) {
-            final boolean shouldSyncToNetwork = !isCallerSync(uri);
-            mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
-        }
-
-        return successes;
-    }
-
-    /**
-     * Clean up some deleted records from the specified table.
-     *
-     * If called in an existing transaction, it is the caller's responsibility
-     * to ensure that the transaction is already upgraded to a writer, because
-     * this method issues a read followed by a write, and thus is potentially
-     * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
-     *
-     * If not called in an existing transaction, no new explicit transaction
-     * will be begun.
-     */
-    protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
-        Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
-
-        // We clean up records marked as deleted that are older than a
-        // predefined max age. It's important not be too greedy here and
-        // remove only a few old deleted records at a time.
-
-        // we cleanup records marked as deleted that are older than a
-        // predefined max age. It's important not be too greedy here and
-        // remove only a few old deleted records at a time.
-
-        // Maximum age of deleted records to be cleaned up (20 days in ms)
-        final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
-
-        // Number of records marked as deleted to be removed
-        final long DELETED_RECORDS_PURGE_LIMIT = 5;
-
-        // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
-        // IDs of matching rows, then delete them in one go.
-        final long now = System.currentTimeMillis();
-        final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
-                                 SyncColumns.DATE_MODIFIED + " <= " +
-                                 (now - MAX_AGE_OF_DELETED_RECORDS);
-
-        final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
-        final String[] ids;
-        final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
-        final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
-        try {
-            ids = new String[cursor.getCount()];
-            int i = 0;
-            while (cursor.moveToNext()) {
-                ids[i++] = Long.toString(cursor.getLong(0), 10);
-            }
-        } finally {
-            cursor.close();
-        }
-
-        final String inClause = computeSQLInClause(ids.length,
-                                                   CommonColumns._ID);
-        db.delete(tableName, inClause, ids);
-    }
-
-    // Calculate these once, at initialization. isLoggable is too expensive to
-    // have in-line in each log call.
-    private static boolean logDebug  = Log.isLoggable(LOGTAG, Log.DEBUG);
-    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-    protected static void trace(String message) {
-        if (logVerbose) {
-            Log.v(LOGTAG, message);
-        }
-    }
-
-    protected static void debug(String message) {
-        if (logDebug) {
-            Log.d(LOGTAG, message);
-        }
-    }
-}
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
@@ -110,17 +110,24 @@ public class FxAccountUpdateCredentialsA
 
     @Override
     public void handleError(Exception e) {
       showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
 
     @Override
     public void handleFailure(FxAccountClientRemoteException e) {
-      // TODO On isUpgradeRequired, transition to Doghouse state.
+      if (e.isUpgradeRequired()) {
+        Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
+        final State state = fxAccount.getState();
+        fxAccount.setState(state.makeDoghouseState());
+        // The status activity will say that the user needs to upgrade.
+        redirectToActivity(FxAccountStatusActivity.class);
+        return;
+      }
       showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
 
     @Override
     public void handleSuccess(LoginResponse result) {
       Logger.info(LOG_TAG, "Got success signing in.");
 
       if (fxAccount == null) {
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -393,43 +393,43 @@ public class AndroidFxAccount {
     boolean isSyncEnabled = true;
     for (String authority : new String[] { BrowserContract.AUTHORITY }) {
       isSyncEnabled &= ContentResolver.getSyncAutomatically(account, authority);
     }
     return isSyncEnabled;
   }
 
   public void enableSyncing() {
-    Logger.info(LOG_TAG, "Enabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
+    Logger.info(LOG_TAG, "Enabling sync for account named like " + getObfuscatedEmail());
     for (String authority : new String[] { BrowserContract.AUTHORITY }) {
       ContentResolver.setSyncAutomatically(account, authority, true);
       ContentResolver.setIsSyncable(account, authority, 1);
     }
   }
 
   public void disableSyncing() {
-    Logger.info(LOG_TAG, "Disabling sync for account named like " + Utils.obfuscateEmail(getEmail()));
+    Logger.info(LOG_TAG, "Disabling sync for account named like " + getObfuscatedEmail());
     for (String authority : new String[] { BrowserContract.AUTHORITY }) {
       ContentResolver.setSyncAutomatically(account, authority, false);
     }
   }
 
   public void requestSync(Bundle extras) {
-    Logger.info(LOG_TAG, "Requesting sync for account named like " + Utils.obfuscateEmail(getEmail()) +
+    Logger.info(LOG_TAG, "Requesting sync for account named like " + getObfuscatedEmail() +
         (extras.isEmpty() ? "." : "; has extras."));
     for (String authority : new String[] { BrowserContract.AUTHORITY }) {
       ContentResolver.requestSync(account, authority, extras);
     }
   }
 
   public synchronized void setState(State state) {
     if (state == null) {
       throw new IllegalArgumentException("state must not be null");
     }
-    Logger.info(LOG_TAG, "Moving account named like " + Utils.obfuscateEmail(getEmail()) +
+    Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
         " to state " + state.getStateLabel().toString());
     updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
     updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
   }
 
   public synchronized State getState() {
     String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
     String stateString = getBundleData(BUNDLE_KEY_STATE);
@@ -471,16 +471,27 @@ public class AndroidFxAccount {
    *
    * @return local email address.
    */
   public String getEmail() {
     return account.name;
   }
 
   /**
+   * Return the Firefox Account's local email address, obfuscated.
+   * <p>
+   * Use this when logging.
+   *
+   * @return local email address, obfuscated.
+   */
+  public String getObfuscatedEmail() {
+    return Utils.obfuscateEmail(account.name);
+  }
+
+  /**
    * Create an intent announcing that a Firefox account will be deleted.
    *
    * @param context
    *          Android context.
    * @param account
    *          Android account being removed.
    * @return <code>Intent</code> to broadcast.
    */
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/receivers/FxAccountUpgradeReceiver.java
@@ -0,0 +1,132 @@
+/* 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.fxa.receivers;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.Utils;
+
+import android.accounts.Account;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * A receiver that takes action when our Android package is upgraded (replaced).
+ */
+public class FxAccountUpgradeReceiver extends BroadcastReceiver {
+  private static final String LOG_TAG = FxAccountUpgradeReceiver.class.getSimpleName();
+
+  /**
+   * Produce a list of Runnable instances to be executed sequentially on
+   * upgrade.
+   * <p>
+   * Each Runnable will be executed sequentially on a background thread. Any
+   * unchecked Exception thrown will be caught and ignored.
+   *
+   * @param context Android context.
+   * @return list of Runnable instances.
+   */
+  protected List<Runnable> onUpgradeRunnables(Context context) {
+    List<Runnable> runnables = new LinkedList<Runnable>();
+    runnables.add(new MaybeUnpickleRunnable(context));
+    // Recovering accounts that are in the Doghouse should happen *after* we
+    // unpickle any accounts saved to disk.
+    runnables.add(new AdvanceFromDoghouseRunnable(context));
+    return runnables;
+  }
+
+  @Override
+  public void onReceive(final Context context, Intent intent) {
+    Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+    Logger.info(LOG_TAG, "Upgrade broadcast received.");
+
+    // Iterate Runnable instances one at a time.
+    final Executor executor = Executors.newSingleThreadExecutor();
+    for (final Runnable runnable : onUpgradeRunnables(context)) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            runnable.run();
+          } catch (Exception e) {
+            // We really don't want to throw on a background thread, so we
+            // catch, log, and move on.
+            Logger.error(LOG_TAG, "Got exception executing background upgrade Runnable; ignoring.", e);
+          }
+        }
+      });
+    }
+  }
+
+  /**
+   * A Runnable that tries to unpickle any pickled Firefox Accounts.
+   */
+  protected static class MaybeUnpickleRunnable implements Runnable {
+    protected final Context context;
+
+    public MaybeUnpickleRunnable(Context context) {
+      this.context = context;
+    }
+
+    @Override
+    public void run() {
+      // Querying the accounts will unpickle any pickled Firefox Account.
+      Logger.info(LOG_TAG, "Trying to unpickle any pickled Firefox Account.");
+      FirefoxAccounts.getFirefoxAccounts(context);
+    }
+  }
+
+  /**
+   * A Runnable that tries to advance existing Firefox Accounts that are in the
+   * Doghouse state to the Separated state.
+   * <p>
+   * This is our main deprecation-and-upgrade mechanism: in some way, the
+   * Account gets moved to the Doghouse state. If possible, an upgraded version
+   * of the package advances to Separated, prompting the user to re-connect the
+   * Account.
+   */
+  protected static class AdvanceFromDoghouseRunnable implements Runnable {
+    protected final Context context;
+
+    public AdvanceFromDoghouseRunnable(Context context) {
+      this.context = context;
+    }
+
+    @Override
+    public void run() {
+      final Account[] accounts = FirefoxAccounts.getFirefoxAccounts(context);
+      Logger.info(LOG_TAG, "Trying to advance " + accounts.length + " existing Firefox Accounts from the Doghouse to Separated (if necessary).");
+      for (Account account : accounts) {
+        try {
+          final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+          // For great debugging.
+          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+            fxAccount.dump();
+          }
+          State state = fxAccount.getState();
+          if (state == null || state.getStateLabel() != StateLabel.Doghouse) {
+            Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is not in the Doghouse; skipping.");
+            continue;
+          }
+          Logger.debug(LOG_TAG, "Account named like " + Utils.obfuscateEmail(account.name) + " is in the Doghouse; advancing to Separated.");
+          fxAccount.setState(state.makeSeparatedState());
+        } catch (Exception e) {
+          Logger.warn(LOG_TAG, "Got exception trying to advance account named like " + Utils.obfuscateEmail(account.name) +
+              " from Doghouse to Separated state; ignoring.", e);
+        }
+      }
+    }
+  }
+}
--- a/mobile/android/base/home/FramePanelLayout.java
+++ b/mobile/android/base/home/FramePanelLayout.java
@@ -33,15 +33,15 @@ class FramePanelLayout extends PanelLayo
         addView(mChildView);
     }
 
     @Override
     public void load() {
         Log.d(LOGTAG, "Loading");
 
         if (mChildView instanceof DatasetBacked) {
-            // TODO: get filter from ViewEntry
-            DatasetRequest request = new DatasetRequest(mChildConfig.getDatasetId(), null);
+            final FilterDetail filter = new FilterDetail(mChildConfig.getFilter(), null);
+            final DatasetRequest request = new DatasetRequest(mChildConfig.getDatasetId(), filter);
             Log.d(LOGTAG, "Requesting child request: " + request);
             requestDataset(request);
         }
     }
 }
--- a/mobile/android/base/home/HomeConfig.java
+++ b/mobile/android/base/home/HomeConfig.java
@@ -603,61 +603,67 @@ public final class HomeConfig {
     }
 
     public static class ViewConfig implements Parcelable {
         private final ViewType mType;
         private final String mDatasetId;
         private final ItemType mItemType;
         private final ItemHandler mItemHandler;
         private final String mBackImageUrl;
+        private final String mFilter;
 
         private static final String JSON_KEY_TYPE = "type";
         private static final String JSON_KEY_DATASET = "dataset";
         private static final String JSON_KEY_ITEM_TYPE = "itemType";
         private static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
         private static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
+        private static final String JSON_KEY_FILTER = "filter";
 
         public ViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
             mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
             mDatasetId = json.getString(JSON_KEY_DATASET);
             mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE));
             mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
             mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null);
+            mFilter = json.optString(JSON_KEY_FILTER, null);
 
             validate();
         }
 
         @SuppressWarnings("unchecked")
         public ViewConfig(Parcel in) {
             mType = (ViewType) in.readParcelable(getClass().getClassLoader());
             mDatasetId = in.readString();
             mItemType = (ItemType) in.readParcelable(getClass().getClassLoader());
             mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
             mBackImageUrl = in.readString();
+            mFilter = in.readString();
 
             validate();
         }
 
         public ViewConfig(ViewConfig viewConfig) {
             mType = viewConfig.mType;
             mDatasetId = viewConfig.mDatasetId;
             mItemType = viewConfig.mItemType;
             mItemHandler = viewConfig.mItemHandler;
             mBackImageUrl = viewConfig.mBackImageUrl;
+            mFilter = viewConfig.mFilter;
 
             validate();
         }
 
         public ViewConfig(ViewType type, String datasetId, ItemType itemType,
-                          ItemHandler itemHandler, String backImageUrl) {
+                          ItemHandler itemHandler, String backImageUrl, String filter) {
             mType = type;
             mDatasetId = datasetId;
             mItemType = itemType;
             mItemHandler = itemHandler;
             mBackImageUrl = backImageUrl;
+            mFilter = filter;
 
             validate();
         }
 
         private void validate() {
             if (mType == null) {
                 throw new IllegalArgumentException("Can't create ViewConfig with null type");
             }
@@ -690,43 +696,52 @@ public final class HomeConfig {
         public ItemHandler getItemHandler() {
             return mItemHandler;
         }
 
         public String getBackImageUrl() {
             return mBackImageUrl;
         }
 
+        public String getFilter() {
+            return mFilter;
+        }
+
         public JSONObject toJSON() throws JSONException {
             final JSONObject json = new JSONObject();
 
             json.put(JSON_KEY_TYPE, mType.toString());
             json.put(JSON_KEY_DATASET, mDatasetId);
             json.put(JSON_KEY_ITEM_TYPE, mItemType.toString());
             json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString());
 
             if (!TextUtils.isEmpty(mBackImageUrl)) {
                 json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl);
             }
 
+            if (!TextUtils.isEmpty(mFilter)) {
+                json.put(JSON_KEY_FILTER, mFilter);
+            }
+
             return json;
         }
 
         @Override
         public int describeContents() {
             return 0;
         }
 
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeParcelable(mType, 0);
             dest.writeString(mDatasetId);
             dest.writeParcelable(mItemType, 0);
             dest.writeParcelable(mItemHandler, 0);
             dest.writeString(mBackImageUrl);
+            dest.writeString(mFilter);
         }
 
         public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {
             @Override
             public ViewConfig createFromParcel(final Parcel in) {
                 return new ViewConfig(in);
             }
 
--- a/mobile/android/base/home/PanelLayout.java
+++ b/mobile/android/base/home/PanelLayout.java
@@ -396,19 +396,19 @@ abstract class PanelLayout extends Frame
 
         /**
          * Adds a filter to the history stack for this view.
          */
         public void pushFilter(FilterDetail filter) {
             if (mFilterStack == null) {
                 mFilterStack = new LinkedList<FilterDetail>();
 
-                // Initialize with a null filter.
-                // TODO: use initial filter from ViewConfig
-                mFilterStack.push(new FilterDetail(null, mPanelConfig.getTitle()));
+                // Initialize with the initial filter.
+                mFilterStack.push(new FilterDetail(mViewConfig.getFilter(),
+                                                   mPanelConfig.getTitle()));
             }
 
             mFilterStack.push(filter);
         }
 
         /**
          * Remove the most recent filter from the stack.
          *
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -112,30 +112,33 @@ gbjar.sources += [
     'ANRReporter.java',
     'AppNotificationClient.java',
     'BaseGeckoInterface.java',
     'BrowserApp.java',
     'ContactService.java',
     'ContextGetter.java',
     'CustomEditText.java',
     'DataReportingNotification.java',
+    'db/AbstractPerProfileDatabaseProvider.java',
+    'db/AbstractTransactionalProvider.java',
     'db/BrowserContract.java',
     'db/BrowserDatabaseHelper.java',
     'db/BrowserDB.java',
     'db/BrowserProvider.java',
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
+    'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/TabsProvider.java',
-    'db/TransactionalProvider.java',
     'Distribution.java',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'EventDispatcher.java',
     'favicons/cache/FaviconCache.java',
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',
index 42566ff2e3322f62ecb36bb3b6b8a4f094abc11e..263b40582d4312d76d6000673c6c52edaf5cb06d
GIT binary patch
literal 538
zc$@(m0_FXQP)<h;3K|Lk000e1NJLTq001cf001Be1^@s6k8e>v00006VoOIv0RI60
z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-UAyGD-yr)?708{0jEhs
zK~z}7?Uy@F!$1&*zlog}NGa0MQ6VnFC1_|k4o!M)z!7LU0761SG*p-n6cBmrP-Gki
zV<*8)jN+9>W3TP?=h>Nm*QhFhI;&JOzUqNxIUD?gRzvPmt8=o3tXzvXy)il44q$&!
z6v@HO!u)ypCN^Uc&;;5*7uYlF04=*NMFV`-`sG34n_jM;)`5rvN}*C8Imp<oVN3E9
z_%QnfW`3#e574+oRI(u6Qh=uOskO}7E^>2V3?#ra@CpR$|Cy6|VP`{&mI{l_hj|rx
z#}T93IS#Is^YV^a*UTSxGySneE&(3x<EOwR7jn~`Q12*SLTdmGizhV*1W>g;yQV>x
zo*;{D*IaE3>sLhs52&63L*T8d5_Gt#c&Y+<@1|LcbW%1MX>>OeS0p|s+b$>baxpJY
zqfAaFR^jtpNICy4L6Y%Kf`a)}E+#g3wz7+=JN>B0p7+)LUI_V@ZIfA8BTZSYj589o
zN+-igNEu(D`zNLLd<F1DLf>$c6r47OyquIxDYm$FYyJCB|31`^B_Xwr<gP%9sv;tl
copM!`FX%0%5)EYFYXATM07*qoM6N<$f?T=bL;wH)
index 370898c4a4b6460acca43de7c87a5b11bf83d18c..5f877646602a10c690faa79c2bb582bb1345e0e1
GIT binary patch
literal 479
zc$@*>0U-W~P)<h;3K|Lk000e1NJLTq000~S000&U1^@s6)0X`500006VoOIv0RI60
z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-UAyGEiT5yH@E-*0c=S`
zK~zY`#g#!$!!Qs<-^3}UNNiYe15ScC3uj`*K@cZl$EGVHP(=}KGoMAr#feftpax4{
zW5vmQf6pHWNdO8QbOhJoiAv6jk;mwSV-M8;&Gk5JXE#v);d?7k6|h+MKpRo;J>gK&
zRK$!)<f>y$z#4c9yaSt(a42bsoNQ9WjHAFbVhUBSKm$AhFQKxdFcTdOBsvbv0_A+x
zdtQg426zl1Yhbk_JiVyzkeOOcPKBu4Ezwd*B)Jk!l2gmIi?}68iB{Xqib{?lwGwlr
zTi`jcC>36I4i+#gs<HFIK#Aj0RKBk)CgL18DUR7n!h66FhPDbN65df*%Sm$OxK%AB
z{8M4%Xn{GfjH7HjE?qoqt_p*cHJlbT9ID+=T~n`yO*pmfo5mw%3$NnEzC-u9G;c7)
z_oYjheZu^E@;!Z6^^kk9?zPQVeds@F{Cykw^-VMS<^3=JLyb*1>M%)|*(fR{*#}##
Vtl+xoi~Ilp002ovPDHLkV1gIH#WVl_
index 93546b15fb9bc0cd40c8ccd80ddf1270f24ca226..f0e482f092ea47529a479c6ecee929d2583418ea
GIT binary patch
literal 663
zc$@*30%-k-P)<h;3K|Lk000e1NJLTq001@s001fo1^@s6_)(}m00006VoOIv0RI60
z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-UAyGFc|g{&OHDC0whU9
zK~!ko?U=D{6G0G$zgeG20R^GUJ3vQ4!z1t<X=r&9n)JMZUjQi$1!Z0U8b?CJ3Boa8
zd_Tp_CF{%HIkB;&vv#D>?%u9W^Uu!C{JTg3P(ynyj;a4ZQrH4}Dj8Uoe8zGpW?&gu
z29|;CPKvGm{p531R35(pz^0?6@g`!^5gfm4pZ_0*CoAMS#+RzJX;uJ&rNKc8psdTo
zlhg68BF7>Pt`@cCw~!K8MD3lM$+MFRLb0)zSs4wMFf=qk1Yppi!NJUdU%<@UxqsIt
zXUq~_WQFmAgwaLIc<BAOt@|~w8E^%B2PVKz;5VSd*GD!v9cu}g-uvisIrI)+RM9#)
zQ*CGwX=rGXFs^A}s$c7)NDmt0frs7?U<`b9=PiK04m!DGoww+x&#gL+@`ZK(rmeoo
z9~LB_jOI#i=gLMap)y`44=y$Sr<?VNPe|{8sqf-^U+UN^;EC^M$jqY$^3fB=pVaw^
z&sb-nzC4rprL>tm;;dJ+WVPXnj8`X)VN3EDM#<v}I0wD}-+*gR9&1x7MRjE1p0XAV
z52#N?hf|6~y1#aC7p^n6Ck@Qry7eE9mL@Cap%|Rnw*DS$Vv_=0Gmp80i3_w+nSGli
zIDS=L+|vOmS`||&`M+vfRW5JBuGsy<9k89OXw%UxWA3?hX2Xl){sF4leSQh<ndFz?
x3@ii7z}5$*yki+y2DUw}C<!y$t8*sF{sBn{*Wl3*XVw4!002ovPDHLkV1mQkDuw_6
--- a/mobile/android/base/sync/setup/activities/SendTabActivity.java
+++ b/mobile/android/base/sync/setup/activities/SendTabActivity.java
@@ -9,17 +9,19 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
+import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State.Action;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandRunner;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConstants;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
@@ -53,47 +55,48 @@ public class SendTabActivity extends Act
     String getAccountGUID();
 
     /**
      * Sync this account, specifying only clients as the engine to sync.
      */
     void syncClientsStage();
   }
 
-  public class FxAccountTabSender implements TabSender {
-    private final AndroidFxAccount account;
+  private static class FxAccountTabSender implements TabSender {
+    private final AndroidFxAccount fxAccount;
 
-    public FxAccountTabSender(Context context, Account account) {
-      this.account = new AndroidFxAccount(context, account);
+    public FxAccountTabSender(Context context, AndroidFxAccount fxAccount) {
+      this.fxAccount = fxAccount;
     }
 
     @Override
     public String getAccountGUID() {
       try {
-        final SharedPreferences prefs = this.account.getSyncPrefs();
+        final SharedPreferences prefs = this.fxAccount.getSyncPrefs();
         return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
       } catch (Exception e) {
         Logger.warn(LOG_TAG, "Could not get Firefox Account parameters or preferences; aborting.");
         return null;
       }
     }
 
     @Override
     public void syncClientsStage() {
       final Bundle extras = new Bundle();
       Utils.putStageNamesToSync(extras, CLIENTS_STAGE, null);
       extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
-      this.account.requestSync(extras);
+      this.fxAccount.requestSync(extras);
     }
   }
 
   private static class Sync11TabSender implements TabSender {
     private final Account account;
     private final AccountManager accountManager;
     private final Context context;
+
     private Sync11TabSender(Context context, Account syncAccount, AccountManager accountManager) {
       this.context = context;
       this.account = syncAccount;
       this.accountManager = accountManager;
     }
 
     @Override
     public String getAccountGUID() {
@@ -219,37 +222,44 @@ public class SendTabActivity extends Act
     /*
      * First, decide if we are able to send anything.
      */
     final Context applicationContext = getApplicationContext();
     final AccountManager accountManager = AccountManager.get(applicationContext);
 
     final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
     if (fxAccounts.length > 0) {
-      this.tabSender = new FxAccountTabSender(applicationContext, fxAccounts[0]);
+      final AndroidFxAccount fxAccount = new AndroidFxAccount(applicationContext, fxAccounts[0]);
+      if (fxAccount.getState().getNeededAction() != Action.None) {
+        // We have a Firefox Account, but it's definitely not able to send a tab
+        // right now. Redirect to the status activity.
+        Logger.warn(LOG_TAG, "Firefox Account named like " + fxAccount.getObfuscatedEmail() +
+            " needs action before it can send a tab; redirecting to status activity.");
+        redirectToNewTask(FxAccountStatusActivity.class, false);
+        return;
+      }
+
+      this.tabSender = new FxAccountTabSender(applicationContext, fxAccount);
 
       Logger.info(LOG_TAG, "Allowing tab send for Firefox Account.");
       registerDisplayURICommand();
       return;
     }
 
     final Account[] syncAccounts = accountManager.getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC);
     if (syncAccounts.length > 0) {
       this.tabSender = new Sync11TabSender(applicationContext, syncAccounts[0], accountManager);
 
       Logger.info(LOG_TAG, "Allowing tab send for Sync account.");
       registerDisplayURICommand();
       return;
     }
 
     // Offer to set up a Firefox Account, and finish this activity.
-    final Intent intent = new Intent(applicationContext, FxAccountGetStartedActivity.class);
-    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-    startActivity(intent);
-    finish();
+    redirectToNewTask(FxAccountGetStartedActivity.class, false);
   }
 
   private static void registerDisplayURICommand() {
     final CommandProcessor processor = CommandProcessor.getProcessor();
     processor.registerCommand("displayURI", new CommandRunner(3) {
       @Override
       public void executeCommand(final GlobalSession session, List<String> args) {
         CommandProcessor.displayURI(args, session.getContext());
@@ -374,9 +384,20 @@ public class SendTabActivity extends Act
     for (Entry<String, ClientRecord> entry : all.entrySet()) {
       if (ourGUID.equals(entry.getKey())) {
         continue;
       }
       out.add(entry.getValue());
     }
     return out;
   }
+
+  // Adapted from FxAccountAbstractActivity.
+  protected void redirectToNewTask(Class<? extends Activity> activityClass, boolean success) {
+    Intent intent = new Intent(this, activityClass);
+    // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+    // the soft keyboard not being shown for the started activity. Why, Android, why?
+    intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    startActivity(intent);
+    notifyAndFinish(success);
+  }
 }
--- a/mobile/android/base/sync/setup/activities/WebViewActivity.java
+++ b/mobile/android/base/sync/setup/activities/WebViewActivity.java
@@ -28,16 +28,17 @@ public class WebViewActivity extends Syn
     super.onCreate(savedInstanceState);
     getWindow().requestFeature(Window.FEATURE_PROGRESS);
     setContentView(R.layout.sync_setup_webview);
     // Extract URI to launch from Intent.
     Uri uri = this.getIntent().getData();
     if (uri == null) {
       Logger.debug(LOG_TAG, "No URI passed to display.");
       finish();
+      return;
     }
 
     WebView wv = (WebView) findViewById(R.id.web_engine);
     // Add a progress bar.
     final Activity activity = this;
     wv.setWebChromeClient(new WebChromeClient() {
       public void onProgressChanged(WebView view, int progress) {
         // Activities and WebViews measure progress with different scales.
--- a/mobile/android/base/tests/AboutHomeTest.java
+++ b/mobile/android/base/tests/AboutHomeTest.java
@@ -1,29 +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/. */
+
 package org.mozilla.gecko.tests;
 
-import com.jayway.android.robotium.solo.Condition;
-import org.mozilla.gecko.*;
+import java.util.ArrayList;
 
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
+import org.mozilla.gecko.Actions;
+
 import android.support.v4.view.ViewPager;
 import android.text.TextUtils;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.GridView;
-import android.widget.LinearLayout;
-import android.widget.TabWidget;
 import android.widget.ListAdapter;
 import android.widget.ListView;
+import android.widget.TabWidget;
 import android.widget.TextView;
 
-import java.util.ArrayList;
-import java.util.Arrays;
+import com.jayway.android.robotium.solo.Condition;
 
 /**
  * This class is an extension of BaseTest that helps with interaction with about:home
  * This class contains methods that access the different tabs from about:home, methods that get information like history and bookmarks from the database, edit and remove bookmarks and history items
  * The purpose of this class is to collect all the logically connected methods that deal with about:home
  * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest
  */
 abstract class AboutHomeTest extends PixelTest {
@@ -31,17 +30,17 @@ abstract class AboutHomeTest extends Pix
     private ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{
                   add("TOP_SITES");
                   add("BOOKMARKS");
                   add("READING_LIST");
               }};
 
 
     @Override
-    protected void setUp() throws Exception {
+    public void setUp() throws Exception {
         super.setUp();
 
         if (aboutHomeTabs.size() < 4) {
             // Update it for tablets vs. phones.
             if (mDevice.type.equals("phone")) {
                 aboutHomeTabs.add(0, AboutHomeTabs.HISTORY.toString());
             } else {
                 aboutHomeTabs.add(AboutHomeTabs.HISTORY.toString());
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/BaseRobocopTest.java
@@ -0,0 +1,57 @@
+/* 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.tests;
+
+import java.util.Map;
+
+import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.FennecInstrumentationTestRunner;
+import org.mozilla.gecko.FennecMochitestAssert;
+import org.mozilla.gecko.FennecNativeDriver;
+import org.mozilla.gecko.FennecTalosAssert;
+
+import android.app.Activity;
+import android.test.ActivityInstrumentationTestCase2;
+
+public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
+    public static final int TEST_MOCHITEST = 0;
+    public static final int TEST_TALOS = 1;
+
+    protected static final String TARGET_PACKAGE_ID = "org.mozilla.gecko";
+    protected Assert mAsserter;
+    protected String mLogFile;
+
+    protected Map<?, ?> mConfig;
+    protected String mRootPath;
+
+    public BaseRobocopTest(Class<Activity> activityClass) {
+        super(activityClass);
+    }
+
+    @SuppressWarnings("deprecation")
+    public BaseRobocopTest(String targetPackageId, Class<Activity> activityClass) {
+        super(targetPackageId, activityClass);
+    }
+
+    protected abstract int getTestType();
+
+    @Override
+    protected void setUp() throws Exception {
+        // Load config file from root path (set up by Python script).
+        mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
+        String configFile = FennecNativeDriver.getFile(mRootPath + "/robotium.config");
+        mConfig = FennecNativeDriver.convertTextToTable(configFile);
+        mLogFile = (String) mConfig.get("logfile");
+
+        // Initialize the asserter.
+        if (getTestType() == TEST_TALOS) {
+            mAsserter = new FennecTalosAssert();
+        } else {
+            mAsserter = new FennecMochitestAssert();
+        }
+        mAsserter.setLogFile(mLogFile);
+        mAsserter.setTestName(this.getClass().getName());
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -1,85 +1,82 @@
+/* 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.tests;
 
-import com.jayway.android.robotium.solo.Condition;
-import com.jayway.android.robotium.solo.Solo;
-
-import org.mozilla.gecko.*;
-import org.mozilla.gecko.GeckoThread.LaunchState;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.Driver;
+import org.mozilla.gecko.Element;
+import org.mozilla.gecko.FennecNativeActions;
+import org.mozilla.gecko.FennecNativeDriver;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoThread.LaunchState;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.RobocopUtils;
+import org.mozilla.gecko.Tabs;
 
 import android.app.Activity;
-import android.app.Instrumentation;
-import android.content.ContentResolver;
 import android.content.ContentValues;
-import android.content.ContentUris;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.res.AssetManager;
 import android.database.Cursor;
-import android.net.Uri;
 import android.os.Build;
 import android.os.SystemClock;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentManager;
-import android.text.InputType;
 import android.text.TextUtils;
-import android.test.ActivityInstrumentationTestCase2;
 import android.util.DisplayMetrics;
+import android.view.View;
 import android.view.inputmethod.InputMethodManager;
-import android.view.KeyEvent;
-import android.view.View;
 import android.widget.AdapterView;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ListAdapter;
-import android.widget.ListView;
 import android.widget.TextView;
 
-import java.io.File;
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
+import com.jayway.android.robotium.solo.Condition;
+import com.jayway.android.robotium.solo.Solo;
 
 /**
  *  A convenient base class suitable for most Robocop tests.
  */
-abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
-    public static final int TEST_MOCHITEST = 0;
-    public static final int TEST_TALOS = 1;
-
-    private static final String TARGET_PACKAGE_ID = "org.mozilla.gecko";
+@SuppressWarnings("unchecked")
+abstract class BaseTest extends BaseRobocopTest {
     private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME = TestConstants.ANDROID_PACKAGE_NAME + ".App";
     private static final int VERIFY_URL_TIMEOUT = 2000;
-    private static final int MAX_LIST_ATTEMPTS = 3;
     private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
     private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
     public static final int MAX_WAIT_MS = 4500;
     public static final int LONG_PRESS_TIME = 6000;
     private static final int GECKO_READY_WAIT_MS = 180000;
     public static final int MAX_WAIT_BLOCK_FOR_EVENT_DATA_MS = 90000;
 
     private static Class<Activity> mLauncherActivityClass;
     private Activity mActivity;
     private int mPreferenceRequestID = 0;
     protected Solo mSolo;
     protected Driver mDriver;
-    protected Assert mAsserter;
     protected Actions mActions;
     protected String mBaseUrl;
     protected String mRawBaseUrl;
-    private String mLogFile;
     protected String mProfile;
     public Device mDevice;
     protected DatabaseHelper mDatabaseHelper;
     protected StringHelper mStringHelper;
 
     protected void blockForGeckoReady() {
         try {
             Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready");
@@ -99,52 +96,39 @@ abstract class BaseTest extends Activity
             throw new RuntimeException(e);
         }
     }
 
     public BaseTest() {
         super(TARGET_PACKAGE_ID, mLauncherActivityClass);
     }
 
-    protected abstract int getTestType();
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
 
-    @Override
-    protected void setUp() throws Exception {
-        // Load config file from root path (setup by python script)
-        String rootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
-        String configFile = FennecNativeDriver.getFile(rootPath + "/robotium.config");
-        HashMap config = FennecNativeDriver.convertTextToTable(configFile);
-        mLogFile = (String)config.get("logfile");
-        mBaseUrl = ((String)config.get("host")).replaceAll("(/$)", "");
-        mRawBaseUrl = ((String)config.get("rawhost")).replaceAll("(/$)", "");
-        // Initialize the asserter
-        if (getTestType() == TEST_TALOS) {
-            mAsserter = new FennecTalosAssert();
-        } else {
-            mAsserter = new FennecMochitestAssert();
-        }
-        mAsserter.setLogFile(mLogFile);
-        mAsserter.setTestName(this.getClass().getName());
         // Create the intent to be used with all the important arguments.
+        mBaseUrl = ((String) mConfig.get("host")).replaceAll("(/$)", "");
+        mRawBaseUrl = ((String) mConfig.get("rawhost")).replaceAll("(/$)", "");
         Intent i = new Intent(Intent.ACTION_MAIN);
-        mProfile = (String)config.get("profile");
+        mProfile = (String) mConfig.get("profile");
         i.putExtra("args", "-no-remote -profile " + mProfile);
-        String envString = (String)config.get("envvars");
+        String envString = (String) mConfig.get("envvars");
         if (envString != "") {
             String[] envStrings = envString.split(",");
             for (int iter = 0; iter < envStrings.length; iter++) {
                 i.putExtra("env" + iter, envStrings[iter]);
             }
         }
         // Start the activity
         setActivityIntent(i);
         mActivity = getActivity();
         // Set up Robotium.solo and Driver objects
         mSolo = new Solo(getInstrumentation(), mActivity);
-        mDriver = new FennecNativeDriver(mActivity, mSolo, rootPath);
+        mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath);
         mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
         mDevice = new Device();
         mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter);
         mStringHelper = new StringHelper();
     }
 
     @Override
     protected void runTest() throws Throwable {
@@ -328,58 +312,36 @@ abstract class BaseTest extends Activity
     }
 
     // TODO: With Robotium 4.2, we should use Condition and waitForCondition instead.
     // Future boolean tests should not implement this interface.
     protected interface BooleanTest {
         public boolean test();
     }
 
-    @SuppressWarnings({"unchecked", "non-varargs"})
     public void SqliteCompare(String dbName, String sqlCommand, ContentValues[] cvs) {
         File profile = new File(mProfile);
         String dbPath = new File(profile, dbName).getPath();
 
         Cursor c = mActions.querySql(dbPath, sqlCommand);
         SqliteCompare(c, cvs);
     }
 
-    private boolean CursorMatches(Cursor c, String[] columns, ContentValues cv) {
-        for (int i = 0; i < columns.length; i++) {
-            String column = columns[i];
-            if (cv.containsKey(column)) {
-                mAsserter.info("Comparing", "Column values for: " + column);
-                Object value = cv.get(column);
-                if (value == null) {
-                    if (!c.isNull(i)) {
-                        return false;
-                    }
-                } else {
-                    if (c.isNull(i) || !value.toString().equals(c.getString(i))) {
-                        return false;
-                    }
-                }
-            }
-        }
-        return true;
-    }
-
-    @SuppressWarnings({"unchecked", "non-varargs"})
     public void SqliteCompare(Cursor c, ContentValues[] cvs) {
         mAsserter.is(c.getCount(), cvs.length, "List is correct length");
         if (c.moveToFirst()) {
             do {
                 boolean found = false;
                 for (int i = 0; !found && i < cvs.length; i++) {
                     if (CursorMatches(c, cvs[i])) {
                         found = true;
                     }
                 }
                 mAsserter.is(found, true, "Password was found");
-            } while(c.moveToNext());
+            } while (c.moveToNext());
         }
     }
 
     public boolean CursorMatches(Cursor c, ContentValues cv) {
         for (int i = 0; i < c.getColumnCount(); i++) {
             String column = c.getColumnName(i);
             if (cv.containsKey(column)) {
                 mAsserter.info("Comparing", "Column values for: " + column);
--- a/mobile/android/base/tests/ContentProviderTest.java
+++ b/mobile/android/base/tests/ContentProviderTest.java
@@ -1,8 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
 package org.mozilla.gecko.tests;
 
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.ContentProviderResult;
 import android.content.ContentProviderOperation;
 import android.content.OperationApplicationException;
@@ -203,17 +207,17 @@ abstract class ContentProviderTest exten
         targetProvider.attachInfo(mProviderContext, null);
 
         mProvider = new DelegatingTestContentProvider(targetProvider);
         mProvider.attachInfo(mProviderContext, null);
 
         mResolver.addProvider(mProviderAuthority, mProvider);
     }
 
-    public Uri appendUriParam(Uri uri, String param, String value) {
+    public static Uri appendUriParam(Uri uri, String param, String value) {
         return uri.buildUpon().appendQueryParameter(param, value).build();
     }
 
     public void setTestName(String testName) {
         mAsserter.setTestName(this.getClass().getName() + " - " + testName);
     }
 
     @Override
--- a/mobile/android/base/tests/testBrowserProviderPerf.java
+++ b/mobile/android/base/tests/testBrowserProviderPerf.java
@@ -1,50 +1,92 @@
+/* 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.tests;
 
+import java.io.File;
+import java.util.Random;
+import java.util.UUID;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserProvider;
+import org.mozilla.gecko.db.LocalBrowserDB;
+import org.mozilla.gecko.util.FileUtils;
+
+import android.app.Activity;
+import android.content.ContentProvider;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
 import android.os.SystemClock;
-
-import java.util.UUID;
-import java.util.Random;
+import android.util.Log;
 
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.BrowserProvider;
+/**
+ * This test is meant to exercise the performance of Fennec's history and
+ * bookmarks content provider.
+ *
+ * It does not extend ContentProviderTest because that class is unable to
+ * accurately assess the performance of the ContentProvider -- it's a second
+ * instance of a class that's only supposed to exist once, wrapped in a bunch of
+ * junk.
+ *
+ * Instead, we directly use the existing ContentProvider, accessing a new
+ * profile directory that we initialize via BrowserDB.
+ */
+@SuppressWarnings("unchecked")
+public class testBrowserProviderPerf extends BaseRobocopTest {
+    private static final String LAUNCH_ACTIVITY_FULL_CLASSNAME = TestConstants.ANDROID_PACKAGE_NAME + ".App";
 
-/*
- * This test is meant to exercise the performance of Fennec's
- * history and bookmarks content provider.
- */
-public class testBrowserProviderPerf extends ContentProviderTest {
+    private static Class<Activity> mLauncherActivityClass;
+
     private final int NUMBER_OF_BASIC_HISTORY_URLS = 10000;
     private final int NUMBER_OF_BASIC_BOOKMARK_URLS = 500;
     private final int NUMBER_OF_COMBINED_URLS = 500;
 
     private final int NUMBER_OF_KNOWN_URLS = 200;
     private final int BATCH_SIZE = 500;
 
     // Include spaces in prefix to test performance querying with
-    // multiple constraint words
+    // multiple constraint words.
     private final String KNOWN_PREFIX = "my mozilla test ";
 
     private Random mGenerator;
 
     private final String MOBILE_FOLDER_GUID = "mobile";
     private long mMobileFolderId;
+    private ContentResolver mResolver;
+    private String mProfile;
+    private Uri mHistoryURI;
+    private Uri mBookmarksURI;
+    private Uri mFaviconsURI;
+
+    static {
+        try {
+            mLauncherActivityClass = (Class<Activity>) Class.forName(LAUNCH_ACTIVITY_FULL_CLASSNAME);
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public testBrowserProviderPerf() {
+        super(TARGET_PACKAGE_ID, mLauncherActivityClass);
+    }
 
     @Override
     protected int getTestType() {
         return TEST_TALOS;
     }
 
     private void loadMobileFolderId() throws Exception {
-        Cursor c = mProvider.query(BrowserContract.Bookmarks.CONTENT_URI, null,
+        Cursor c = mResolver.query(mBookmarksURI, null,
                                    BrowserContract.Bookmarks.GUID + " = ?",
                                    new String[] { MOBILE_FOLDER_GUID },
                                    null);
         c.moveToFirst();
         mMobileFolderId = c.getLong(c.getColumnIndex(BrowserContract.Bookmarks._ID));
 
         c.close();
     }
@@ -116,95 +158,173 @@ public class testBrowserProviderPerf ext
         return faviconEntry;
     }
 
     private String createRandomUrl(String knownPrefix) throws Exception {
         return knownPrefix + UUID.randomUUID().toString();
     }
 
     private void addTonsOfUrls() throws Exception {
-        // Create some random bookmark entries
+        // Create some random bookmark entries.
         ContentValues[] bookmarkEntries = new ContentValues[BATCH_SIZE];
 
         for (int i = 0; i < NUMBER_OF_BASIC_BOOKMARK_URLS / BATCH_SIZE; i++) {
             bookmarkEntries = new ContentValues[BATCH_SIZE];
 
             for (int j = 0; j < BATCH_SIZE; j++) {
                 bookmarkEntries[j] = createRandomBookmarkEntry();
             }
 
-            mProvider.bulkInsert(BrowserContract.Bookmarks.CONTENT_URI, bookmarkEntries);
+            mResolver.bulkInsert(mBookmarksURI, bookmarkEntries);
         }
 
-        // Create some random history entries
+        // Create some random history entries.
         ContentValues[] historyEntries = new ContentValues[BATCH_SIZE];
         ContentValues[] faviconEntries = new ContentValues[BATCH_SIZE];
 
         for (int i = 0; i < NUMBER_OF_BASIC_HISTORY_URLS / BATCH_SIZE; i++) {
             historyEntries = new ContentValues[BATCH_SIZE];
             faviconEntries = new ContentValues[BATCH_SIZE];
 
             for (int j = 0; j < BATCH_SIZE; j++) {
                 historyEntries[j] = createRandomHistoryEntry();
                 faviconEntries[j] = createFaviconEntryWithUrl(historyEntries[j].getAsString(BrowserContract.History.URL));
             }
 
-            mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
-            mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
+            mResolver.bulkInsert(mHistoryURI, historyEntries);
+            mResolver.bulkInsert(mFaviconsURI, faviconEntries);
         }
 
 
-        // Create random bookmark/history entries with the same url
+        // Create random bookmark/history entries with the same URL.
         for (int i = 0; i < NUMBER_OF_COMBINED_URLS / BATCH_SIZE; i++) {
             bookmarkEntries = new ContentValues[BATCH_SIZE];
             historyEntries = new ContentValues[BATCH_SIZE];
 
             for (int j = 0; j < BATCH_SIZE; j++) {
                 String url = createRandomUrl("");
                 bookmarkEntries[j] = createBookmarkEntryWithUrl(url);
                 historyEntries[j] = createHistoryEntryWithUrl(url);
                 faviconEntries[j] = createFaviconEntryWithUrl(url);
             }
 
-            mProvider.bulkInsert(BrowserContract.Bookmarks.CONTENT_URI, bookmarkEntries);
-            mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
-            mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
+            mResolver.bulkInsert(mBookmarksURI, bookmarkEntries);
+            mResolver.bulkInsert(mHistoryURI, historyEntries);
+            mResolver.bulkInsert(mFaviconsURI, faviconEntries);
         }
 
-        // Create some history entries with a known prefix
+        // Create some history entries with a known prefix.
         historyEntries = new ContentValues[NUMBER_OF_KNOWN_URLS];
         faviconEntries = new ContentValues[NUMBER_OF_KNOWN_URLS];
         for (int i = 0; i < NUMBER_OF_KNOWN_URLS; i++) {
             historyEntries[i] = createRandomHistoryEntry(KNOWN_PREFIX);
             faviconEntries[i] = createFaviconEntryWithUrl(historyEntries[i].getAsString(BrowserContract.History.URL));
         }
 
-        mProvider.bulkInsert(BrowserContract.History.CONTENT_URI, historyEntries);
-        mProvider.bulkInsert(BrowserContract.Favicons.CONTENT_URI, faviconEntries);
+        mResolver.bulkInsert(mHistoryURI, historyEntries);
+        mResolver.bulkInsert(mFaviconsURI, faviconEntries);
     }
 
     @Override
     public void setUp() throws Exception {
-        super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
+        super.setUp();
+
+        mProfile = "prof" + System.currentTimeMillis();
+
+        mHistoryURI = prepUri(BrowserContract.History.CONTENT_URI);
+        mBookmarksURI = prepUri(BrowserContract.Bookmarks.CONTENT_URI);
+        mFaviconsURI = prepUri(BrowserContract.Favicons.CONTENT_URI);
+
+        mResolver = getActivity().getApplicationContext().getContentResolver();
 
         mGenerator = new Random(19580427);
     }
 
-    public void testBrowserProviderPerf() throws Exception {
-        BrowserDB.initialize(GeckoProfile.DEFAULT_PROFILE);
+    @Override
+    public void tearDown() {
+        final ContentProviderClient client = mResolver.acquireContentProviderClient(mBookmarksURI);
+        try {
+            final ContentProvider cp = client.getLocalContentProvider();
+            final BrowserProvider bp = ((BrowserProvider) cp);
+
+            // This will be the DB we were just testing.
+            final SQLiteDatabase db = bp.getWritableDatabaseForTesting(mBookmarksURI);
+            try {
+                db.close();
+            } catch (Throwable e) {
+                // Nothing we can do.
+            }
+        } finally {
+            try {
+                client.release();
+            } catch (Throwable e) {
+                // Still go ahead and try to delete the profile.
+            }
+
+            try {
+                FileUtils.delTree(new File(mProfile), null, true);
+            } catch (Exception e) {
+                Log.w("GeckoTest", "Unable to delete profile " + mProfile, e);
+            }
+        }
+    }
 
+    public Uri prepUri(Uri uri) {
+        return uri.buildUpon()
+                  .appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile)
+                  .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1")       // So we don't trigger a sync.
+                  .build();
+    }
+
+    /**
+     * This method:
+     *
+     * * Adds a bunch of test data via the ContentProvider API.
+     * * Runs a single query against that test data via BrowserDB.
+     * * Reports timing for Talos.
+     */
+    public void testBrowserProviderQueryPerf() throws Exception {
+        // We add at least this many results.
+        final int limit = 100;
+
+        // Make sure we're querying the right profile.
+        final LocalBrowserDB db = new LocalBrowserDB(mProfile);
+
+        final Cursor before = db.filter(mResolver, KNOWN_PREFIX, limit);
+        try {
+            mAsserter.is(before.getCount(), 0, "Starts empty");
+        } finally {
+            before.close();
+        }
+
+        // Add data.
         loadMobileFolderId();
         addTonsOfUrls();
 
-        long start = SystemClock.uptimeMillis();
-
-        final Cursor c = BrowserDB.filter(mResolver, KNOWN_PREFIX, 100);
-        c.getCount(); // ensure query is not lazy loaded
-
-        long end = SystemClock.uptimeMillis();
+        // Wait for a little while after inserting data. We do this because
+        // this test launches about:home, and Top Sites watches for DB changes.
+        // We don't have a good way for it to only watch changes related to
+        // its current profile, nor is it convenient for us to launch a different
+        // activity that doesn't depend on the DB.
+        // We can fix this by:
+        // * Adjusting the provider interface to allow a "don't notify" param.
+        // * Adjusting the interface schema to include the profile in the path,
+        //   and only observe the correct path.
+        // * Launching a different activity.
+        Thread.sleep(5000);
 
-        mAsserter.dumpLog("__start_report" + Long.toString(end - start) + "__end_report");
-        mAsserter.dumpLog("__startTimestamp" + Long.toString(end - start) + "__endTimestamp");
+        // Time the query.
+        final long start = SystemClock.uptimeMillis();
+        final Cursor c = db.filter(mResolver, KNOWN_PREFIX, limit);
+
+        try {
+            final int count = c.getCount();
+            final long end = SystemClock.uptimeMillis();
 
-        c.close();
+            mAsserter.is(count, limit, "Retrieved results");
+            mAsserter.dumpLog("Results: " + count);
+            mAsserter.dumpLog("__start_report" + Long.toString(end - start) + "__end_report");
+            mAsserter.dumpLog("__startTimestamp" + Long.toString(end - start) + "__endTimestamp");
+        } finally {
+            c.close();
+        }
     }
-
 }
--- a/mobile/android/base/tests/testReadingListProvider.java
+++ b/mobile/android/base/tests/testReadingListProvider.java
@@ -1,18 +1,21 @@
+/* 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.tests;
 
 import java.util.HashSet;
 import java.util.Random;
 import java.util.concurrent.Callable;
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.ReadingListProvider;
-import org.mozilla.gecko.db.TransactionalProvider;
 
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 
@@ -63,22 +66,23 @@ public class testReadingListProvider ext
     @Override
     public void setUp() throws Exception {
         super.setUp(sProviderFactory, BrowserContract.READING_LIST_AUTHORITY, DB_NAME);
         for (TestCase test: TESTS_TO_RUN) {
             mTests.add(test);
         }
     }
 
-    public void testReadingListProvider() throws Exception {
+    public void testReadingListProviderTests() throws Exception {
         for (Runnable test : mTests) {
             setTestName(test.getClass().getSimpleName());
             ensureEmptyDatabase();
             test.run();
         }
+
         // Ensure browser initialization is complete before completing test,
         // so that the minidumps directory is consistently created.
         blockForGeckoReady();
     }
 
     /**
      * Verify that we can insert a reading list item into the DB.
      */
--- a/mobile/android/base/toolbar/ToolbarEditText.java
+++ b/mobile/android/base/toolbar/ToolbarEditText.java
@@ -105,16 +105,17 @@ public class ToolbarEditText extends Cus
         addTextChangedListener(new TextChangeListener());
     }
 
     @Override
     public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
 
         if (gainFocus) {
+            resetAutocompleteState();
             return;
         }
 
         InputMethodManager imm = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
         try {
             imm.hideSoftInputFromWindow(getWindowToken(), 0);
         } catch (NullPointerException e) {
             Log.e(LOGTAG, "InputMethodManagerService, why are you throwing"
@@ -147,16 +148,21 @@ public class ToolbarEditText extends Cus
     }
 
     @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
         updateTextTypeFromText(getText().toString());
     }
 
+    private void resetAutocompleteState() {
+        mAutoCompleteResult = "";
+        mAutoCompletePrefix = null;
+    }
+
     private void updateKeyboardInputType() {
         // If the user enters a space, then we know they are entering
         // search terms, not a URL. We can then switch to text mode so,
         //   1) the IME auto-inserts spaces between words
         //   2) the IME doesn't reset input keyboard to Latin keyboard.
         final String text = getText().toString();
         final int currentInputType = getInputType();
 
--- a/mobile/android/base/util/FileUtils.java
+++ b/mobile/android/base/util/FileUtils.java
@@ -5,16 +5,18 @@
 package org.mozilla.gecko.util;
 
 import android.util.Log;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.FilenameFilter;
 
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
 public class FileUtils {
     private static final String LOGTAG= "GeckoFileUtils";
     /*
     * A basic Filter for checking a filename and age.
     **/
     static public class NameAndAgeFilter implements FilenameFilter {
         final private String mName;
         final private double mMaxAge;
@@ -33,16 +35,17 @@ public class FileUtils {
                     return true;
                 }
             }
 
             return false;
         }
     }
 
+    @RobocopTarget
     public static void delTree(File dir, FilenameFilter filter, boolean recurse) {
         String[] files = null;
 
         if (filter != null) {
           files = dir.list(filter);
         } else {
           files = dir.list();
         }
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
@@ -69,8 +69,16 @@
 
         <receiver
             android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedReceiver"
             android:permission="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
             <intent-filter>
                 <action android:name="@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@.accounts.ACCOUNT_DELETED_ACTION"/>
             </intent-filter>
         </receiver>
+
+        <receiver
+            android:name="org.mozilla.gecko.fxa.receivers.FxAccountUpgradeReceiver">
+            <intent-filter>
+                <action android:name="android.intent.action.PACKAGE_REPLACED" />
+                <data android:scheme="package"/>
+            </intent-filter>
+        </receiver>
--- a/mobile/android/services/manifests/SyncAndroidManifest_activities.xml.in
+++ b/mobile/android/services/manifests/SyncAndroidManifest_activities.xml.in
@@ -56,17 +56,18 @@
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
             </intent-filter>
          </activity>
 
         <receiver
             android:name="org.mozilla.gecko.sync.receivers.UpgradeReceiver">
             <intent-filter>
-                <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+                <action android:name="android.intent.action.PACKAGE_REPLACED" />
+                <data android:scheme="package"/>
             </intent-filter>
         </receiver>
 
         <receiver
             android:name="org.mozilla.gecko.sync.receivers.SyncAccountDeletedReceiver"
             android:permission="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.permission.PER_ACCOUNT_TYPE">
             <intent-filter>
                 <action android:name="@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@.accounts.SYNC_ACCOUNT_DELETED_ACTION"/>
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -560,19 +560,16 @@ pref("devtools.debugger.remote-enabled",
 pref("devtools.debugger.remote-port", 6000);
 // Force debugger server binding on the loopback interface
 pref("devtools.debugger.force-local", true);
 // Display a prompt when a new connection starts to accept/reject it
 pref("devtools.debugger.prompt-connection", true);
 // Block tools from seeing / interacting with certified apps
 pref("devtools.debugger.forbid-certified-apps", true);
 
-// Disable add-on debugging
-pref("devtools.debugger.addon-enabled", false);
-
 // DevTools default color unit
 pref("devtools.defaultColorUnit", "hex");
 
 // Used for devtools debugging
 pref("devtools.dump.emit", false);
 
 // view source
 pref("view_source.syntax_highlight", true);
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -44,16 +44,20 @@ user_pref("dom.undo_manager.enabled", tr
 user_pref("dom.webcomponents.enabled", true);
 // Set a future policy version to avoid the telemetry prompt.
 user_pref("toolkit.telemetry.prompted", 999);
 user_pref("toolkit.telemetry.notifiedOptOut", 999);
 // Existing tests assume there is no font size inflation.
 user_pref("font.size.inflation.emPerLine", 0);
 user_pref("font.size.inflation.minTwips", 0);
 
+// AddonManager tests require that the experiments feature be enabled.
+user_pref("experiments.enabled", true);
+user_pref("experiments.supported", true);
+
 // Only load extensions from the application and user profile
 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
 user_pref("extensions.enabledScopes", 5);
 // Disable metadata caching for installed add-ons by default
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable intalling any distribution add-ons
 user_pref("extensions.installDistroAddons", false);
 // XPI extensions are required for test harnesses to load
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -1372,17 +1372,17 @@ ThreadClient.prototype = {
       // Put the client in a tentative "resuming" state so we can prevent
       // further requests that should only be sent in the paused state.
       this._state = "resuming";
       return aPacket;
     },
     after: function (aResponse) {
       if (aResponse.error) {
         // There was an error resuming, back to paused state.
-        self._state = "paused";
+        this._state = "paused";
       }
       return aResponse;
     },
     telemetry: "CLIENTEVALUATE"
   }),
 
   /**
    * Detach from the thread actor.
--- a/toolkit/devtools/server/tests/mochitest/test_preference.html
+++ b/toolkit/devtools/server/tests/mochitest/test_preference.html
@@ -44,17 +44,17 @@ function runTests() {
       };
 
 
       function checkValues() {
         is(prefs.boolPref, localPref.boolPref, "read/write bool pref");
         is(prefs.intPref, localPref.intPref, "read/write int pref");
         is(prefs.charPref, localPref.charPref, "read/write string pref");
 
-        for (var key in prefs.allPrefs) {
+        ["test.all.bool", "test.all.int", "test.all.string"].forEach(function(key) {
           var expectedValue;
           switch(Services.prefs.getPrefType(key)) {
             case Ci.nsIPrefBranch.PREF_STRING:
               expectedValue = Services.prefs.getCharPref(key);
               break;
             case Ci.nsIPrefBranch.PREF_INT:
               expectedValue = Services.prefs.getIntPref(key);
               break;
@@ -63,19 +63,20 @@ function runTests() {
               break;
             default:
               ok(false, "unexpected pref type (" + key + ")");
               break;
           }
 
           is(prefs.allPrefs[key].value, expectedValue, "valid preference value (" + key + ")");
           is(prefs.allPrefs[key].hasUserValue, Services.prefs.prefHasUserValue(key), "valid hasUserValue (" + key + ")");
-        }
+        });
 
         ["test.bool", "test.int", "test.string"].forEach(function(key) {
+          ok(!prefs.allPrefs.hasOwnProperty(key), "expect no pref (" + key + ")");
           is(Services.prefs.getPrefType(key), Ci.nsIPrefBranch.PREF_INVALID, "pref (" + key + ") is clear");
         });
 
         client.close(() => {
           DebuggerServer.destroy();
           SimpleTest.finish()
         });
       }
@@ -97,15 +98,18 @@ function runTests() {
   });
 
 }
 
 window.onload = function () {
   SpecialPowers.pushPrefEnv({
     "set": [
       ["devtools.debugger.forbid-certified-apps", false],
+      ["test.all.bool", true],
+      ["test.all.int", 0x4321],
+      ["test.all.string", "allizom"],
     ]
   }, runTests);
 }
 </script>
 </pre>
 </body>
 </html>
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.dtd
@@ -212,8 +212,19 @@
 <!ENTITY addon.createdBy.label                "By ">
 
 <!ENTITY eula.title                           "End-User License Agreement">
 <!ENTITY eula.width                           "560px">
 <!ENTITY eula.height                          "400px">
 <!ENTITY eula.accept                          "Accept and Install…">
 
 <!ENTITY settings.path.button.label           "Browse…">
+
+<!-- LOCALIZATION NOTE (experiment.info.label): The strings related to
+     experiments are present on the "Experiments" tab of the add-ons manager.
+     This tab won't be displayed unless an Experiment add-on is installed.
+     Install https://people.mozilla.org/~gszorc/dummy-experiment-addon.xpi
+     to cause this tab to appear. -->
+<!ENTITY experiment.info.label "What's this? Telemetry may install and run experiments from time to time.">
+<!ENTITY experiment.info.learnmore "Learn More">
+<!ENTITY experiment.info.learnmore.accesskey "L">
+<!ENTITY experiment.info.changetelemetry "Telemetry Settings">
+<!ENTITY experiment.info.changetelemetry.accesskey "T">
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
@@ -126,8 +126,9 @@ cmd.purchaseAddon.accesskey=u
 eulaHeader=%S requires that you accept the following End User License Agreement before installation can proceed:
 
 type.extension.name=Extensions
 type.theme.name=Appearance
 type.locale.name=Languages
 type.plugin.name=Plugins
 type.dictionary.name=Dictionaries
 type.service.name=Services
+type.experiment.name=Experiments
--- a/toolkit/mozapps/extensions/content/extensions.css
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -183,17 +183,21 @@ setting[type="menulist"] {
 
 /* Plugins aren't yet disabled by safemode (bug 342333),
    so don't show that warning when viewing plugins. */
 #addons-page[warning="safemode"] .view-pane[type="plugin"] .global-warning-container,
 #addons-page[warning="safemode"] #detail-view[loading="true"] .global-warning {
   display: none;
 }
 
-#addons-page .view-pane:not([type="plugin"]) .global-info-container {
+#addons-page .view-pane:not([type="plugin"]) .plugin-info-container {
+  display: none;
+}
+
+#addons-page .view-pane:not([type="experiment"]) .experiment-info-container {
   display: none;
 }
 
 .addon .relnotes {
   -moz-user-select: text;
 }
 #detail-name, #detail-desc, #detail-fulldesc {
   -moz-user-select: text;
@@ -216,8 +220,16 @@ richlistitem:not([selected]) * {
 
 #header-utils-btn {
   -moz-user-focus: normal;
 }
 
 .discover-button[disabled="true"] {
   display: none;
 }
+
+#experiments-learn-more[disabled="true"] {
+  display: none;
+}
+
+#experiments-change-telemetry[disabled="true"] {
+  display: none;
+}
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -24,17 +24,17 @@ XPCOMUtils.defineLazyGetter(this, "Brows
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
 const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
 const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
-const PREF_ADDON_DEBUGGING_ENABLED = "devtools.debugger.addon-enabled";
+const PREF_ADDON_DEBUGGING_ENABLED = "devtools.chrome.enabled";
 const PREF_REMOTE_DEBUGGING_ENABLED = "devtools.debugger.remote-enabled";
 
 const LOADING_MSG_DELAY = 100;
 
 const SEARCH_SCORE_MULTIPLIER_NAME = 2;
 const SEARCH_SCORE_MULTIPLIER_DESCRIPTION = 2;
 
 // Use integers so search scores are sortable by nsIXULSortService
@@ -204,16 +204,46 @@ function isDiscoverEnabled() {
     if (!Services.prefs.getBoolPref(PREF_XPI_ENABLED))
       return false;
   } catch (e) {}
 
   return true;
 }
 
 /**
+ * Obtain the main DOMWindow for the current context.
+ */
+function getMainWindow() {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIWebNavigation)
+               .QueryInterface(Ci.nsIDocShellTreeItem)
+               .rootTreeItem
+               .QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindow);
+}
+
+/**
+ * Obtain the DOMWindow that can open a preferences pane.
+ *
+ * This is essentially "get the browser chrome window" with the added check
+ * that the supposed browser chrome window is capable of opening a preferences
+ * pane.
+ *
+ * This may return null if we can't find the browser chrome window.
+ */
+function getMainWindowWithPreferencesPane() {
+  let mainWindow = getMainWindow();
+  if (mainWindow && "openAdvancedPreferences" in mainWindow) {
+    return mainWindow;
+  } else {
+    return null;
+  }
+}
+
+/**
  * A wrapper around the HTML5 session history service that allows the browser
  * back/forward controls to work within the manager
  */
 var HTML5History = {
   get index() {
     return window.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIWebNavigation)
                  .sessionHistory.index;
@@ -1240,16 +1270,37 @@ var gViewController = {
         let addonType = AddonManager.addonTypes[aAddon.type];
         return ((addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) &&
                 hasPermission(aAddon, "disable"));
       },
       doCommand: function cmd_neverActivateItem_doCommand(aAddon) {
         aAddon.userDisabled = true;
       }
     },
+
+    cmd_experimentsLearnMore: {
+      isEnabled: function cmd_experimentsLearnMore_isEnabled() {
+        let mainWindow = getMainWindow();
+        return mainWindow && "switchToTabHavingURI" in mainWindow;
+      },
+      doCommand: function cmd_experimentsLearnMore_doCommand() {
+        let url = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
+        openOptionsInTab(url);
+      },
+    },
+
+    cmd_experimentsOpenTelemetryPreferences: {
+      isEnabled: function cmd_experimentsOpenTelemetryPreferences_isEnabled() {
+        return !!getMainWindowWithPreferencesPane();
+      },
+      doCommand: function cmd_experimentsOpenTelemetryPreferences_doCommand() {
+        let mainWindow = getMainWindowWithPreferencesPane();
+        mainWindow.openAdvancedPreferences("dataChoicesTab");
+      },
+    },
   },
 
   supportsCommand: function gVC_supportsCommand(aCommand) {
     return (aCommand in this.commands);
   },
 
   isCommandEnabled: function gVC_isCommandEnabled(aCommand) {
     if (!this.supportsCommand(aCommand))
@@ -1297,21 +1348,17 @@ var gViewController = {
 };
 
 function hasInlineOptions(aAddon) {
   return (aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE ||
           aAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO);
 }
 
 function openOptionsInTab(optionsURL) {
-  var mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShellTreeItem)
-                         .rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindow);
+  let mainWindow = getMainWindow();
   if ("switchToTabHavingURI" in mainWindow) {
     mainWindow.switchToTabHavingURI(optionsURL, true);
     return true;
   }
   return false;
 }
 
 function formatDate(aDate) {
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -1338,17 +1338,17 @@
 
           this.setAttribute("active", this.mAddon.isActive);
 
           var showProgress = this.mAddon.purchaseURL || (this.mAddon.install &&
                              this.mAddon.install.state != AddonManager.STATE_INSTALLED);
           this._showStatus(showProgress ? "progress" : "none");
 
           let debuggable = this.mAddon.isDebuggable &&
-                           Services.prefs.getBoolPref('devtools.debugger.addon-enabled') &&
+                           Services.prefs.getBoolPref('devtools.chrome.enabled') &&
                            Services.prefs.getBoolPref('devtools.debugger.remote-enabled');
 
           this._debugBtn.disabled = this._debugBtn.hidden = !debuggable
         ]]></body>
       </method>
 
       <method name="_updateUpgradeInfo">
         <body><![CDATA[
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -84,16 +84,18 @@
     <command id="cmd_installFromFile"/>
     <command id="cmd_back"/>
     <command id="cmd_forward"/>
     <command id="cmd_enableCheckCompatibility"/>
     <command id="cmd_pluginCheck"/>
     <command id="cmd_enableUpdateSecurity"/>
     <command id="cmd_toggleAutoUpdateDefault"/>
     <command id="cmd_resetAddonAutoUpdate"/>
+    <command id="cmd_experimentsLearnMore"/>
+    <command id="cmd_experimentsOpenTelemetryPreferences"/>
   </commandset>
 
   <!-- view commands - these act on the selected addon -->
   <commandset id="viewCommandSet"
               events="richlistbox-select" commandupdater="true">
     <command id="cmd_showItemDetails"/>
     <command id="cmd_findItemUpdates"/>
     <command id="cmd_showItemPreferences"/>
@@ -342,25 +344,41 @@
               </hbox>
               <button class="button-link global-warning-updatesecurity"
                       label="&warning.updatesecurity.enable.label;"
                       tooltiptext="&warning.updatesecurity.enable.tooltip;"
                       command="cmd_enableUpdateSecurity"/>
               <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
             </hbox>
           </hbox>
-          <hbox class="view-header global-info-container">
+          <hbox class="view-header global-info-container plugin-info-container">
             <hbox class="global-info" flex="1" align="center">
               <button class="button-link global-info-plugincheck"
                       label="&info.plugincheck.label;"
                       tooltiptext="&info.plugincheck.tooltip;"
                       command="cmd_pluginCheck"/>
               <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
             </hbox>
           </hbox>
+          <hbox class="view-header global-info-container experiment-info-container">
+            <hbox class="global-info" flex="1" align="center">
+              <label value="&experiment.info.label;"/>
+              <button id="experiments-learn-more"
+                      label="&experiment.info.learnmore;"
+                      tooltiptext="&experiment.info.learnmore;"
+                      accesskey="&experiment.info.learnmore.accesskey;"
+                      command="cmd_experimentsLearnMore"/>
+              <button id="experiments-change-telemetry"
+                      label="&experiment.info.changetelemetry;"
+                      tooltiptext="&experiment.info.changetelemetry;"
+                      accesskey="&experiment.info.changetelemetry.accesskey;"
+                      command="cmd_experimentsOpenTelemetryPreferences"/>
+              <spacer flex="5000"/> <!-- Necessary to allow the message to wrap. -->
+            </hbox>
+          </hbox>
           <vbox id="addon-list-empty" class="alert-container"
                 flex="1" hidden="true">
             <spacer class="alert-spacer-before"/>
             <vbox class="alert">
               <label value="&listEmpty.installed.label;"/>
               <button class="discover-button"
                       id="discover-button-install"
                       label="&listEmpty.button.label;"
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -163,19 +163,26 @@ const BOOTSTRAP_REASONS = {
 };
 
 // Map new string type identifiers to old style nsIUpdateItem types
 const TYPES = {
   extension: 2,
   theme: 4,
   locale: 8,
   multipackage: 32,
-  dictionary: 64
+  dictionary: 64,
+  experiment: 128,
 };
 
+const RESTARTLESS_TYPES = new Set([
+  "dictionary",
+  "experiment",
+  "locale",
+]);
+
 // Keep track of where we are in startup for telemetry
 // event happened during XPIDatabase.startup()
 const XPI_STARTING = "XPIStarting";
 // event happened after startup() but before the final-ui-startup event
 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
 // event happened after final-ui-startup
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
 
@@ -814,19 +821,20 @@ function loadManifestFromRDF(aUri, aStre
         addon.optionsType != AddonManager.OPTIONS_TYPE_DIALOG &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_TAB &&
         addon.optionsType != AddonManager.OPTIONS_TYPE_INLINE_INFO) {
       throw new Error("Install manifest specifies unknown type: " + addon.optionsType);
     }
   }
   else {
-    // spell check dictionaries and language packs never require a restart
-    if (addon.type == "dictionary" || addon.type == "locale")
+    // Some add-on types are always restartless.
+    if (RESTARTLESS_TYPES.has(addon.type)) {
       addon.bootstrap = true;
+    }
 
     // Only extensions are allowed to provide an optionsURL, optionsType or aboutURL. For
     // all other types they are silently ignored
     addon.optionsURL = null;
     addon.optionsType = null;
     addon.aboutURL = null;
 
     if (addon.type == "theme") {
@@ -7265,24 +7273,39 @@ WinRegInstallLocation.prototype = {
    * @see DirectoryInstallLocation
    */
   isLinkedAddon: function RegInstallLocation_isLinkedAddon(aId) {
     return true;
   }
 };
 #endif
 
-AddonManagerPrivate.registerProvider(XPIProvider, [
+let addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 4000),
   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 5000),
   new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 7000,
                                     AddonManager.TYPE_UI_HIDE_EMPTY),
   new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 8000,
-                                    AddonManager.TYPE_UI_HIDE_EMPTY)
-]);
+                                    AddonManager.TYPE_UI_HIDE_EMPTY),
+];
+
+// We only register experiments support if the application supports them.
+// Ideally, we would install an observer to watch the pref. Installing
+// an observer for this pref is not necessary here and may be buggy with
+// regards to registering this XPIProvider twice.
+if (Prefs.getBoolPref("experiments.supported", false)) {
+  addonTypes.push(
+    new AddonManagerPrivate.AddonType("experiment",
+                                      URI_EXTENSION_STRINGS,
+                                      STRING_TYPE_NAME,
+                                      AddonManager.VIEW_TYPE_LIST, 11000,
+                                      AddonManager.TYPE_UI_HIDE_EMPTY));
+}
+
+AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>test-experiment1@experiments.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:type>128</em:type>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Experiment 1</em:name>
+    <em:description>Test Description</em:description>
+
+  </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/browser/browser-common.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini
@@ -28,16 +28,17 @@ support-files =
 [browser_bug618502.js]
 [browser_bug679604.js]
 [browser_bug714593.js]
 [browser_bug590347.js]
 [browser_debug_button.js]
 [browser_details.js]
 [browser_discovery.js]
 [browser_dragdrop.js]
+[browser_experiments.js]
 [browser_list.js]
 [browser_metadataTimeout.js]
 [browser_searching.js]
 [browser_sorting.js]
 [browser_uninstalling.js]
 [browser_install.js]
 [browser_recentupdates.js]
 [browser_manualupdates.js]
--- a/toolkit/mozapps/extensions/test/browser/browser_debug_button.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_debug_button.js
@@ -7,17 +7,17 @@
  */
 
 let { Promise } = Components.utils.import("resource://gre/modules/Promise.jsm", {});
 let { Task } = Components.utils.import("resource://gre/modules/Task.jsm", {});
 
 const getDebugButton = node =>
     node.ownerDocument.getAnonymousElementByAttribute(node, "anonid", "debug-btn");
 const addonDebuggingEnabled = bool =>
-  Services.prefs.setBoolPref("devtools.debugger.addon-enabled", !!bool);
+  Services.prefs.setBoolPref("devtools.chrome.enabled", !!bool);
 const remoteDebuggingEnabled = bool =>
   Services.prefs.setBoolPref("devtools.debugger.remote-enabled", !!bool);
 
 function test() {
   requestLongerTimeout(2);
 
   waitForExplicitFinish();
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+let gManagerWindow;
+let gCategoryUtilities;
+let gInstalledAddons = [];
+
+function test() {
+  waitForExplicitFinish();
+
+  open_manager(null, (win) => {
+    gManagerWindow = win;
+    gCategoryUtilities = new CategoryUtilities(win);
+
+    run_next_test();
+  });
+}
+
+function end_test() {
+  for (let addon of gInstalledAddons) {
+    addon.uninstall();
+  }
+
+  close_manager(gManagerWindow, finish);
+}
+
+// On an empty profile with no experiments, the experiment category
+// should be hidden.
+add_test(function testInitialState() {
+  Assert.ok(gCategoryUtilities.get("experiment", false), "Experiment tab is defined.");
+
+  Assert.ok(!gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab hidden by default.");
+
+  run_next_test();
+});
+
+add_test(function testExperimentInfoNotVisible() {
+  gCategoryUtilities.openType("extension", () => {
+    let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+    is_element_hidden(el, "Experiment info not visible on other types.");
+
+    run_next_test();
+  });
+});
+
+// If we have an active experiment, we should see the experiments tab
+// and that tab should have some messages.
+add_test(function testActiveExperiment() {
+  install_addon("addons/browser_experiment1.xpi", (addon) => {
+    gInstalledAddons.push(addon);
+
+    // This may change if we remove compatibility checking from experiments.
+    // Putting this check here so a test fails if preconditions change.
+    Assert.equal(addon.isActive, false, "Add-on is not active.");
+
+    Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
+
+    gCategoryUtilities.openType("experiment", (win) => {
+      let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+      is_element_visible(el, "Experiment info is visible on experiment tab.");
+
+      run_next_test();
+    });
+  });
+});
+
+add_test(function testExperimentLearnMore() {
+  // Actual URL is irrelevant.
+  Services.prefs.setCharPref("toolkit.telemetry.infoURL",
+                             "http://mochi.test:8888/server.js");
+
+  gCategoryUtilities.openType("experiment", (win) => {
+    let btn = gManagerWindow.document.getElementById("experiments-learn-more");
+
+    if (!gUseInContentUI) {
+      is_element_hidden(btn, "Learn more button hidden if not using in-content UI.");
+      Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
+
+      run_next_test();
+      return;
+    } else {
+      is_element_visible(btn, "Learn more button visible.");
+    }
+
+    window.addEventListener("DOMContentLoaded", function onLoad(event) {
+      info("Telemetry privacy policy window opened.");
+      window.removeEventListener("DOMContentLoaded", onLoad, false);
+
+      let browser = gBrowser.selectedTab.linkedBrowser;
+      let expected = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
+      Assert.equal(browser.currentURI.spec, expected, "New tab should have loaded privacy policy.");
+      browser.contentWindow.close();
+
+      Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
+
+      run_next_test();
+    }, false);
+
+    info("Opening telemetry privacy policy.");
+    EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
+  });
+});
+
+add_test(function testOpenPreferences() {
+  gCategoryUtilities.openType("experiment", (win) => {
+    let btn = gManagerWindow.document.getElementById("experiments-change-telemetry");
+    if (!gUseInContentUI) {
+      is_element_hidden(btn, "Change telemetry button not enabled in out of window UI.");
+      info("Skipping preferences open test because not using in-content UI.");
+      run_next_test();
+      return;
+    }
+
+    is_element_visible(btn, "Change telemetry button visible in in-content UI.");
+
+    Services.obs.addObserver(function observer(prefWin, topic, data) {
+      Services.obs.removeObserver(observer, "advanced-pane-loaded");
+
+      info("Advanced preference pane opened.");
+
+      // We want this test to fail if the preferences pane changes.
+      let el = prefWin.document.getElementById("dataChoicesPanel");
+      is_element_visible(el);
+
+      prefWin.close();
+      info("Closed preferences pane.");
+
+      run_next_test();
+    }, "advanced-pane-loaded", false);
+
+    info("Loading preferences pane.");
+    EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
+  });
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -63,17 +63,17 @@ var gRestorePrefs = [{name: PREF_LOGGING
                      {name: "extensions.webservice.discoverURL"},
                      {name: "extensions.update.url"},
                      {name: "extensions.update.background.url"},
                      {name: "extensions.getAddons.get.url"},
                      {name: "extensions.getAddons.getWithPerformance.url"},
                      {name: "extensions.getAddons.search.browseURL"},
                      {name: "extensions.getAddons.search.url"},
                      {name: "extensions.getAddons.cache.enabled"},
-                     {name: "devtools.debugger.addon-enabled"},
+                     {name: "devtools.chrome.enabled"},
                      {name: "devtools.debugger.remote-enabled"},
                      {name: PREF_SEARCH_MAXRESULTS},
                      {name: PREF_STRICT_COMPAT},
                      {name: PREF_CHECK_COMPATIBILITY}];
 
 for (let pref of gRestorePrefs) {
   if (!Services.prefs.prefHasUserValue(pref.name)) {
     pref.type = "clear";
@@ -413,16 +413,35 @@ function is_element_visible(aElement, aM
   ok(!is_hidden(aElement), aMsg);
 }
 
 function is_element_hidden(aElement, aMsg) {
   isnot(aElement, null, "Element should not be null, when checking visibility");
   ok(is_hidden(aElement), aMsg);
 }
 
+/**
+ * Install an add-on and call a callback when complete.
+ *
+ * The callback will receive the Addon for the installed add-on.
+ */
+function install_addon(path, cb, pathPrefix=TESTROOT) {
+  AddonManager.getInstallForURL(pathPrefix + path, (install) => {
+    install.addListener({
+      onInstallEnded: () => {
+        executeSoon(() => {
+          cb(install.addon);
+        });
+      },
+    });
+
+    install.install();
+  }, "application/x-xpinstall");
+}
+
 function CategoryUtilities(aManagerWindow) {
   this.window = aManagerWindow;
 
   var self = this;
   this.window.addEventListener("unload", function() {
     self.window.removeEventListener("unload", arguments.callee, false);
     self.window = null;
   }, false);
--- a/toolkit/themes/linux/global/alerts/alert.css
+++ b/toolkit/themes/linux/global/alerts/alert.css
@@ -57,18 +57,17 @@ label {
   text-decoration: underline;
 }
 
 .alertText[clickable="true"]:hover:active {
   color: -moz-activehyperlinktext;
 }
 
 .alertCloseButton {
-  list-style-image: url("moz-icon://stock/gtk-close?size=button");
-}
-
-.alertCloseButton > .toolbarbutton-icon {
-  margin: -4px;
+  -moz-appearance: none;
+  height: 16px;
+  padding: 4px 2px;
+  width: 16px;
 }
 
 .alertCloseButton > .toolbarbutton-text {
   display: none;
 }
--- a/toolkit/themes/linux/global/findBar.css
+++ b/toolkit/themes/linux/global/findBar.css
@@ -24,18 +24,20 @@ findbar[hidden] {
 
 .findbar-container {
   -moz-padding-start: 8px;
   padding-top: 4px;
   padding-bottom: 4px;
 }
 
 .findbar-closebutton {
-  -moz-margin-start: 4px;
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
+  -moz-appearance: none;
+  width: 16px;
+  height: 16px;
+  margin: 0 8px;
 }
 
 /* Search field */
 
 .findbar-textbox {
   -moz-appearance: none;
   border: 1px solid ThreeDShadow;
   box-shadow: 0 0 1px 0 ThreeDShadow inset;
--- a/toolkit/themes/linux/global/global.css
+++ b/toolkit/themes/linux/global/global.css
@@ -301,8 +301,24 @@ notification > button {
 
 .autoscroller[scrolldir="NS"] {
   background-position: right center;
 }
 
 .autoscroller[scrolldir="EW"] {
   background-position: right bottom;
 }
+
+/* :::::: Close button icons ::::: */
+
+.close-icon {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 16, 16, 0);
+  background-position: center center;
+  background-repeat: no-repeat;
+}
+
+.close-icon:hover {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 32, 16, 16);
+}
+
+.close-icon:hover:active {
+  background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 48, 16, 32);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/themes/linux/global/icons/close.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
+<svg version="1.1"
+     id="icon-close"
+     xmlns="http://www.w3.org/2000/svg"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     x="0px"
+     y="0px"
+     width="96px"
+     height="16px"
+     viewBox="0 0 96 16">
+
+  <defs>
+    <style type="text/css"><![CDATA[
+      /* X Glyph Styles */
+
+      .glyphShape-style-foreground {
+        fill: ButtonText;
+        fill-opacity: .8;
+      }
+
+      .glyphShape-style-background {
+        fill: -moz-MenuBarText;
+        fill-opacity: .8;
+      }
+
+      .glyphShape-style-hover {
+        fill: #fff;
+      }
+
+      .glyphShape-style-hover-shadow {
+        fill: #b32c12;
+      }
+
+      .glyphShape-style-hover-active {
+        fill: #fff;
+        fill-opacity: .8;
+      }
+
+      .glyphShape-style-hover-active-shadow {
+        fill: #99260f;
+      }
+
+      .glyphShape-style-LWT-bright {
+        fill: #fff;
+        fill-opacity: .8;
+      }
+
+      .glyphShape-style-LWT-dark {
+        fill: #000;
+        fill-opacity: .8;
+      }
+
+
+      /* Close Button Background Styles */
+
+      .icon-background-hover {
+        fill: #d93616;
+      }
+
+      .icon-background-hover-active {
+        fill: #b32c12;
+      }
+      ]]></style>
+
+    <polygon id="glyphShape-close" points="4,5.5 5.5,4 8,6.5 10.5,4 12,5.5 9.5,8 12,10.5 10.5,12 8,9.5 5.5,12 4,10.5 6.5,8"/>
+    <polygon id="glyphShape-close-topHighlight" points="4,5.5 5.5,4 8,6.5 10.5,4 12,5.5 9.5,8 11.5,6 10.5,5 8,7.5 5.5,5 4.5,6"/>
+    <rect    id="glyphShape-background" x="2" y="2" rx="2" width="12" height="12"/>
+
+  </defs>
+
+  <g id="icon-closeForeground-default">
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-foreground" />
+    <use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-foreground" />
+  </g>
+
+  <g id="icon-close-hover" transform="translate(16)">
+    <use xlink:href="#glyphShape-background" class="icon-background-hover" />
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-hover-shadow" transform="translate(0,1)" />
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-hover" />
+  </g>
+
+  <g id="icon-close-hover-active" transform="translate(32)">
+    <use xlink:href="#glyphShape-background" class="icon-background-hover-active" />
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-hover-active-shadow" transform="translate(0,1)" />
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-hover-active" />
+  </g>
+
+  <g id="icon-closeBackground-default" transform="translate(48)">
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-background" />
+    <use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-background" />
+  </g>
+
+  <g id="icon-close-LWT-bright" transform="translate(64)">
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-LWT-bright" />
+    <use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-LWT-bright" />
+  </g>
+
+  <g id="icon-close-LWT-dark" transform="translate(80)">
+    <use xlink:href="#glyphShape-close" class="glyphShape-style-LWT-dark" />
+    <use xlink:href="#glyphShape-close-topHighlight" class="glyphShape-style-LWT-dark" />
+  </g>
+
+</svg>
--- a/toolkit/themes/linux/global/jar.mn
+++ b/toolkit/themes/linux/global/jar.mn
@@ -38,16 +38,17 @@ toolkit.jar:
 +  skin/classic/global/console/console.css                     (console/console.css)
 +  skin/classic/global/console/console.png                     (console/console.png)
 +  skin/classic/global/console/console-toolbar.png             (console/console-toolbar.png)
 +  skin/classic/global/dirListing/remote.png                   (dirListing/remote.png)
 +  skin/classic/global/icons/Authentication.png                (icons/Authentication.png)
 +  skin/classic/global/icons/autoscroll.png                    (icons/autoscroll.png)
 +  skin/classic/global/icons/blacklist_favicon.png             (icons/blacklist_favicon.png)
 +  skin/classic/global/icons/blacklist_large.png               (icons/blacklist_large.png)
++  skin/classic/global/icons/close.svg                         (icons/close.svg)
 +  skin/classic/global/icons/find.png                          (icons/find.png)
 +  skin/classic/global/icons/loading_16.png                    (icons/loading_16.png)
 +  skin/classic/global/icons/panelarrow-horizontal.svg         (icons/panelarrow-horizontal.svg)
 +  skin/classic/global/icons/panelarrow-vertical.svg           (icons/panelarrow-vertical.svg)
 +  skin/classic/global/icons/resizer.png                       (icons/resizer.png)
 +  skin/classic/global/icons/sslWarning.png                    (icons/sslWarning.png)
 +  skin/classic/global/icons/wrap.png                          (icons/wrap.png)
 +  skin/classic/global/icons/webapps-16.png                    (icons/webapps-16.png)
--- a/toolkit/themes/linux/global/notification.css
+++ b/toolkit/themes/linux/global/notification.css
@@ -47,19 +47,21 @@ notification[type="critical"] {
   list-style-image: url("moz-icon://stock/gtk-dialog-warning?size=menu");
 }
 
 .messageImage[type="critical"] {
   list-style-image: url("moz-icon://stock/gtk-dialog-error?size=menu");
 }
 
 .messageCloseButton {
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-  margin-top: 0;
-  margin-bottom: 0;
+  -moz-appearance: none;
+  width: 16px;
+  height: 16px;
+  padding-left: 11px;
+  padding-right: 11px;
 }
 
 /* Popup notification */
 
 .popup-notification-description {
   max-width: 24em;
 }
 
@@ -69,12 +71,8 @@ notification[type="critical"] {
 
 .popup-notification-learnmore-link:not([href]) {
   display: none;
 }
 
 .popup-notification-button-container {
   margin-top: 17px;
 }
-
-.popup-notification-closeitem {
-  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
-}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
--- a/toolkit/themes/linux/mozapps/extensions/extensions.css
+++ b/toolkit/themes/linux/mozapps/extensions/extensions.css
@@ -221,16 +221,19 @@
   list-style-image: url("chrome://mozapps/skin/extensions/category-themes.png");
 }
 #category-plugin > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-plugins.png");
 }
 #category-dictionary > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-dictionaries.png");
 }
+#category-experiment > .category-icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/category-experiments.png");
+}
 #category-availableUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-available.png");
 }
 #category-recentUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-recent.png");
 }
 
 
@@ -412,16 +415,20 @@
 .addon-view[type="plugin"] .icon {
   list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png");
 }
 
 .addon-view[type="dictionary"] .icon {
   list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png");
 }
 
+.addon-view[type="experiment"] .icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/experimentGeneric.png");
+}
+
 .name-container {
   font-size: 150%;
   margin-bottom: 0;
   font-weight: bold;
   -moz-box-align: end;
   -moz-box-flex: 1;
 }
 
--- a/toolkit/themes/linux/mozapps/jar.mn
+++ b/toolkit/themes/linux/mozapps/jar.mn
@@ -10,22 +10,24 @@ toolkit.jar:
 + skin/classic/mozapps/extensions/category-search.png      (extensions/category-search.png)
 + skin/classic/mozapps/extensions/category-discover.png    (extensions/category-discover.png)
 + skin/classic/mozapps/extensions/category-languages.png   (extensions/localeGeneric.png)
 + skin/classic/mozapps/extensions/category-extensions.png  (extensions/extensionGeneric.png)
 + skin/classic/mozapps/extensions/category-themes.png      (extensions/themeGeneric.png)
 + skin/classic/mozapps/extensions/category-plugins.png     (extensions/category-plugins.png)
 + skin/classic/mozapps/extensions/category-service.png     (extensions/category-service.png)
 + skin/classic/mozapps/extensions/category-dictionaries.png (extensions/category-dictionaries.png)
++ skin/classic/mozapps/extensions/category-experiments.png (extensions/category-experiments.png)
 + skin/classic/mozapps/extensions/category-recent.png      (extensions/category-recent.png)
 + skin/classic/mozapps/extensions/category-available.png   (extensions/category-available.png)
 + skin/classic/mozapps/extensions/extensionGeneric.png     (extensions/extensionGeneric.png)
 + skin/classic/mozapps/extensions/extensionGeneric-16.png  (extensions/extensionGeneric-16.png)
 + skin/classic/mozapps/extensions/dictionaryGeneric.png    (extensions/dictionaryGeneric.png)
 + skin/classic/mozapps/extensions/dictionaryGeneric-16.png (extensions/dictionaryGeneric-16.png)
++ skin/classic/mozapps/extensions/experimentGeneric.png    (extensions/experimentGeneric.png)
 + skin/classic/mozapps/extensions/themeGeneric.png         (extensions/themeGeneric.png)
 + skin/classic/mozapps/extensions/themeGeneric-16.png      (extensions/themeGeneric-16.png)
 + skin/classic/mozapps/extensions/localeGeneric.png        (extensions/localeGeneric.png)
 + skin/classic/mozapps/extensions/newaddon.css             (extensions/newaddon.css)
 + skin/classic/mozapps/extensions/selectAddons.css         (extensions/selectAddons.css)
 + skin/classic/mozapps/passwordmgr/key.png                 (passwordmgr/key-16.png)
 + skin/classic/mozapps/passwordmgr/key-16.png              (passwordmgr/key-16.png)
 + skin/classic/mozapps/passwordmgr/key-64.png              (passwordmgr/key-64.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
--- a/toolkit/themes/osx/mozapps/extensions/extensions.css
+++ b/toolkit/themes/osx/mozapps/extensions/extensions.css
@@ -254,16 +254,19 @@
   list-style-image: url("chrome://mozapps/skin/extensions/category-themes.png");
 }
 #category-plugin > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-plugins.png");
 }
 #category-dictionary > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-dictionaries.png");
 }
+#category-experiment > .category-icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/category-experiments.png");
+}
 #category-availableUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-available.png");
 }
 #category-recentUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-recent.png");
 }
 
 
@@ -478,16 +481,20 @@
 .addon-view[type="plugin"] .icon {
   list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png");
 }
 
 .addon-view[type="dictionary"] .icon {
   list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png");
 }
 
+.addon-view[type="experiment"] .icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/experimentGeneric.png");
+}
+
 .name-container {
   font-size: 150%;
   margin-bottom: 0;
   font-weight: bold;
   color: #000;
   text-shadow: @loweredShadow@;
   -moz-box-align: end;
   -moz-box-flex: 1;
--- a/toolkit/themes/osx/mozapps/jar.mn
+++ b/toolkit/themes/osx/mozapps/jar.mn
@@ -12,25 +12,27 @@ toolkit.jar:
   skin/classic/mozapps/extensions/category-discover.png           (extensions/category-discover.png)
   skin/classic/mozapps/extensions/category-languages.png          (extensions/localeGeneric.png)
   skin/classic/mozapps/extensions/category-searchengines.png      (extensions/category-searchengines.png)
   skin/classic/mozapps/extensions/category-extensions.png         (extensions/extensionGeneric.png)
   skin/classic/mozapps/extensions/category-themes.png             (extensions/themeGeneric.png)
   skin/classic/mozapps/extensions/category-plugins.png            (extensions/category-plugins.png)
   skin/classic/mozapps/extensions/category-service.png            (extensions/category-service.png)
   skin/classic/mozapps/extensions/category-dictionaries.png       (extensions/category-dictionaries.png)
+  skin/classic/mozapps/extensions/category-experiments.png        (extensions/category-experiments.png)
   skin/classic/mozapps/extensions/category-recent.png             (extensions/category-recent.png)
   skin/classic/mozapps/extensions/category-available.png          (extensions/category-available.png)
   skin/classic/mozapps/extensions/discover-logo.png               (extensions/discover-logo.png)
   skin/classic/mozapps/extensions/extensionGeneric.png            (extensions/extensionGeneric.png)
   skin/classic/mozapps/extensions/extensionGeneric-16.png         (extensions/extensionGeneric-16.png)
   skin/classic/mozapps/extensions/themeGeneric.png                (extensions/themeGeneric.png)
   skin/classic/mozapps/extensions/themeGeneric-16.png             (extensions/themeGeneric-16.png)
   skin/classic/mozapps/extensions/dictionaryGeneric.png           (extensions/dictionaryGeneric.png)
   skin/classic/mozapps/extensions/dictionaryGeneric-16.png        (extensions/dictionaryGeneric-16.png)
+  skin/classic/mozapps/extensions/experimentGeneric.png           (extensions/experimentGeneric.png)
   skin/classic/mozapps/extensions/localeGeneric.png               (extensions/localeGeneric.png)
   skin/classic/mozapps/extensions/rating-won.png                  (extensions/rating-won.png)
   skin/classic/mozapps/extensions/rating-not-won.png              (extensions/rating-not-won.png)
   skin/classic/mozapps/extensions/cancel.png                      (extensions/cancel.png)
   skin/classic/mozapps/extensions/utilities.png                   (extensions/utilities.png)
   skin/classic/mozapps/extensions/toolbarbutton-dropmarker.png    (extensions/toolbarbutton-dropmarker.png)
   skin/classic/mozapps/extensions/heart.png                       (extensions/heart.png)
   skin/classic/mozapps/extensions/navigation.png                  (extensions/navigation.png)
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a9d00545ef31fbb7dfb4295be97cfc800f5289ea
GIT binary patch
literal 822
zc$@(?1Ihe}P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800090Nkl<Zc-pPa
zPl%Ou6vpw#X0AD?H-)()jM*P#GAl!f2BI*^&`K=hLXn$RB7_S!K?E8YXyjsMyq5nm
zCsU|p5VUI3M9gwV%*n}0GPERRE@aoSW=`M5x$ts$U)~=wix0f-`JLxE+HzW{y0IN!
z;0H{@+%(Sl+jieo3cYl?qs*iD9JgSJ*d;@@tY4c?%`Z~LVa&j8hi4CYm-1-SsjfVa
zU=lV*CcRUUUifTr)Q{`15>vQ<-!Pr9f8D#4rHv<-W<89*U<F386Dy&h&)-H1^K|<S
zwduK&J#7my0n2d#&qFDE_a|87Hx@yVp8d49Z4j2@D^#Hz=9HuAS&RIZAU!>NukaxL
zg$3WB0^trQFsA~Y_!1WU=QpLNPV@=Gu;6Dbf$%EEP=hkeDOiOa5Ef$+77Rmr;>1H$
z`~wTtL+Hdf3};b;oH@BgcppvN54rWR-)~mc|H;~|uwVqrsNpOO<LHEf4fqv?-B2(B
z3%2U9<NZfr!44>>VH~f8`CT}USr{g<Am+W(f@50y@sp#l;2tPPNBDmnzrfrq)<KbX
zS}>|L$DX+g%eV$5cp0sb9Vp3M3;S2K`sl_MEMo$47AWI#cpJlllH5d?xAee~%{O2f
z=OJf-GA_g16#5c!=fnJlRvxKefn_wI1TT8_cWg?`Oody2=)S|RkHCUe&|LCP3%=8e
zLvJ)-Ic{PMd(aIbnP>LmjCWa%hI&8RwgpzAi8_?1j;Vy+SJd<2n_XzaO3b1Tm8ko!
zm1v?%-3NCHL$Cx*^gsn_zGDd*kd`*y5>}uEORyIzuqUyj<u|2j<DIr$umoqI0%M6C
zyC6sl54_t}!S}Ed(@=q%WqH3bAA;01^j_B8xCAS)4oe_(px0+JCAmv}Lv6ZqXkXNa
zi?9T@Fq61@(YuwU%HaMy%keF2j*NJxB6SQ76zRm<_#1Y6$kO3m%A<J$2g)qN2lx|~
zh+Q&d%lf5(gQ+UmfPMHHKjMnxn;CDuQ-M<a58nSmbTc?5t^fc407*qoM6N<$f}OgC
Au>b%7
--- a/toolkit/themes/windows/mozapps/extensions/extensions.css
+++ b/toolkit/themes/windows/mozapps/extensions/extensions.css
@@ -266,16 +266,19 @@
   list-style-image: url("chrome://mozapps/skin/extensions/category-themes.png");
 }
 #category-plugin > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-plugins.png");
 }
 #category-dictionary > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-dictionaries.png");
 }
+#category-experiment > .category-icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/category-experiments.png");
+}
 #category-availableUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-available.png");
 }
 #category-recentUpdates > .category-icon {
   list-style-image: url("chrome://mozapps/skin/extensions/category-recent.png");
 }
 
 
@@ -480,16 +483,20 @@
 .addon-view[type="plugin"] .icon {
   list-style-image: url("chrome://mozapps/skin/plugins/pluginGeneric.png");
 }
 
 .addon-view[type="dictionary"] .icon {
   list-style-image: url("chrome://mozapps/skin/extensions/dictionaryGeneric.png");
 }
 
+.addon-view[type="experiment"] .icon {
+  list-style-image: url("chrome://mozapps/skin/extensions/experimentGeneric.png");
+}
+
 .name-container {
   font-size: 150%;
   font-weight: bold;
   color: #3F3F3F;
   margin-bottom: 0;
   -moz-box-align: end;
   -moz-box-flex: 1;
 }
--- a/toolkit/themes/windows/mozapps/jar.mn
+++ b/toolkit/themes/windows/mozapps/jar.mn
@@ -21,25 +21,27 @@ toolkit.jar:
         skin/classic/mozapps/extensions/category-discover.png      (extensions/category-discover.png)
         skin/classic/mozapps/extensions/category-languages.png     (extensions/localeGeneric.png)
         skin/classic/mozapps/extensions/category-searchengines.png (extensions/category-searchengines.png)
         skin/classic/mozapps/extensions/category-extensions.png    (extensions/extensionGeneric.png)
         skin/classic/mozapps/extensions/category-themes.png        (extensions/themeGeneric.png)
         skin/classic/mozapps/extensions/category-plugins.png       (extensions/category-plugins.png)
         skin/classic/mozapps/extensions/category-service.png       (extensions/category-service.png)
         skin/classic/mozapps/extensions/category-dictionaries.png  (extensions/category-dictionaries.png)
+        skin/classic/mozapps/extensions/category-experiments.png   (extensions/category-experiments.png)
         skin/classic/mozapps/extensions/category-recent.png        (extensions/category-recent.png)
         skin/classic/mozapps/extensions/category-available.png     (extensions/category-available.png)
         skin/classic/mozapps/extensions/discover-logo.png          (extensions/discover-logo.png)
         skin/classic/mozapps/extensions/extensionGeneric.png       (extensions/extensionGeneric.png)
         skin/classic/mozapps/extensions/extensionGeneric-16.png    (extensions/extensionGeneric-16.png)
         skin/classic/mozapps/extensions/themeGeneric.png           (extensions/themeGeneric.png)
         skin/classic/mozapps/extensions/themeGeneric-16.png        (extensions/themeGeneric-16.png)
         skin/classic/mozapps/extensions/dictionaryGeneric.png      (extensions/dictionaryGeneric.png)
         skin/classic/mozapps/extensions/dictionaryGeneric-16.png   (extensions/dictionaryGeneric-16.png)
+        skin/classic/mozapps/extensions/experimentGeneric.png      (extensions/experimentGeneric.png)
         skin/classic/mozapps/extensions/localeGeneric.png          (extensions/localeGeneric.png)
         skin/classic/mozapps/extensions/rating-won.png             (extensions/rating-won.png)
         skin/classic/mozapps/extensions/rating-not-won.png         (extensions/rating-not-won.png)
         skin/classic/mozapps/extensions/cancel.png                 (extensions/cancel.png)
         skin/classic/mozapps/extensions/utilities.png              (extensions/utilities.png)
         skin/classic/mozapps/extensions/heart.png                  (extensions/heart.png)
         skin/classic/mozapps/extensions/navigation.png             (extensions/navigation.png)
         skin/classic/mozapps/extensions/stripes-warning.png        (extensions/stripes-warning.png)
@@ -102,26 +104,28 @@ toolkit.jar:
         skin/classic/aero/mozapps/extensions/category-discover.png         (extensions/category-discover-aero.png)
         skin/classic/aero/mozapps/extensions/category-languages.png        (extensions/localeGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/category-searchengines.png    (extensions/category-searchengines.png)
         skin/classic/aero/mozapps/extensions/category-extensions.png       (extensions/extensionGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/category-themes.png           (extensions/themeGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/category-plugins.png          (extensions/category-plugins-aero.png)
         skin/classic/aero/mozapps/extensions/category-service.png          (extensions/category-service.png)
         skin/classic/aero/mozapps/extensions/category-dictionaries.png     (extensions/category-dictionaries-aero.png)
+        skin/classic/aero/mozapps/extensions/category-experiments.png      (extensions/category-experiments-aero.png)
         skin/classic/aero/mozapps/extensions/category-recent.png           (extensions/category-recent-aero.png)
         skin/classic/aero/mozapps/extensions/category-available.png        (extensions/category-available-aero.png)
         skin/classic/aero/mozapps/extensions/discover-logo.png             (extensions/discover-logo.png)
         skin/classic/aero/mozapps/extensions/extensionGeneric.png          (extensions/extensionGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/extensionGeneric-16.png       (extensions/extensionGeneric-16-aero.png)
         skin/classic/aero/mozapps/extensions/themeGeneric.png              (extensions/themeGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/themeGeneric-16.png           (extensions/themeGeneric-16-aero.png)
         skin/classic/aero/mozapps/extensions/dictionaryGeneric.png         (extensions/dictionaryGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/dictionaryGeneric-16.png      (extensions/dictionaryGeneric-16-aero.png)
         skin/classic/aero/mozapps/extensions/localeGeneric.png             (extensions/localeGeneric-aero.png)
+        skin/classic/aero/mozapps/extensions/experimentGeneric.png         (extensions/experimentGeneric-aero.png)
         skin/classic/aero/mozapps/extensions/rating-won.png                (extensions/rating-won.png)
         skin/classic/aero/mozapps/extensions/rating-not-won.png            (extensions/rating-not-won.png)
         skin/classic/aero/mozapps/extensions/cancel.png                    (extensions/cancel.png)
         skin/classic/aero/mozapps/extensions/utilities.png                 (extensions/utilities.png)
         skin/classic/aero/mozapps/extensions/heart.png                     (extensions/heart.png)
         skin/classic/aero/mozapps/extensions/navigation.png                (extensions/navigation.png)
         skin/classic/aero/mozapps/extensions/stripes-warning.png           (extensions/stripes-warning.png)
         skin/classic/aero/mozapps/extensions/stripes-error.png             (extensions/stripes-error.png)
--- a/webapprt/prefs.js
+++ b/webapprt/prefs.js
@@ -54,16 +54,18 @@ pref("dom.sysmsg.enabled", true);
 
 // Alarm API
 pref("dom.mozAlarms.enabled", true);
 
 // Disable slow script dialog for apps
 pref("dom.max_script_run_time", 0);
 pref("dom.max_chrome_script_run_time", 0);
 
+// The request URL of the GeoLocation backend
+pref("geo.wifi.uri", "https://location.services.mozilla.com/v1/geolocate?key=%MOZILLA_API_KEY%");
 
 #ifndef RELEASE_BUILD
 // Enable mozPay default provider
 pref("dom.payment.provider.0.name", "Firefox Marketplace");
 pref("dom.payment.provider.0.description", "marketplace.firefox.com");
 pref("dom.payment.provider.0.uri", "https://marketplace.firefox.com/mozpay/?req=");
 pref("dom.payment.provider.0.type", "mozilla/payments/pay/v1");
 pref("dom.payment.provider.0.requestMethod", "GET");
--- a/widget/windows/nsLookAndFeel.cpp
+++ b/widget/windows/nsLookAndFeel.cpp
@@ -364,21 +364,17 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
         break;
     case eIntID_DragThresholdY:
         aResult = ::GetSystemMetrics(SM_CYDRAG) - 1;
         break;
     case eIntID_UseAccessibilityTheme:
         // High contrast is a misnomer under Win32 -- any theme can be used with it, 
         // e.g. normal contrast with large fonts, low contrast, etc.
         // The high contrast flag really means -- use this theme and don't override it.
-        HIGHCONTRAST contrastThemeInfo;
-        contrastThemeInfo.cbSize = sizeof(contrastThemeInfo);
-        ::SystemParametersInfo(SPI_GETHIGHCONTRAST, 0, &contrastThemeInfo, 0);
-
-        aResult = ((contrastThemeInfo.dwFlags & HCF_HIGHCONTRASTON) != 0);
+        aResult = nsUXThemeData::IsHighContrastOn();
         break;
     case eIntID_ScrollArrowStyle:
         aResult = eScrollArrowStyle_Single;
         break;
     case eIntID_ScrollSliderStyle:
         aResult = eScrollThumbStyle_Proportional;
         break;
     case eIntID_TreeOpenDelay:
--- a/widget/windows/nsUXThemeData.cpp
+++ b/widget/windows/nsUXThemeData.cpp
@@ -248,30 +248,37 @@ const THEMELIST knownColors[] = {
   { L"metallic",    WINTHEMECOLOR_METALLIC }
 };
 
 LookAndFeel::WindowsTheme
 nsUXThemeData::sThemeId = LookAndFeel::eWindowsTheme_Generic;
 
 bool
 nsUXThemeData::sIsDefaultWindowsTheme = false;
+bool
+nsUXThemeData::sIsHighContrastOn = false;
 
 // static
 LookAndFeel::WindowsTheme
 nsUXThemeData::GetNativeThemeId()
 {
   return sThemeId;
 }
 
 // static
 bool nsUXThemeData::IsDefaultWindowTheme()
 {
   return sIsDefaultWindowsTheme;
 }
 
+bool nsUXThemeData::IsHighContrastOn()
+{
+  return sIsHighContrastOn;
+}
+
 // static
 bool nsUXThemeData::CheckForCompositor(bool aUpdateCache)
 {
   static BOOL sCachedValue = FALSE;
   if (aUpdateCache && WinUtils::dwmIsCompositionEnabledPtr) {
     WinUtils::dwmIsCompositionEnabledPtr(&sCachedValue);
   }
   return sCachedValue;
@@ -287,16 +294,24 @@ nsUXThemeData::UpdateNativeThemeInfo()
   sIsDefaultWindowsTheme = false;
   sThemeId = LookAndFeel::eWindowsTheme_Generic;
 
   if (!IsAppThemed()) {
     sThemeId = LookAndFeel::eWindowsTheme_Classic;
     return;
   }
 
+  HIGHCONTRAST highContrastInfo;
+  highContrastInfo.cbSize = sizeof(HIGHCONTRAST);
+  if (SystemParametersInfo(SPI_GETHIGHCONTRAST, 0, &highContrastInfo, 0)) {
+    sIsHighContrastOn = ((highContrastInfo.dwFlags & HCF_HIGHCONTRASTON) != 0);
+  } else {
+    sIsHighContrastOn = false;
+  }
+
   WCHAR themeFileName[MAX_PATH + 1];
   WCHAR themeColor[MAX_PATH + 1];
   if (FAILED(GetCurrentThemeName(themeFileName,
                                  MAX_PATH,
                                  themeColor,
                                  MAX_PATH,
                                  nullptr, 0))) {
     sThemeId = LookAndFeel::eWindowsTheme_Classic;
@@ -312,19 +327,26 @@ nsUXThemeData::UpdateNativeThemeInfo()
       theme = (WindowsTheme)knownThemes[i].type;
       break;
     }
   }
 
   if (theme == WINTHEME_UNRECOGNIZED)
     return;
 
-  if (theme == WINTHEME_AERO || theme == WINTHEME_AERO_LITE || theme == WINTHEME_LUNA)
+  // We're using the default theme if we're using any of Aero, Aero Lite, or
+  // luna. However, on Win8, GetCurrentThemeName (see above) returns
+  // AeroLite.msstyles for the 4 builtin highcontrast themes as well. Those
+  // themes "don't count" as default themes, so we specifically check for high
+  // contrast mode in that situation.
+  if (!(IsWin8OrLater() && sIsHighContrastOn) &&
+      (theme == WINTHEME_AERO || theme == WINTHEME_AERO_LITE || theme == WINTHEME_LUNA)) {
     sIsDefaultWindowsTheme = true;
-  
+  }
+
   if (theme != WINTHEME_LUNA) {
     switch(theme) {
       case WINTHEME_AERO:
         sThemeId = LookAndFeel::eWindowsTheme_Aero;
         return;
       case WINTHEME_AERO_LITE:
         sThemeId = LookAndFeel::eWindowsTheme_AeroLite;
         return;
--- a/widget/windows/nsUXThemeData.h
+++ b/widget/windows/nsUXThemeData.h
@@ -87,30 +87,32 @@ class nsUXThemeData {
 public:
   static const wchar_t kThemeLibraryName[];
   static bool sFlatMenus;
   static bool sTitlebarInfoPopulatedAero;
   static bool sTitlebarInfoPopulatedThemed;
   static SIZE sCommandButtons[4];
   static mozilla::LookAndFeel::WindowsTheme sThemeId;
   static bool sIsDefaultWindowsTheme;
+  static bool sIsHighContrastOn;
 
   static void Initialize();
   static void Teardown();
   static void Invalidate();
   static HANDLE GetTheme(nsUXThemeClass cls);
   static HMODULE GetThemeDLL();
 
   // nsWindow calls this to update desktop settings info
   static void InitTitlebarInfo();
   static void UpdateTitlebarInfo(HWND aWnd);
 
   static void UpdateNativeThemeInfo();
   static mozilla::LookAndFeel::WindowsTheme GetNativeThemeId();
   static bool IsDefaultWindowTheme();
+  static bool IsHighContrastOn();
 
   // This method returns the cached compositor state. Most
   // callers should call without the argument. The cache
   // should be modified only when the application receives
   // WM_DWMCOMPOSITIONCHANGED. This rule prevents inconsistent
   // results for two or more calls which check the state during
   // composition transition.
   static bool CheckForCompositor(bool aUpdateCache = false);