Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Wed, 12 Mar 2014 20:01:45 -0700
changeset 190488 46041cc216fdac8bdced304e84a6acd859acbd83
parent 190462 c12c92db5588a0fb6083486f7bcc1ed55f0b86f5 (current diff)
parent 190487 d2f55c8689ff9cccb2ea59fea382c59887f14a76 (diff)
child 190504 ad954f1cf3db3b89455265e45ffe890a3b9a59f8
child 190537 9b1812a7852ea59b33e685b8cba4e0f320bf599d
child 190550 782b35c99abe8168bff2d5b54a056fd741a1abed
push id3503
push userraliiev@mozilla.com
push dateMon, 28 Apr 2014 18:51:11 +0000
treeherdermozilla-beta@c95ac01e332e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
46041cc216fd / 30.0a1 / 20140313030202 / files
nightly linux64
46041cc216fd / 30.0a1 / 20140313030202 / files
nightly mac
46041cc216fd / 30.0a1 / 20140313030202 / files
nightly win32
46041cc216fd / 30.0a1 / 20140313030202 / files
nightly win64
46041cc216fd / 30.0a1 / 20140313030202 / 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/app/mobile.js
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -288,17 +288,17 @@
                 accesskey="&selectAllCmd.accesskey;"
                 command="cmd_selectAll"/>
       <menuseparator id="context-sep-selectall"/>
       <menuitem id="context-keywordfield"
                 label="&keywordfield.label;"
                 accesskey="&keywordfield.accesskey;"
                 oncommand="AddKeywordForSearchField();"/>
       <menuitem id="context-searchselect"
-                oncommand="BrowserSearch.loadSearchFromContext(getBrowserSelection());"/>
+                oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
       <menuitem id="context-shareselect"
                 label="&shareSelectCmd.label;"
                 accesskey="&shareSelectCmd.accesskey;"
                 oncommand="gContextMenu.shareSelect(getBrowserSelection());"/>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
           <menuitem id="context-showonlythisframe"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -27,17 +27,17 @@ nsContextMenu.prototype = {
     }
 
     this.isFrameImage = document.getElementById("isFrameImage");
     this.ellipsis = "\u2026";
     try {
       this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
                                                    Ci.nsIPrefLocalizedString).data;
     } catch (e) { }
-    this.isTextSelected = this.isTextSelection();
+
     this.isContentSelected = this.isContentSelection();
     this.onPlainTextLink = false;
 
     // Initialize (disable/remove) menu items.
     this.initItems();
   },
 
   hiding: function CM_hiding() {
@@ -263,41 +263,44 @@ nsContextMenu.prototype = {
     document.getElementById("context-viewbgimage")
             .disabled = !this.hasBGImage;
 
     this.showItem("context-viewimageinfo", this.onImage);
     this.showItem("context-viewimagedesc", this.onImage && this.imageDescURL !== "");
   },
 
   initMiscItems: function CM_initMiscItems() {
-    var isTextSelected = this.isTextSelected;
-
     // Use "Bookmark This Link" if on a link.
     this.showItem("context-bookmarkpage",
                   !(this.isContentSelected || this.onTextInput || this.onLink ||
                     this.onImage || this.onVideo || this.onAudio || this.onSocial));
     this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
                                            !this.onSocial) || this.onPlainTextLink);
-    this.showItem("context-searchselect", isTextSelected);
     this.showItem("context-keywordfield",
                   this.onTextInput && this.onKeywordField);
     this.showItem("frame", this.inFrame);
 
+    let showSearchSelect = (this.isTextSelected || this.onLink) && !this.onImage;
+    this.showItem("context-searchselect", showSearchSelect);
+    if (showSearchSelect) {
+      this.formatSearchContextItem();
+    }
+
     // srcdoc cannot be opened separately due to concerns about web
     // content with about:srcdoc in location bar masquerading as trusted
     // chrome/addon content.
     // No need to also test for this.inFrame as this is checked in the parent
     // submenu.
     this.showItem("context-showonlythisframe", !this.inSrcdocFrame);
     this.showItem("context-openframeintab", !this.inSrcdocFrame);
     this.showItem("context-openframe", !this.inSrcdocFrame);
     this.showItem("context-bookmarkframe", !this.inSrcdocFrame);
     this.showItem("open-frame-sep", !this.inSrcdocFrame);
 
-    this.showItem("frame-sep", this.inFrame && isTextSelected);
+    this.showItem("frame-sep", this.inFrame && this.isTextSelected);
 
     // Hide menu entries for images, show otherwise
     if (this.inFrame) {
       if (mimeTypeIsTextBased(this.target.ownerDocument.contentType))
         this.isFrameImage.removeAttribute('hidden');
       else
         this.isFrameImage.setAttribute('hidden', 'true');
     }
@@ -535,16 +538,18 @@ nsContextMenu.prototype = {
     this.inSrcdocFrame     = false;
     this.inSyntheticDoc    = false;
     this.hasBGImage        = false;
     this.bgImageURL        = "";
     this.onEditableArea    = false;
     this.isDesignMode      = false;
     this.onCTPPlugin       = false;
     this.canSpellCheck     = false;
+    this.textSelected      = getBrowserSelection();
+    this.isTextSelected    = this.textSelected.length != 0;
 
     // Remember the node that was clicked.
     this.target = aNode;
 
     // If this is a remote context menu event, use the information from
     // gContextMenuContentData instead.
     if (this.isRemote) {
       this.browser = gContextMenuContentData.browser;
@@ -1437,49 +1442,16 @@ nsContextMenu.prototype = {
         if (!text || !text.match(/\S/))
           text = this.linkURL;
       }
     }
 
     return text;
   },
 
-  // Get selected text. Only display the first 15 chars.
-  isTextSelection: function() {
-    // Get 16 characters, so that we can trim the selection if it's greater
-    // than 15 chars
-    var selectedText = getBrowserSelection(16);
-
-    if (!selectedText)
-      return false;
-
-    if (selectedText.length > 15)
-      selectedText = selectedText.substr(0,15) + this.ellipsis;
-
-    // Use the current engine if the search bar is visible, the default
-    // engine otherwise.
-    var engineName = "";
-    var ss = Cc["@mozilla.org/browser/search-service;1"].
-             getService(Ci.nsIBrowserSearchService);
-    if (isElementVisible(BrowserSearch.searchBar))
-      engineName = ss.currentEngine.name;
-    else
-      engineName = ss.defaultEngine.name;
-
-    // format "Search <engine> for <selection>" string to show in menu
-    var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
-                                                        [engineName,
-                                                         selectedText]);
-    document.getElementById("context-searchselect").label = menuLabel;
-    document.getElementById("context-searchselect").accessKey =
-             gNavigatorBundle.getString("contextMenuSearch.accesskey"); 
-
-    return true;
-  },
-
   // Returns true if anything is selected.
   isContentSelection: function() {
     return !document.commandDispatcher.focusedWindow.getSelection().isCollapsed;
   },
 
   toString: function () {
     return "contextMenu.target     = " + this.target + "\n" +
            "contextMenu.onImage    = " + this.onImage + "\n" +
@@ -1683,10 +1655,39 @@ nsContextMenu.prototype = {
                     getService(Ci.nsIClipboardHelper);
     clipboard.copyString(this.mediaURL, document);
   },
 
   get imageURL() {
     if (this.onImage)
       return this.mediaURL;
     return "";
+  },
+
+  // Formats the 'Search <engine> for "<selection or link text>"' context menu.
+  formatSearchContextItem: function() {
+    var menuItem = document.getElementById("context-searchselect");
+    var selectedText = this.onLink ? this.linkText() : this.textSelected;
+
+    // Store searchTerms in context menu item so we know what to search onclick
+    menuItem.searchTerms = selectedText;
+
+    if (selectedText.length > 15)
+      selectedText = selectedText.substr(0,15) + this.ellipsis;
+
+    // Use the current engine if the search bar is visible, the default
+    // engine otherwise.
+    var engineName = "";
+    var ss = Cc["@mozilla.org/browser/search-service;1"].
+             getService(Ci.nsIBrowserSearchService);
+    if (isElementVisible(BrowserSearch.searchBar))
+      engineName = ss.currentEngine.name;
+    else
+      engineName = ss.defaultEngine.name;
+
+    // format "Search <engine> for <selection>" string to show in menu
+    var menuLabel = gNavigatorBundle.getFormattedString("contextMenuSearch",
+                                                        [engineName,
+                                                         selectedText]);
+    menuItem.label = menuLabel;
+    menuItem.accessKey = gNavigatorBundle.getString("contextMenuSearch.accesskey");
   }
 };
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -4,16 +4,17 @@ support-files =
   accounts_testRemoteCommands.html
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
   authenticate.sjs
   browser_bug479408_sample.html
   browser_bug678392-1.html
   browser_bug678392-2.html
+  browser_bug970746.xhtml
   browser_registerProtocolHandler_notification.html
   browser_star_hsts.sjs
   browser_tab_dragdrop2_frame1.xul
   bug564387.html
   bug564387_video1.ogv
   bug564387_video1.ogv^headers^
   bug592338.html
   bug792517-2.html
@@ -197,16 +198,17 @@ skip-if = os == "mac" # Intermittent fai
 [browser_bug817947.js]
 [browser_bug822367.js]
 [browser_bug832435.js]
 [browser_bug839103.js]
 [browser_bug880101.js]
 [browser_bug882977.js]
 [browser_bug902156.js]
 [browser_bug906190.js]
+[browser_bug970746.js]
 [browser_canonizeURL.js]
 [browser_contentAreaClick.js]
 [browser_contextSearchTabPosition.js]
 skip-if = os == "mac" # bug 967013, bug 926729
 [browser_ctrlTab.js]
 [browser_customize_popupNotification.js]
 [browser_datareporting_notification.js]
 run-if = datareporting
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug970746.js
@@ -0,0 +1,104 @@
+/* Make sure context menu includes option to search hyperlink text on search engine */
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+
+    let doc = gBrowser.contentDocument;
+    let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+    let ellipsis = "\u2026";
+
+    // Tests if the "Search <engine> for '<some terms>'" context menu item is shown for the
+    // given query string of an element. Tests to make sure label includes the proper search terms.
+    //
+    // Options:
+    // 
+    //   id: The id of the element to test.
+    //   isSelected: Flag to enable selection (text hilight) the contents of the element
+    //   shouldBeShown: The display state of the menu item
+    //   expectedLabelContents: The menu item label should contain a portion of this string.
+    //                          Will only be tested if shouldBeShown is true.
+
+    let testElement = function(opts) {
+      let element = doc.getElementById(opts.id);
+      document.popupNode = element;
+
+      let selection = content.getSelection();
+      selection.removeAllRanges();
+
+      if(opts.isSelected) {
+        selection.selectAllChildren(element);
+      }
+
+      let contextMenu = new nsContextMenu(contentAreaContextMenu);
+      let menuItem = document.getElementById("context-searchselect");
+
+      is(document.getElementById("context-searchselect").hidden, !opts.shouldBeShown, "search context menu item is shown for  '#" + opts.id + "' and selected is '" + opts.isSelected + "'");
+
+      if(opts.shouldBeShown) {
+        ok(menuItem.label.contains(opts.expectedLabelContents), "Menu item text '" + menuItem.label  + "' contains the correct search terms '" + opts.expectedLabelContents  + "'");
+      }
+    }
+
+    testElement({
+      id: "link",
+      isSelected: true,
+      shouldBeShown: true,
+      expectedLabelContents: "I'm a link!",
+    });
+    testElement({
+      id: "link",
+      isSelected: false,
+      shouldBeShown: true,
+      expectedLabelContents: "I'm a link!",
+    });
+
+    testElement({
+      id: "longLink",
+      isSelected: true,
+      shouldBeShown: true,
+      expectedLabelContents: "I'm a really lo" + ellipsis,
+    });
+    testElement({
+      id: "longLink",
+      isSelected: false,
+      shouldBeShown: true,
+      expectedLabelContents: "I'm a really lo" + ellipsis,
+    });
+
+    testElement({
+      id: "plainText",
+      isSelected: true,
+      shouldBeShown: true,
+      expectedLabelContents: "Right clicking " + ellipsis,
+    });
+    testElement({
+      id: "plainText",
+      isSelected: false,
+      shouldBeShown: false,
+    });
+
+    testElement({
+      id: "mixedContent",
+      isSelected: true,
+      shouldBeShown: true,
+      expectedLabelContents: "I'm some text, " + ellipsis,
+    });
+    testElement({
+      id: "mixedContent",
+      isSelected: false,
+      shouldBeShown: false,
+    });
+
+    // cleanup
+    document.popupNode = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }, true);
+
+  content.location = "http://mochi.test:8888/browser/browser/base/content/test/general/browser_bug970746.xhtml";
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_bug970746.xhtml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<body>
+		<a href="http://mozilla.org" id="link">I'm a link!</a>
+		<a href="http://mozilla.org" id="longLink">I'm a really long link and I should be truncated.</a>
+
+		<span id="plainText">
+			Right clicking me when I'm selected should show the menu item.
+		</span>
+		<span id="mixedContent">
+			I'm some text, and <a href="http://mozilla.org">I'm a link!</a>
+		</span>
+	</body>
+</html>
\ No newline at end of file
--- a/browser/base/content/test/general/test_contextmenu.html
+++ b/browser/base/content/test/general/test_contextmenu.html
@@ -113,34 +113,38 @@ function runTest(testNum) {
         // Context menu for text link
         if (perWindowPrivateBrowsing) {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "context-openlinkprivate", true,
                             "---",                   null,
                             "context-bookmarklink",  true,
                             "context-savelink",      true,
-                            "context-copylink",      true
+                            "context-copylink",      true,
+                            "context-searchselect",  true
                            ].concat(inspectItems));
         } else {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "---",                   null,
                             "context-bookmarklink",  true,
                             "context-savelink",      true,
-                            "context-copylink",      true
+                            "context-copylink",      true,
+                            "context-searchselect",  true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         openContextMenuFor(mailto); // Invoke context menu for next test.
         break;
 
     case 4:
         // Context menu for text mailto-link
-        checkContextMenu(["context-copyemail", true].concat(inspectItems));
+        checkContextMenu(["context-copyemail", true,
+                          "context-searchselect", true
+                        ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(img); // Invoke context menu for next test.
         break;
 
     case 5:
         // Context menu for an image
         checkContextMenu(["context-viewimage",            true,
                           "context-copyimage-contents",   true,
--- a/browser/components/distribution.js
+++ b/browser/components/distribution.js
@@ -162,17 +162,17 @@ DistributionCustomizer.prototype = {
           index = prependIndex++;
 
         // Don't bother updating the livemark contents on creation.
         PlacesUtils.livemarks.addLivemark({ title: items[iid]["title"]
                                           , parentId: parentId
                                           , index: index
                                           , feedURI: this._makeURI(items[iid]["feedLink"])
                                           , siteURI: this._makeURI(items[iid]["siteLink"])
-                                          });
+                                          }).then(null, Cu.reportError);
         break;
 
       case "bookmark":
       default:
         if (iid < defaultItemId)
           index = prependIndex++;
 
         newId = PlacesUtils.bookmarks.insertBookmark(parentId,
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -246,34 +246,30 @@ var BookmarkPropertiesPanel = {
           // Load In Sidebar
           this._loadInSidebar = PlacesUtils.annotations
                                            .itemHasAnnotation(this._itemId,
                                                               PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
           break;
 
         case "folder":
           this._itemType = BOOKMARK_FOLDER;
-          PlacesUtils.livemarks.getLivemark(
-            { id: this._itemId },
-            (function (aStatus, aLivemark) {
-              if (Components.isSuccessCode(aStatus)) {
-                this._itemType = LIVEMARK_CONTAINER;
-                this._feedURI = aLivemark.feedURI;
-                this._siteURI = aLivemark.siteURI;
-                this._fillEditProperties();
+          PlacesUtils.livemarks.getLivemark({ id: this._itemId })
+            .then(aLivemark => {
+              this._itemType = LIVEMARK_CONTAINER;
+              this._feedURI = aLivemark.feedURI;
+              this._siteURI = aLivemark.siteURI;
+              this._fillEditProperties();
 
-                let acceptButton = document.documentElement.getButton("accept");
-                acceptButton.disabled = !this._inputIsValid();
+              let acceptButton = document.documentElement.getButton("accept");
+              acceptButton.disabled = !this._inputIsValid();
 
-                let newHeight = window.outerHeight +
-                                this._element("descriptionField").boxObject.height;
-                window.resizeTo(window.outerWidth, newHeight);
-              }
-            }).bind(this)
-          );
+              let newHeight = window.outerHeight +
+                              this._element("descriptionField").boxObject.height;
+              window.resizeTo(window.outerWidth, newHeight);
+            }, () => undefined);
 
           break;
       }
 
       // Description
       if (PlacesUtils.annotations
                      .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) {
         this._description = PlacesUtils.annotations
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -312,31 +312,27 @@ PlacesViewBase.prototype = {
           if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
             element.setAttribute("tagContainer", "true");
           else if (PlacesUtils.nodeIsDay(aPlacesNode))
             element.setAttribute("dayContainer", "true");
           else if (PlacesUtils.nodeIsHost(aPlacesNode))
             element.setAttribute("hostContainer", "true");
         }
         else if (itemId != -1) {
-          PlacesUtils.livemarks.getLivemark(
-            { id: itemId },
-            function (aStatus, aLivemark) {
-              if (Components.isSuccessCode(aStatus)) {
-                element.setAttribute("livemark", "true");
+          PlacesUtils.livemarks.getLivemark({ id: itemId })
+            .then(aLivemark => {
+              element.setAttribute("livemark", "true");
 #ifdef XP_MACOSX
-                // OS X native menubar doesn't track list-style-images since
-                // it doesn't have a frame (bug 733415).  Thus enforce updating.
-                element.setAttribute("image", "");
-                element.removeAttribute("image");
+              // OS X native menubar doesn't track list-style-images since
+              // it doesn't have a frame (bug 733415).  Thus enforce updating.
+              element.setAttribute("image", "");
+              element.removeAttribute("image");
 #endif
-                this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-              }
-            }.bind(this)
-          );
+              this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            }, () => undefined);
         }
 
         let popup = document.createElement("menupopup");
         popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
 
         if (!this._nativeView) {
           popup.setAttribute("placespopup", "true");
         }
@@ -504,26 +500,22 @@ PlacesViewBase.prototype = {
 #ifdef XP_MACOSX
         // OS X native menubar doesn't track list-style-images since
         // it doesn't have a frame (bug 733415).  Thus enforce updating.
         menu.setAttribute("image", "");
         menu.removeAttribute("image");
 #endif
       }
 
-      PlacesUtils.livemarks.getLivemark(
-        { id: aPlacesNode.itemId },
-        function (aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            // Controller will use this to build the meta data for the node.
-            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-            this.invalidateContainer(aPlacesNode);
-          }
-        }.bind(this)
-      );
+      PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+        .then(aLivemark => {
+          // Controller will use this to build the meta data for the node.
+          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+          this.invalidateContainer(aPlacesNode);
+        }, () => undefined);
     }
   },
 
   nodeTitleChanged:
   function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // There's no UI representation for the root node, thus there's
@@ -642,69 +634,65 @@ PlacesViewBase.prototype = {
       this.invalidateContainer(aPlacesNode);
 
       if (PlacesUtils.nodeIsFolder(aPlacesNode)) {
         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
         if (queryOptions.excludeItems) {
           return;
         }
 
-        PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId },
-          function (aStatus, aLivemark) {
-            if (Components.isSuccessCode(aStatus)) {
-              let shouldInvalidate =
-                !this.controller.hasCachedLivemarkInfo(aPlacesNode);
-              this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-              if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
-                aLivemark.registerForUpdates(aPlacesNode, this);
-                // Prioritize the current livemark.
-                aLivemark.reload();
-                PlacesUtils.livemarks.reloadLivemarks();
-                if (shouldInvalidate)
-                  this.invalidateContainer(aPlacesNode);
-              }
-              else {
-                aLivemark.unregisterForUpdates(aPlacesNode);
-              }
+        PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+          .then(aLivemark => {
+            let shouldInvalidate =
+              !this.controller.hasCachedLivemarkInfo(aPlacesNode);
+            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+              aLivemark.registerForUpdates(aPlacesNode, this);
+              // Prioritize the current livemark.
+              aLivemark.reload();
+              PlacesUtils.livemarks.reloadLivemarks();
+              if (shouldInvalidate)
+                this.invalidateContainer(aPlacesNode);
             }
-          }.bind(this)
-        );
+            else {
+              aLivemark.unregisterForUpdates(aPlacesNode);
+            }
+          }, () => undefined);
       }
     }
   },
 
   _populateLivemarkPopup: function PVB__populateLivemarkPopup(aPopup)
   {
     this._setLivemarkSiteURIMenuItem(aPopup);
     // Show the loading status only if there are no entries yet.
     if (aPopup._startMarker.nextSibling == aPopup._endMarker)
       this._setLivemarkStatusMenuItem(aPopup, Ci.mozILivemark.STATUS_LOADING);
 
-    PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId },
-      function (aStatus, aLivemark) {
+    PlacesUtils.livemarks.getLivemark({ id: aPopup._placesNode.itemId })
+      .then(aLivemark => {
         let placesNode = aPopup._placesNode;
-        if (!Components.isSuccessCode(aStatus) || !placesNode.containerOpen)
+        if (!placesNode.containerOpen)
           return;
 
         if (aLivemark.status != Ci.mozILivemark.STATUS_LOADING)
           this._setLivemarkStatusMenuItem(aPopup, aLivemark.status);
         this._cleanPopup(aPopup,
           this._nativeView && aPopup.parentNode.hasAttribute("open"));
 
         let children = aLivemark.getNodesForContainer(placesNode);
         for (let i = 0; i < children.length; i++) {
           let child = children[i];
           this.nodeInserted(placesNode, child, i);
           if (child.accessCount)
             this._getDOMNodeForPlacesNode(child).setAttribute("visited", true);
           else
             this._getDOMNodeForPlacesNode(child).removeAttribute("visited");
         }
-      }.bind(this)
-    );
+      }, Components.utils.reportError);
   },
 
   invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
     elt._built = false;
 
     // If the menupopup is open we should live-update it.
     if (elt.parentNode.open)
@@ -1003,25 +991,21 @@ PlacesToolbar.prototype = {
         button.setAttribute("container", "true");
 
         if (PlacesUtils.nodeIsQuery(aChild)) {
           button.setAttribute("query", "true");
           if (PlacesUtils.nodeIsTagQuery(aChild))
             button.setAttribute("tagContainer", "true");
         }
         else if (PlacesUtils.nodeIsFolder(aChild)) {
-          PlacesUtils.livemarks.getLivemark(
-            { id: aChild.itemId },
-            function (aStatus, aLivemark) {
-              if (Components.isSuccessCode(aStatus)) {
-                button.setAttribute("livemark", "true");
-                this.controller.cacheLivemarkInfo(aChild, aLivemark);
-              }
-            }.bind(this)
-          );
+          PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+            .then(aLivemark => {
+              button.setAttribute("livemark", "true");
+              this.controller.cacheLivemarkInfo(aChild, aLivemark);
+            }, () => undefined);
         }
 
         let popup = document.createElement("menupopup");
         popup.setAttribute("placespopup", "true");
         button.appendChild(popup);
         popup._placesNode = PlacesUtils.asContainer(aChild);
         popup.setAttribute("context", "placesContext");
 
@@ -1263,25 +1247,21 @@ PlacesToolbar.prototype = {
 
     if (elt.parentNode == this._rootElt) {
       // Node is on the toolbar.
 
       // All livemarks have a feedURI, so use it as our indicator.
       if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
         elt.setAttribute("livemark", true);
 
-        PlacesUtils.livemarks.getLivemark(
-          { id: aPlacesNode.itemId },
-          function (aStatus, aLivemark) {
-            if (Components.isSuccessCode(aStatus)) {
-              this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-              this.invalidateContainer(aPlacesNode);
-            }
-          }.bind(this)
-        );
+        PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+          .then(aLivemark => {
+            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+            this.invalidateContainer(aPlacesNode);
+          }, Components.utils.reportError);
       }
     }
     else {
       // Node is in a submenu.
       PlacesViewBase.prototype.nodeAnnotationChanged.apply(this, arguments);
     }
   },
 
@@ -1835,25 +1815,21 @@ PlacesPanelMenuView.prototype = {
         button.setAttribute("container", "true");
 
         if (PlacesUtils.nodeIsQuery(aChild)) {
           button.setAttribute("query", "true");
           if (PlacesUtils.nodeIsTagQuery(aChild))
             button.setAttribute("tagContainer", "true");
         }
         else if (PlacesUtils.nodeIsFolder(aChild)) {
-          PlacesUtils.livemarks.getLivemark(
-            { id: aChild.itemId },
-            function (aStatus, aLivemark) {
-              if (Components.isSuccessCode(aStatus)) {
-                button.setAttribute("livemark", "true");
-                this.controller.cacheLivemarkInfo(aChild, aLivemark);
-              }
-            }.bind(this)
-          );
+          PlacesUtils.livemarks.getLivemark({ id: aChild.itemId })
+            .then(aLivemark => {
+              button.setAttribute("livemark", "true");
+              this.controller.cacheLivemarkInfo(aChild, aLivemark);
+            }, () => undefined);
         }
       }
       else if (PlacesUtils.nodeIsURI(aChild)) {
         button.setAttribute("scheme",
                             PlacesUIUtils.guessUrlSchemeForUI(aChild.uri));
       }
     }
 
@@ -1907,25 +1883,21 @@ PlacesPanelMenuView.prototype = {
 
     if (elt.parentNode != this._rootElt)
       return;
 
     // All livemarks have a feedURI, so use it as our indicator.
     if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
       elt.setAttribute("livemark", true);
 
-      PlacesUtils.livemarks.getLivemark(
-        { id: aPlacesNode.itemId },
-        function (aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
-            this.invalidateContainer(aPlacesNode);
-          }
-        }.bind(this)
-      );
+      PlacesUtils.livemarks.getLivemark({ id: aPlacesNode.itemId })
+        .then(aLivemark => {
+          this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
+          this.invalidateContainer(aPlacesNode);
+        }, Components.utils.reportError);
     }
   },
 
   nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
     let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
 
     // There's no UI representation for the root node.
     if (elt == this._rootElt)
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -689,24 +689,20 @@ PlacesController.prototype = {
 
   /**
    * Reloads the selected livemark if any.
    */
   reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
     var selectedNode = this._view.selectedNode;
     if (selectedNode) {
       let itemId = selectedNode.itemId;
-      PlacesUtils.livemarks.getLivemark(
-        { id: itemId },
-        (function(aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            aLivemark.reload(true);
-          }
-        }).bind(this)
-      );
+      PlacesUtils.livemarks.getLivemark({ id: itemId })
+        .then(aLivemark => {
+          aLivemark.reload(true);
+        }, Components.utils.reportError);
     }
   },
 
   /**
    * Opens the links in the selected folder, or the selected links in new tabs.
    */
   openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
     var node = this._view.selectedNode;
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -142,27 +142,23 @@ var gEditItemOverlay = {
                                        .getKeywordForBookmark(this._itemId));
         this._element("loadInSidebarCheckbox").checked =
           PlacesUtils.annotations.itemHasAnnotation(this._itemId,
                                                     PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
       }
       else {
         this._uri = null;
         this._isLivemark = false;
-        PlacesUtils.livemarks.getLivemark(
-          {id: this._itemId },
-          (function (aStatus, aLivemark) {
-            if (Components.isSuccessCode(aStatus)) {
-              this._isLivemark = true;
-              this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
-              this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
-              this._showHideRows();
-            }
-          }).bind(this)
-        );
+        PlacesUtils.livemarks.getLivemark({id: this._itemId })
+          .then(aLivemark => {
+            this._isLivemark = true;
+            this._initTextField("feedLocationField", aLivemark.feedURI.spec, true);
+            this._initTextField("siteLocationField", aLivemark.siteURI ? aLivemark.siteURI.spec : "", true);
+            this._showHideRows();
+          }, () => undefined);
       }
 
       // folder picker
       this._initFolderMenuList(containerId);
 
       // description field
       this._initTextField("descriptionField", 
                           PlacesUIUtils.getItemDescription(this._itemId));
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -321,20 +321,23 @@ var PlacesOrganizer = {
     if (view) {
       let selectedNodes = view.selectedNode ?
                           [view.selectedNode] : view.selectedNodes;
       this._fillDetailsPane(selectedNodes);
     }
   },
 
   openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
-    if (aContainer.itemId != -1)
+    if (aContainer.itemId != -1) {
+      PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
       this._places.selectItems([aContainer.itemId], false);
-    else if (PlacesUtils.nodeIsQuery(aContainer))
+    }
+    else if (PlacesUtils.nodeIsQuery(aContainer)) {
       this._places.selectPlaceURI(aContainer.uri);
+    }
   },
 
   /**
    * Returns the options associated with the query currently loaded in the
    * main places pane.
    */
   getCurrentOptions: function PO_getCurrentOptions() {
     return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
--- a/browser/components/places/content/treeView.js
+++ b/browser/components/places/content/treeView.js
@@ -781,29 +781,29 @@ PlacesTreeView.prototype = {
       let lastModifiedColumn =
         this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
       if (lastModifiedColumn && !lastModifiedColumn.hidden)
         this._tree.invalidateCell(row, lastModifiedColumn);
     }
   },
 
   _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) {
-    PlacesUtils.livemarks.getLivemark({ id: aNode.itemId },
-      function (aStatus, aLivemark) {
+    PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+      .then(aLivemark => {
         let placesNode = aNode;
         // Need to check containerOpen since getLivemark is async.
-        if (!Components.isSuccessCode(aStatus) || !placesNode.containerOpen)
+        if (!placesNode.containerOpen)
           return;
 
         let children = aLivemark.getNodesForContainer(placesNode);
         for (let i = 0; i < children.length; i++) {
           let child = children[i];
           this.nodeInserted(placesNode, child, i);
         }
-      }.bind(this));
+      }, Components.utils.reportError);
   },
 
   nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   },
 
   nodeURIChanged: function PTV_nodeURIChanged(aNode, aNewURI) {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
@@ -842,29 +842,25 @@ PlacesTreeView.prototype = {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD);
   },
 
   nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
     if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
       this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
     }
     else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
-      PlacesUtils.livemarks.getLivemark(
-        { id: aNode.itemId },
-        function (aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            this._controller.cacheLivemarkInfo(aNode, aLivemark);
-            let properties = this._cellProperties.get(aNode);
-            this._cellProperties.set(aNode, properties += " livemark ");
+      PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+        .then(aLivemark => {
+          this._controller.cacheLivemarkInfo(aNode, aLivemark);
+          let properties = this._cellProperties.get(aNode);
+          this._cellProperties.set(aNode, properties += " livemark ");
 
-            // The livemark attribute is set as a cell property on the title cell.
-            this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
-          }
-        }.bind(this)
-      );
+          // The livemark attribute is set as a cell property on the title cell.
+          this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
+        }, Components.utils.reportError);
     }
   },
 
   nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) {
     this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
   },
 
   nodeLastModifiedChanged:
@@ -878,36 +874,33 @@ PlacesTreeView.prototype = {
 
     if (PlacesUtils.nodeIsFolder(aNode) ||
         (this._flatList && aNode == this._rootNode)) {
       let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
       if (queryOptions.excludeItems) {
         return;
       }
 
-      PlacesUtils.livemarks.getLivemark({ id: aNode.itemId },
-        function (aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            let shouldInvalidate = 
-              !this._controller.hasCachedLivemarkInfo(aNode);
-            this._controller.cacheLivemarkInfo(aNode, aLivemark);
-            if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
-              aLivemark.registerForUpdates(aNode, this);
-              // Prioritize the current livemark.
-              aLivemark.reload();
-              PlacesUtils.livemarks.reloadLivemarks();
-              if (shouldInvalidate)
-                this.invalidateContainer(aNode);
-            }
-            else {
-              aLivemark.unregisterForUpdates(aNode);
-            }
+      PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
+        .then(aLivemark => {
+          let shouldInvalidate = 
+            !this._controller.hasCachedLivemarkInfo(aNode);
+          this._controller.cacheLivemarkInfo(aNode, aLivemark);
+          if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
+            aLivemark.registerForUpdates(aNode, this);
+            // Prioritize the current livemark.
+            aLivemark.reload();
+            PlacesUtils.livemarks.reloadLivemarks();
+            if (shouldInvalidate)
+              this.invalidateContainer(aNode);
           }
-        }.bind(this)
-      );
+          else {
+            aLivemark.unregisterForUpdates(aNode);
+          }
+        }, () => undefined);
     }
   },
 
   invalidateContainer: function PTV_invalidateContainer(aContainer) {
     NS_ASSERT(this._result, "Need to have a result to update");
     if (!this._tree)
       return;
 
@@ -1169,27 +1162,23 @@ PlacesTreeView.prototype = {
             properties += " hostContainer";
         }
         else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
                  nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
           if (this._controller.hasCachedLivemarkInfo(node)) {
             properties += " livemark";
           }
           else {
-            PlacesUtils.livemarks.getLivemark(
-              { id: node.itemId },
-              function (aStatus, aLivemark) {
-                if (Components.isSuccessCode(aStatus)) {
-                  this._controller.cacheLivemarkInfo(node, aLivemark);
-                  properties += " livemark";
-                  // The livemark attribute is set as a cell property on the title cell.
-                  this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
-                }
-              }.bind(this)
-            );
+            PlacesUtils.livemarks.getLivemark({ id: node.itemId })
+              .then(aLivemark => {
+                this._controller.cacheLivemarkInfo(node, aLivemark);
+                properties += " livemark";
+                // The livemark attribute is set as a cell property on the title cell.
+                this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
+              }, () => undefined);
           }
         }
 
         if (itemId != -1) {
           let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
           if (queryName)
             properties += " OrganizerQuery_" + queryName;
         }
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -42,8 +42,9 @@ skip-if = true
 [browser_toolbar_migration.js]
 [browser_library_batch_delete.js]
 [browser_555547.js]
 [browser_416459_cut.js]
 [browser_library_downloads.js]
 [browser_library_left_pane_select_hierarchy.js]
 [browser_435851_copy_query.js]
 [browser_toolbarbutton_menu_context.js]
+[browser_library_openFlatContainer.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test opening a flat container in the right pane even if its parent in the
+ * left pane is closed.
+ */
+
+add_task(function* () {
+  let folder = PlacesUtils.bookmarks
+                          .createFolder(PlacesUtils.unfiledBookmarksFolderId,
+                                        "Folder",
+                                        PlacesUtils.bookmarks.DEFAULT_INDEX);
+  let bookmark = PlacesUtils.bookmarks
+                            .insertBookmark(folder, NetUtil.newURI("http://example.com/"),
+                                            PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                            "Bookmark");
+
+  let library = yield promiseLibrary("AllBookmarks");
+  registerCleanupFunction(function () {
+    library.close();
+    PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+  });
+
+  // Select unfiled later, to ensure it's closed.
+  library.PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+  ok(!library.PlacesOrganizer._places.selectedNode.containerOpen,
+     "Unfiled container is closed");
+
+  let folderNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+  is(folderNode.itemId, folder,
+     "Found the expected folder in the right pane");
+  // Select the folder node in the right pane.
+  library.ContentTree.view.selectNode(folderNode);
+
+  synthesizeClickOnSelectedTreeCell(library.ContentTree.view,
+                                    { clickCount: 2 });
+
+  is(library.ContentTree.view.view.nodeForTreeIndex(0).itemId, bookmark,
+     "Found the expected bookmark in the right pane");
+});
--- a/browser/components/places/tests/browser/browser_sidebarpanels_click.js
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -114,37 +114,16 @@ function test() {
         // Note that for the history sidebar, the URL itself is not opened,
         // and Places will show the load-js-data-url-error prompt as an alert
         // box, which means that the click actually worked, so it's good enough
         // for the purpose of this test.
       });
     }, true);
   }
 
-  function synthesizeClickOnSelectedTreeCell(aTree) {
-    let tbo = aTree.treeBoxObject;
-    is(tbo.view.selection.count, 1,
-       "The test node should be successfully selected");
-    // Get selection rowID.
-    let min = {}, max = {};
-    tbo.view.selection.getRangeAt(0, min, max);
-    let rowID = min.value;
-    tbo.ensureRowIsVisible(rowID);
-
-    // Calculate the click coordinates.
-    let x = {}, y = {}, width = {}, height = {};
-    tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text",
-                             x, y, width, height);
-    x = x.value + width.value / 2;
-    y = y.value + height.value / 2;
-    // Simulate the click.
-    EventUtils.synthesizeMouse(aTree.body, x, y, {},
-                               aTree.ownerDocument.defaultView);
-  }
-
   function changeSidebarDirection(aDirection) {
     sidebar.contentDocument.documentElement.style.direction = aDirection;
   }
 
   function runNextTest() {
     // Remove eventual tabs created by previous sub-tests.
     while (gBrowser.tabs.length > 1) {
       gBrowser.removeTab(gBrowser.tabContainer.lastChild);
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -1,12 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 // We need to cache this before test runs...
 let cachedLeftPaneFolderIdGetter;
 let (getter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId")) {
   if (!cachedLeftPaneFolderIdGetter && typeof(getter) == "function")
     cachedLeftPaneFolderIdGetter = getter;
 }
 // ...And restore it when test ends.
@@ -177,11 +181,27 @@ function addVisits(aPlaceInfo, aWindow, 
       handleCompletion: function UP_handleCompletion() {
         if (aCallback)
           aCallback();
       }
     }
   );
 }
 
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-  "resource://gre/modules/commonjs/sdk/core/promise.js");
-
+function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
+  let tbo = aTree.treeBoxObject;
+  if (tbo.view.selection.count != 1)
+     throw new Error("The test node should be successfully selected");
+  // Get selection rowID.
+  let min = {}, max = {};
+  tbo.view.selection.getRangeAt(0, min, max);
+  let rowID = min.value;
+  tbo.ensureRowIsVisible(rowID);
+  // Calculate the click coordinates.
+  let x = {}, y = {}, width = {}, height = {};
+  tbo.getCoordsForCellItem(rowID, aTree.columns[0], "text",
+                           x, y, width, height);
+  x = x.value + width.value / 2;
+  y = y.value + height.value / 2;
+  // Simulate the click.
+  EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
+                             aTree.ownerDocument.defaultView);
+}
--- a/browser/devtools/debugger/test/browser_dbg_chrome-create.js
+++ b/browser/devtools/debugger/test/browser_dbg_chrome-create.js
@@ -6,18 +6,18 @@
  */
 
 // Enable logging for this test, bug 860349.
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 
 let gProcess;
 
 function test() {
-  // Windows XP test slaves are terribly slow at this test.
-  requestLongerTimeout(4);
+  // Windows XP and 8.1 test slaves are terribly slow at this test.
+  requestLongerTimeout(5);
 
   initChromeDebugger(aOnClose).then(aProcess => {
     gProcess = aProcess;
 
     info("Starting test...");
     performTest();
   });
 }
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -212,20 +212,24 @@ toolbarbutton[sdk-button="true"][cui-are
   -moz-box-orient: vertical;
 }
 
 .panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-appearance: none;
   -moz-box-orient: vertical;
   width: calc(@menuPanelButtonWidth@ - 2px);
   height: calc(49px + 2.2em);
-  margin-top: 3px; /* Hack needed to get type=menu-button to properly align vertically. */
   border: 0;
 }
 
+.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-text,
+.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-multiline-text {
+  margin-top: 2px; /* Hack needed to get the label of type=menu-button aligned with other buttons */
+}
+
 .panel-customization-placeholder-child {
   margin: 6px 0 0;
   padding: 2px 6px;
   border: 1px solid transparent;
 }
 
 .panelUI-grid .toolbarbutton-1[type="menu"] {
   background-image: url("chrome://browser/skin/toolbarbutton-dropdown-arrow.png");
--- a/layout/reftests/font-face/reftest.list
+++ b/layout/reftests/font-face/reftest.list
@@ -136,17 +136,17 @@ fails-if(cocoaWidget) fails-if(winWidget
 
 HTTP(..) != 534352-1-extra-cmap-sentinel.html 534352-1-extra-cmap-sentinel-ref.html
 HTTP(..) == bug533251.html bug533251-ref.html
 
 # Bug 875287
 HTTP(..) == font-familiy-whitespace-1.html font-familiy-whitespace-1-ref.html
 HTTP(..) != font-familiy-whitespace-1.html font-familiy-whitespace-1-notref.html
 
-skip-if(B2G) fails-if(Android) HTTP(..) == ivs-1.html ivs-1-ref.html # bug 773482
+skip-if(B2G) HTTP(..) == ivs-1.html ivs-1-ref.html # bug 773482
 
 skip-if(B2G) HTTP(..) == missing-names.html missing-names-ref.html # bug 773482
 
 # Tests for bug 670900 - handling of 404 (not found) error in @font-face URL
 # (using Chunkfive font data returned from a .sjs file)
 HTTP(..) == font-error-404-1.html font-error-404-1-ref.html # HTTP status 404, don't load
 skip-if(B2G) fails-if(Android&&AndroidVersion==17) HTTP(..) == font-error-404-2.html font-error-404-2-ref.html # HTTP status 200, load # bug 773482
 fails-if(Android&&AndroidVersion==17) HTTP(..) != font-error-404-1.html font-error-404-2.html # sanity-check that the results differ
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -841,8 +841,12 @@ pref("browser.webapps.updateCheckUrl", "
 
 // The mode of home provider syncing.
 // 0: Sync always
 // 1: Sync only when on wifi
 pref("home.sync.updateMode", 0);
 
 // How frequently to check if we should sync home provider data.
 pref("home.sync.checkIntervalSecs", 3600);
+
+#ifdef NIGHTLY_BUILD
+pref("devtools.debugger.remote-enabled", true);
+#endif
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -37,17 +37,16 @@ import org.mozilla.gecko.home.BrowserSea
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeConfigInvalidator;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
-import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.StringUtils;
@@ -1442,17 +1441,17 @@ abstract public class BrowserApp extends
 
     /* Favicon stuff. */
     private static OnFaviconLoadedListener sFaviconLoadedListener = new OnFaviconLoadedListener() {
         @Override
         public void onFaviconLoaded(String pageUrl, String faviconURL, Bitmap favicon) {
             // If we failed to load a favicon, we use the default favicon instead.
             Tabs.getInstance()
                 .updateFaviconForURL(pageUrl,
-                                     (favicon == null) ? Favicons.sDefaultFavicon : favicon);
+                                     (favicon == null) ? Favicons.defaultFavicon : favicon);
         }
     };
 
     private void loadFavicon(final Tab tab) {
         maybeCancelFaviconLoad(tab);
 
         final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
 
@@ -1689,26 +1688,32 @@ abstract public class BrowserApp extends
             mLayerView.getLayerMarginsAnimator().showMargins(true);
         }
 
         if (mHomePager == null) {
             final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
             mHomePager = (HomePager) homePagerStub.inflate();
 
             final HomeBanner homeBanner = (HomeBanner) findViewById(R.id.home_banner);
-            mHomePager.setBanner(homeBanner);
-
-            // Remove the banner from the view hierarchy if it is dismissed.
-            homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
-                @Override
-                public void onDismiss() {
-                    mHomePager.setBanner(null);
-                    mHomePagerContainer.removeView(homeBanner);
-                }
-            });
+
+            // Never show the home banner in guest mode.
+            if (GeckoProfile.get(this).inGuestMode()) {
+                mHomePagerContainer.removeView(homeBanner);
+            } else {
+                mHomePager.setBanner(homeBanner);
+
+                // Remove the banner from the view hierarchy if it is dismissed.
+                homeBanner.setOnDismissListener(new HomeBanner.OnDismissListener() {
+                    @Override
+                    public void onDismiss() {
+                        mHomePager.setBanner(null);
+                        mHomePagerContainer.removeView(homeBanner);
+                    }
+                });
+            }
         }
 
         mHomePagerContainer.setVisibility(View.VISIBLE);
         mHomePager.load(getSupportLoaderManager(),
                         getSupportFragmentManager(),
                         pageId, animator);
 
         // Hide the web content so it cannot be focused by screen readers.
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -1116,20 +1116,23 @@ public class GeckoAppShell
             }
         }
         return true;
     }
 
     /**
      * Given the inputs to <code>getOpenURIIntent</code>, plus an optional
      * package name and class name, create and fire an intent to open the
-     * provided URI.
+     * provided URI. If a class name is specified but a package name is not,
+     * we will default to using the current fennec package.
      *
      * @param targetURI the string spec of the URI to open.
      * @param mimeType an optional MIME type string.
+     * @param packageName an optional app package name.
+     * @param className an optional intent class name.
      * @param action an Android action specifier, such as
      *               <code>Intent.ACTION_SEND</code>.
      * @param title the title to use in <code>ACTION_SEND</code> intents.
      * @return true if the activity started successfully; false otherwise.
      */
     @WrapElementForJNI
     public static boolean openUriExternal(String targetURI,
                                           String mimeType,
@@ -1140,18 +1143,23 @@ public class GeckoAppShell
         final Context context = getContext();
         final Intent intent = getOpenURIIntent(context, targetURI,
                                                mimeType, action, title);
 
         if (intent == null) {
             return false;
         }
 
-        if (packageName.length() > 0 && className.length() > 0) {
-            intent.setClassName(packageName, className);
+        if (!TextUtils.isEmpty(className)) {
+            if (!TextUtils.isEmpty(packageName)) {
+                intent.setClassName(packageName, className);
+            } else {
+                // Default to using the fennec app context.
+                intent.setClassName(context, className);
+            }
         }
 
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         try {
             context.startActivity(intent);
             return true;
         } catch (ActivityNotFoundException e) {
             return false;
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -8,38 +8,34 @@ package org.mozilla.gecko.favicons;
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.cache.FaviconCache;
-import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.NonEvictingLruCache;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import java.io.File;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 
 public class Favicons {
     private static final String LOGTAG = "GeckoFavicons";
 
     // A magic URL representing the app's own favicon, used for about: pages.
     private static final String BUILT_IN_FAVICON_URL = "about:favicon";
 
     // Size of the favicon bitmap cache, in bytes (Counting payload only).
@@ -48,46 +44,46 @@ public class Favicons {
     // Number of URL mappings from page URL to Favicon URL to cache in memory.
     public static final int NUM_PAGE_URL_MAPPINGS_TO_STORE = 128;
 
     public static final int NOT_LOADING  = 0;
     public static final int LOADED       = 1;
     public static final int FLAG_PERSIST = 2;
     public static final int FLAG_SCALE   = 4;
 
-    protected static Context sContext;
+    protected static Context context;
 
     // The default Favicon to show if no other can be found.
-    public static Bitmap sDefaultFavicon;
+    public static Bitmap defaultFavicon;
 
     // The density-adjusted default Favicon dimensions.
-    public static int sDefaultFaviconSize;
+    public static int defaultFaviconSize;
 
     // The density-adjusted maximum Favicon dimensions.
-    public static int sLargestFaviconSize;
+    public static int largestFaviconSize;
 
-    private static final SparseArray<LoadFaviconTask> sLoadTasks = new SparseArray<LoadFaviconTask>();
+    private static final SparseArray<LoadFaviconTask> loadTasks = new SparseArray<LoadFaviconTask>();
 
     // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
     // doing so is not necessary.
-    private static final NonEvictingLruCache<String, String> sPageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE);
+    private static final NonEvictingLruCache<String, String> pageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE);
 
     public static String getFaviconURLForPageURLFromCache(String pageURL) {
-        return sPageURLMappings.get(pageURL);
+        return pageURLMappings.get(pageURL);
     }
 
     /**
      * Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings.
      * Useful for short-circuiting local database access.
      */
     public static void putFaviconURLForPageURLInCache(String pageURL, String faviconURL) {
-        sPageURLMappings.put(pageURL, faviconURL);
+        pageURLMappings.put(pageURL, faviconURL);
     }
 
-    private static FaviconCache sFaviconsCache;
+    private static FaviconCache faviconsCache;
 
     /**
      * Returns either NOT_LOADING, or LOADED if the onFaviconLoaded call could
      * be made on the main thread.
      * If no listener is provided, NOT_LOADING is returned.
      */
     static int dispatchResult(final String pageUrl, final String faviconURL, final Bitmap image,
             final OnFaviconLoadedListener listener) {
@@ -112,17 +108,17 @@ public class Favicons {
 
     /**
      * Only returns a non-null Bitmap if the entire path is cached -- the
      * page URL to favicon URL, and the favicon URL to in-memory bitmaps.
      *
      * Returns null otherwise.
      */
     public static Bitmap getSizedFaviconForPageFromCache(final String pageURL, int targetSize) {
-        final String faviconURL = sPageURLMappings.get(pageURL);
+        final String faviconURL = pageURLMappings.get(pageURL);
         if (faviconURL == null) {
             return null;
         }
         return getSizedFaviconFromCache(faviconURL, targetSize);
     }
 
     /**
      * Get a Favicon as close as possible to the target dimensions for the URL provided.
@@ -138,37 +134,37 @@ public class Favicons {
      *                  immediately available.
      * @return The id of the asynchronous task created, NOT_LOADING if none is created, or
      *         LOADED if the value could be dispatched on the current thread.
      */
     public static int getSizedFavicon(String pageURL, String faviconURL, int targetSize, int flags, OnFaviconLoadedListener listener) {
         // Do we know the favicon URL for this page already?
         String cacheURL = faviconURL;
         if (cacheURL == null) {
-            cacheURL = sPageURLMappings.get(pageURL);
+            cacheURL = pageURLMappings.get(pageURL);
         }
 
         // If there's no favicon URL given, try and hit the cache with the default one.
         if (cacheURL == null)  {
             cacheURL = guessDefaultFaviconURL(pageURL);
         }
 
         // If it's something we can't even figure out a default URL for, just give up.
         if (cacheURL == null) {
-            return dispatchResult(pageURL, null, sDefaultFavicon, listener);
+            return dispatchResult(pageURL, null, defaultFavicon, listener);
         }
 
         Bitmap cachedIcon = getSizedFaviconFromCache(cacheURL, targetSize);
         if (cachedIcon != null) {
             return dispatchResult(pageURL, cacheURL, cachedIcon, listener);
         }
 
         // Check if favicon has failed.
-        if (sFaviconsCache.isFailedFavicon(cacheURL)) {
-            return dispatchResult(pageURL, cacheURL, sDefaultFavicon, listener);
+        if (faviconsCache.isFailedFavicon(cacheURL)) {
+            return dispatchResult(pageURL, cacheURL, defaultFavicon, listener);
         }
 
         // Failing that, try and get one from the database or internet.
         return loadUncachedFavicon(pageURL, faviconURL, flags, targetSize, listener);
     }
 
     /**
      * Returns the cached Favicon closest to the target size if any exists or is coercible. Returns
@@ -176,17 +172,17 @@ public class Favicons {
      * immediately available.
      *
      * @param faviconURL URL of the Favicon to query for.
      * @param targetSize The desired size of the returned Favicon.
      * @return The cached Favicon, rescaled to be as close as possible to the target size, if any exists.
      *         null if no applicable Favicon exists in the cache.
      */
     public static Bitmap getSizedFaviconFromCache(String faviconURL, int targetSize) {
-        return sFaviconsCache.getFaviconForDimensions(faviconURL, targetSize);
+        return faviconsCache.getFaviconForDimensions(faviconURL, targetSize);
     }
 
     /**
      * Attempts to find a Favicon for the provided page URL from either the mem cache or the database.
      * Does not need an explicit favicon URL, since, as we are accessing the database anyway, we
      * can query the history DB for the Favicon URL.
      * Handy for easing the transition from caching with page URLs to caching with Favicon URLs.
      *
@@ -196,43 +192,43 @@ public class Favicons {
      * @param pageURL Page URL for which a Favicon is wanted.
      * @param targetSize Target size of the desired Favicon to pass to the cache query
      * @param callback Callback to fire with the result.
      * @return The job ID of the spawned async task, if any.
      */
     public static int getSizedFaviconForPageFromLocal(final String pageURL, final int targetSize, final OnFaviconLoadedListener callback) {
         // Firstly, try extremely hard to cheat.
         // Have we cached this favicon URL? If we did, we can consult the memcache right away.
-        String targetURL = sPageURLMappings.get(pageURL);
+        String targetURL = pageURLMappings.get(pageURL);
         if (targetURL != null) {
             // Check if favicon has failed.
-            if (sFaviconsCache.isFailedFavicon(targetURL)) {
+            if (faviconsCache.isFailedFavicon(targetURL)) {
                 return dispatchResult(pageURL, targetURL, null, callback);
             }
 
             // Do we have a Favicon in the cache for this favicon URL?
             Bitmap result = getSizedFaviconFromCache(targetURL, targetSize);
             if (result != null) {
                 // Victory - immediate response!
                 return dispatchResult(pageURL, targetURL, result, callback);
             }
         }
 
         // No joy using in-memory resources. Go to background thread and ask the database.
         LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageURL, targetURL, 0, callback, targetSize, true);
         int taskId = task.getId();
-        synchronized(sLoadTasks) {
-            sLoadTasks.put(taskId, task);
+        synchronized(loadTasks) {
+            loadTasks.put(taskId, task);
         }
         task.execute();
         return taskId;
     }
 
     public static int getSizedFaviconForPageFromLocal(final String pageURL, final OnFaviconLoadedListener callback) {
-        return getSizedFaviconForPageFromLocal(pageURL, sDefaultFaviconSize, callback);
+        return getSizedFaviconForPageFromLocal(pageURL, defaultFaviconSize, callback);
     }
 
     /**
      * Helper method to determine the URL of the Favicon image for a given page URL by querying the
      * history database. Should only be called from the background thread - does database access.
      *
      * @param pageURL The URL of a webpage with a Favicon.
      * @return The URL of the Favicon used by that webpage, according to either the History database
@@ -245,17 +241,17 @@ public class Favicons {
         Tab theTab = Tabs.getInstance().getFirstTabForUrl(pageURL);
         if (theTab != null) {
             targetURL = theTab.getFaviconURL();
             if (targetURL != null) {
                 return targetURL;
             }
         }
 
-        targetURL = BrowserDB.getFaviconUrlForHistoryUrl(sContext.getContentResolver(), pageURL);
+        targetURL = BrowserDB.getFaviconUrlForHistoryUrl(context.getContentResolver(), pageURL);
         if (targetURL == null) {
             // Nothing in the history database. Fall back to the default URL and hope for the best.
             targetURL = guessDefaultFaviconURL(pageURL);
         }
         return targetURL;
     }
 
     /**
@@ -281,124 +277,125 @@ public class Favicons {
         if (TextUtils.isEmpty(pageUrl)) {
             dispatchResult(null, null, null, listener);
             return NOT_LOADING;
         }
 
         LoadFaviconTask task = new LoadFaviconTask(ThreadUtils.getBackgroundHandler(), pageUrl, faviconUrl, flags, listener, targetSize, false);
 
         int taskId = task.getId();
-        synchronized(sLoadTasks) {
-            sLoadTasks.put(taskId, task);
+        synchronized(loadTasks) {
+            loadTasks.put(taskId, task);
         }
 
         task.execute();
 
         return taskId;
     }
 
     public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
-        sFaviconsCache.putSingleFavicon(pageUrl, image);
+        faviconsCache.putSingleFavicon(pageUrl, image);
     }
 
     /**
      * Adds the bitmaps given by the specified iterator to the cache associated with the url given.
      * Future requests for images will be able to select the least larger image than the target
      * size from this new set of images.
      *
      * @param pageUrl The URL to associate the new favicons with.
      * @param images An iterator over the new favicons to put in the cache.
      */
     public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images, boolean permanently) {
-        sFaviconsCache.putFavicons(pageUrl, images, permanently);
+        faviconsCache.putFavicons(pageUrl, images, permanently);
     }
 
     public static void putFaviconsInMemCache(String pageUrl, Iterator<Bitmap> images) {
         putFaviconsInMemCache(pageUrl, images, false);
     }
 
     public static void clearMemCache() {
-        sFaviconsCache.evictAll();
-        sPageURLMappings.evictAll();
+        faviconsCache.evictAll();
+        pageURLMappings.evictAll();
     }
 
     public static void putFaviconInFailedCache(String faviconURL) {
-        sFaviconsCache.putFailed(faviconURL);
+        faviconsCache.putFailed(faviconURL);
     }
 
     public static boolean cancelFaviconLoad(int taskId) {
         if (taskId == NOT_LOADING) {
             return false;
         }
 
         boolean cancelled;
-        synchronized (sLoadTasks) {
-            if (sLoadTasks.indexOfKey(taskId) < 0)
+        synchronized (loadTasks) {
+            if (loadTasks.indexOfKey(taskId) < 0) {
                 return false;
+            }
 
             Log.d(LOGTAG, "Cancelling favicon load (" + taskId + ")");
 
-            LoadFaviconTask task = sLoadTasks.get(taskId);
+            LoadFaviconTask task = loadTasks.get(taskId);
             cancelled = task.cancel(false);
         }
         return cancelled;
     }
 
     public static void close() {
         Log.d(LOGTAG, "Closing Favicons database");
 
         // Cancel any pending tasks
-        synchronized (sLoadTasks) {
-            final int count = sLoadTasks.size();
+        synchronized (loadTasks) {
+            final int count = loadTasks.size();
             for (int i = 0; i < count; i++) {
-                cancelFaviconLoad(sLoadTasks.keyAt(i));
+                cancelFaviconLoad(loadTasks.keyAt(i));
             }
-            sLoadTasks.clear();
+            loadTasks.clear();
         }
 
         LoadFaviconTask.closeHTTPClient();
     }
 
     /**
      * Get the dominant colour of the Favicon at the URL given, if any exists in the cache.
      *
      * @param url The URL of the Favicon, to be used as the cache key for the colour value.
      * @return The dominant colour of the provided Favicon.
      */
     public static int getFaviconColor(String url) {
-        return sFaviconsCache.getDominantColor(url);
+        return faviconsCache.getDominantColor(url);
     }
 
     /**
      * Called by GeckoApp on startup to pass this class a reference to the GeckoApp object used as
      * the application's Context.
      * Consider replacing with references to a staticly held reference to the GeckoApp object.
      *
      * @param context A reference to the GeckoApp instance.
      */
     public static void attachToContext(Context context) throws Exception {
         final Resources res = context.getResources();
-        sContext = context;
+        Favicons.context = context;
 
         // Decode the default Favicon ready for use.
-        sDefaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon);
-        if (sDefaultFavicon == null) {
+        defaultFavicon = BitmapFactory.decodeResource(res, R.drawable.favicon);
+        if (defaultFavicon == null) {
             throw new Exception("Null default favicon was returned from the resources system!");
         }
 
-        sDefaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
+        defaultFaviconSize = res.getDimensionPixelSize(R.dimen.favicon_bg);
 
         // Screen-density-adjusted upper limit on favicon size. Favicons larger than this are
         // downscaled to this size or discarded.
-        sLargestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
-        sFaviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, sLargestFaviconSize);
+        largestFaviconSize = context.getResources().getDimensionPixelSize(R.dimen.favicon_largest_interesting_size);
+        faviconsCache = new FaviconCache(FAVICON_CACHE_SIZE_BYTES, largestFaviconSize);
 
         // Initialize page mappings for each of our special pages.
         for (String url : AboutPages.getDefaultIconPages()) {
-            sPageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL);
+            pageURLMappings.putWithoutEviction(url, BUILT_IN_FAVICON_URL);
         }
 
         // Load and cache the built-in favicon in each of its sizes.
         // TODO: don't open the zip twice!
         List<Bitmap> toInsert = Arrays.asList(loadBrandingBitmap(context, "favicon64.png"),
                                               loadBrandingBitmap(context, "favicon32.png"));
 
         putFaviconsInMemCache(BUILT_IN_FAVICON_URL, toInsert.iterator(), true);
@@ -448,28 +445,28 @@ public class Favicons {
                            null).toString();
         } catch (URISyntaxException e) {
             Log.e(LOGTAG, "URISyntaxException getting default favicon URL", e);
             return null;
         }
     }
 
     public static void removeLoadTask(int taskId) {
-        synchronized(sLoadTasks) {
-            sLoadTasks.delete(taskId);
+        synchronized(loadTasks) {
+            loadTasks.delete(taskId);
         }
     }
 
     /**
      * Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask.
      *
      * @param faviconURL Favicon URL to check for failure.
      */
     static boolean isFailedFavicon(String faviconURL) {
-        return sFaviconsCache.isFailedFavicon(faviconURL);
+        return faviconsCache.isFailedFavicon(faviconURL);
     }
 
     /**
      * Sidestep the cache and get, from either the database or the internet, a favicon
      * suitable for use as an app icon for the provided URL.
      *
      * Useful for creating homescreen shortcuts without being limited
      * by possibly low-resolution values in the cache.
--- a/mobile/android/base/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/favicons/LoadFaviconTask.java
@@ -17,17 +17,17 @@ import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
-import static org.mozilla.gecko.favicons.Favicons.sContext;
+import static org.mozilla.gecko.favicons.Favicons.context;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -48,66 +48,66 @@ public class LoadFaviconTask extends UiA
 
     public static final int FLAG_PERSIST = 1;
     public static final int FLAG_SCALE = 2;
     private static final int MAX_REDIRECTS_TO_FOLLOW = 5;
     // The default size of the buffer to use for downloading Favicons in the event no size is given
     // by the server.
     private static final int DEFAULT_FAVICON_BUFFER_SIZE = 25000;
 
-    private static AtomicInteger mNextFaviconLoadId = new AtomicInteger(0);
-    private int mId;
-    private String mPageUrl;
-    private String mFaviconUrl;
-    private OnFaviconLoadedListener mListener;
-    private int mFlags;
+    private static AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
+    private int id;
+    private String pageUrl;
+    private String faviconURL;
+    private OnFaviconLoadedListener listener;
+    private int flags;
 
-    private final boolean mOnlyFromLocal;
+    private final boolean onlyFromLocal;
 
     // Assuming square favicons, judging by width only is acceptable.
-    protected int mTargetWidth;
-    private LinkedList<LoadFaviconTask> mChainees;
-    private boolean mIsChaining;
+    protected int targetWidth;
+    private LinkedList<LoadFaviconTask> chainees;
+    private boolean isChaining;
 
-    static AndroidHttpClient sHttpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
+    static AndroidHttpClient httpClient = AndroidHttpClient.newInstance(GeckoAppShell.getGeckoInterface().getDefaultUAString());
 
     public LoadFaviconTask(Handler backgroundThreadHandler,
                            String pageUrl, String faviconUrl, int flags,
                            OnFaviconLoadedListener listener) {
         this(backgroundThreadHandler, pageUrl, faviconUrl, flags, listener, -1, false);
     }
     public LoadFaviconTask(Handler backgroundThreadHandler,
                            String pageUrl, String faviconUrl, int flags,
-                           OnFaviconLoadedListener aListener, int targetSize, boolean fromLocal) {
+                           OnFaviconLoadedListener listener, int targetWidth, boolean onlyFromLocal) {
         super(backgroundThreadHandler);
 
-        mId = mNextFaviconLoadId.incrementAndGet();
+        id = nextFaviconLoadId.incrementAndGet();
 
-        mPageUrl = pageUrl;
-        mFaviconUrl = faviconUrl;
-        mListener = aListener;
-        mFlags = flags;
-        mTargetWidth = targetSize;
-        mOnlyFromLocal = fromLocal;
+        this.pageUrl = pageUrl;
+        this.faviconURL = faviconUrl;
+        this.listener = listener;
+        this.flags = flags;
+        this.targetWidth = targetWidth;
+        this.onlyFromLocal = onlyFromLocal;
     }
 
     // Runs in background thread
     private LoadFaviconResult loadFaviconFromDb() {
-        ContentResolver resolver = sContext.getContentResolver();
-        return BrowserDB.getFaviconForFaviconUrl(resolver, mFaviconUrl);
+        ContentResolver resolver = context.getContentResolver();
+        return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL);
     }
 
     // Runs in background thread
     private void saveFaviconToDb(final byte[] encodedFavicon) {
-        if ((mFlags & FLAG_PERSIST) == 0) {
+        if ((flags & FLAG_PERSIST) == 0) {
             return;
         }
 
-        ContentResolver resolver = sContext.getContentResolver();
-        BrowserDB.updateFaviconForUrl(resolver, mPageUrl, encodedFavicon, mFaviconUrl);
+        ContentResolver resolver = context.getContentResolver();
+        BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL);
     }
 
     /**
      * Helper method for trying the download request to grab a Favicon.
      * @param faviconURI URL of Favicon to try and download
      * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
      */
     private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException {
@@ -116,17 +116,17 @@ public class LoadFaviconTask extends UiA
         return tryDownloadRecurse(faviconURI, visitedLinkSet);
     }
     private HttpResponse tryDownloadRecurse(URI faviconURI, HashSet<String> visited) throws URISyntaxException, IOException {
         if (visited.size() == MAX_REDIRECTS_TO_FOLLOW) {
             return null;
         }
 
         HttpGet request = new HttpGet(faviconURI);
-        HttpResponse response = sHttpClient.execute(request);
+        HttpResponse response = httpClient.execute(request);
         if (response == null) {
             return null;
         }
 
         if (response.getStatusLine() != null) {
 
             // Was the response a failure?
             int status = response.getStatusLine().getStatusCode();
@@ -167,17 +167,17 @@ public class LoadFaviconTask extends UiA
      */
     private static Bitmap fetchJARFavicon(String uri) {
         if (uri == null) {
             return null;
         }
         if (uri.startsWith("jar:jar:")) {
             Log.d(LOGTAG, "Fetching favicon from JAR.");
             try {
-                return GeckoJarReader.getBitmap(sContext.getResources(), uri);
+                return GeckoJarReader.getBitmap(context.getResources(), uri);
             } catch (Exception e) {
                 // Just about anything could happen here.
                 Log.w(LOGTAG, "Error fetching favicon from JAR.", e);
                 return null;
             }
         }
         return null;
     }
@@ -282,129 +282,129 @@ public class LoadFaviconTask extends UiA
             return null;
         }
 
         String storedFaviconUrl;
         boolean isUsingDefaultURL = false;
 
         // Handle the case of malformed favicon URL.
         // If favicon is empty, fall back to the stored one.
-        if (TextUtils.isEmpty(mFaviconUrl)) {
+        if (TextUtils.isEmpty(faviconURL)) {
             // Try to get the favicon URL from the memory cache.
-            storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(mPageUrl);
+            storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl);
 
             // If that failed, try to get the URL from the database.
             if (storedFaviconUrl == null) {
-                storedFaviconUrl = Favicons.getFaviconURLForPageURL(mPageUrl);
+                storedFaviconUrl = Favicons.getFaviconURLForPageURL(pageUrl);
                 if (storedFaviconUrl != null) {
                     // If that succeeded, cache the URL loaded from the database in memory.
-                    Favicons.putFaviconURLForPageURLInCache(mPageUrl, storedFaviconUrl);
+                    Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl);
                 }
             }
 
             // If we found a faviconURL - use it.
             if (storedFaviconUrl != null) {
-                mFaviconUrl = storedFaviconUrl;
+                faviconURL = storedFaviconUrl;
             } else {
                 // If we don't have a stored one, fall back to the default.
-                mFaviconUrl = Favicons.guessDefaultFaviconURL(mPageUrl);
+                faviconURL = Favicons.guessDefaultFaviconURL(pageUrl);
 
-                if (TextUtils.isEmpty(mFaviconUrl)) {
+                if (TextUtils.isEmpty(faviconURL)) {
                     return null;
                 }
                 isUsingDefaultURL = true;
             }
         }
 
         // Check if favicon has failed - if so, give up. We need this check because, sometimes, we
         // didn't know the real Favicon URL until we asked the database.
-        if (Favicons.isFailedFavicon(mFaviconUrl)) {
+        if (Favicons.isFailedFavicon(faviconURL)) {
             return null;
         }
 
         if (isCancelled()) {
             return null;
         }
 
         Bitmap image;
         // Determine if there is already an ongoing task to fetch the Favicon we desire.
         // If there is, just join the queue and wait for it to finish. If not, we carry on.
         synchronized(loadsInFlight) {
             // Another load of the current Favicon is already underway
-            LoadFaviconTask existingTask = loadsInFlight.get(mFaviconUrl);
+            LoadFaviconTask existingTask = loadsInFlight.get(faviconURL);
             if (existingTask != null && !existingTask.isCancelled()) {
                 existingTask.chainTasks(this);
-                mIsChaining = true;
+                isChaining = true;
 
                 // If we are chaining, we want to keep the first task started to do this job as the one
                 // in the hashmap so subsequent tasks will add themselves to its chaining list.
                 return null;
             }
 
             // We do not want to update the hashmap if the task has chained - other tasks need to
             // chain onto the same parent task.
-            loadsInFlight.put(mFaviconUrl, this);
+            loadsInFlight.put(faviconURL, this);
         }
 
         if (isCancelled()) {
             return null;
         }
 
         // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
         LoadFaviconResult loadedBitmaps = loadFaviconFromDb();
         if (loadedBitmaps != null) {
             return pushToCacheAndGetResult(loadedBitmaps);
         }
 
-        if (mOnlyFromLocal || isCancelled()) {
+        if (onlyFromLocal || isCancelled()) {
             return null;
         }
 
         // Let's see if it's in a JAR.
-        image = fetchJARFavicon(mFaviconUrl);
+        image = fetchJARFavicon(faviconURL);
         if (imageIsValid(image)) {
             // We don't want to put this into the DB.
-            Favicons.putFaviconInMemCache(mFaviconUrl, image);
+            Favicons.putFaviconInMemCache(faviconURL, image);
             return image;
         }
 
         try {
-            loadedBitmaps = downloadFavicon(new URI(mFaviconUrl));
+            loadedBitmaps = downloadFavicon(new URI(faviconURL));
         } catch (URISyntaxException e) {
             Log.e(LOGTAG, "The provided favicon URL is not valid");
             return null;
         } catch (Exception e) {
             Log.e(LOGTAG, "Couldn't download favicon.", e);
         }
 
         if (loadedBitmaps != null) {
             saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
             return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         if (isUsingDefaultURL) {
-            Favicons.putFaviconInFailedCache(mFaviconUrl);
+            Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
         if (isCancelled()) {
             return null;
         }
 
         // If we're not already trying the default URL, try it now.
-        final String guessed = Favicons.guessDefaultFaviconURL(mPageUrl);
+        final String guessed = Favicons.guessDefaultFaviconURL(pageUrl);
         if (guessed == null) {
-            Favicons.putFaviconInFailedCache(mFaviconUrl);
+            Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
         image = fetchJARFavicon(guessed);
         if (imageIsValid(image)) {
             // We don't want to put this into the DB.
-            Favicons.putFaviconInMemCache(mFaviconUrl, image);
+            Favicons.putFaviconInMemCache(faviconURL, image);
             return image;
         }
 
         try {
             loadedBitmaps = downloadFavicon(new URI(guessed));
         } catch (Exception e) {
             // Not interesting. It was an educated guess, anyway.
             return null;
@@ -423,124 +423,124 @@ public class LoadFaviconTask extends UiA
      * cache for the particular bitmap we want for this request.
      * This call is certain to succeed, provided there was enough memory to decode this favicon.
      *
      * @param loadedBitmaps LoadFaviconResult to store.
      * @return The optimal favicon available to satisfy this LoadFaviconTask's request, or null if
      *         we are under extreme memory pressure and find ourselves dropping the cache immediately.
      */
     private Bitmap pushToCacheAndGetResult(LoadFaviconResult loadedBitmaps) {
-        Favicons.putFaviconsInMemCache(mFaviconUrl, loadedBitmaps.getBitmaps());
-        Bitmap result = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
+        Favicons.putFaviconsInMemCache(faviconURL, loadedBitmaps.getBitmaps());
+        Bitmap result = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
         return result;
     }
 
     private static boolean imageIsValid(final Bitmap image) {
         return image != null &&
                image.getWidth() > 0 &&
                image.getHeight() > 0;
     }
 
     @Override
     protected void onPostExecute(Bitmap image) {
-        if (mIsChaining) {
+        if (isChaining) {
             return;
         }
 
         // Process the result, scale for the listener, etc.
         processResult(image);
 
         synchronized (loadsInFlight) {
             // Prevent any other tasks from chaining on this one.
-            loadsInFlight.remove(mFaviconUrl);
+            loadsInFlight.remove(faviconURL);
         }
 
-        // Since any update to mChainees is done while holding the loadsInFlight lock, once we reach
+        // Since any update to chainees is done while holding the loadsInFlight lock, once we reach
         // this point no further updates to that list can possibly take place (As far as other tasks
         // are concerned, there is no longer a task to chain from. The above block will have waited
         // for any tasks that were adding themselves to the list before reaching this point.)
 
         // As such, I believe we're safe to do the following without holding the lock.
         // This is nice - we do not want to take the lock unless we have to anyway, and chaining rarely
         // actually happens outside of the strange situations unit tests create.
 
         // Share the result with all chained tasks.
-        if (mChainees != null) {
-            for (LoadFaviconTask t : mChainees) {
+        if (chainees != null) {
+            for (LoadFaviconTask t : chainees) {
                 // In the case that we just decoded multiple favicons, either we're passing the right
                 // image now, or the call into the cache in processResult will fetch the right one.
                 t.processResult(image);
             }
         }
     }
 
     private void processResult(Bitmap image) {
-        Favicons.removeLoadTask(mId);
+        Favicons.removeLoadTask(id);
         Bitmap scaled = image;
 
         // Notify listeners, scaling if required.
-        if (mTargetWidth != -1 && image != null &&  image.getWidth() != mTargetWidth) {
-            scaled = Favicons.getSizedFaviconFromCache(mFaviconUrl, mTargetWidth);
+        if (targetWidth != -1 && image != null &&  image.getWidth() != targetWidth) {
+            scaled = Favicons.getSizedFaviconFromCache(faviconURL, targetWidth);
         }
 
-        Favicons.dispatchResult(mPageUrl, mFaviconUrl, scaled, mListener);
+        Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener);
     }
 
     @Override
     protected void onCancelled() {
-        Favicons.removeLoadTask(mId);
+        Favicons.removeLoadTask(id);
 
         synchronized(loadsInFlight) {
             // Only remove from the hashmap if the task there is the one that's being canceled.
             // Cancellation of a task that would have chained is not interesting to the hashmap.
-            final LoadFaviconTask primary = loadsInFlight.get(mFaviconUrl);
+            final LoadFaviconTask primary = loadsInFlight.get(faviconURL);
             if (primary == this) {
-                loadsInFlight.remove(mFaviconUrl);
+                loadsInFlight.remove(faviconURL);
                 return;
             }
             if (primary == null) {
                 // This shouldn't happen.
                 return;
             }
-            if (primary.mChainees != null) {
-              primary.mChainees.remove(this);
+            if (primary.chainees != null) {
+              primary.chainees.remove(this);
             }
         }
 
         // Note that we don't call the listener callback if the
         // favicon load is cancelled.
     }
 
     /**
      * When the result of this job is ready, also notify the chainee of the result.
      * Used for aggregating concurrent requests for the same Favicon into a single actual request.
      * (Don't want to download a hundred instances of Google's Favicon at once, for example).
      * The loadsInFlight lock must be held when calling this function.
      *
      * @param aChainee LoadFaviconTask
      */
     private void chainTasks(LoadFaviconTask aChainee) {
-        if (mChainees == null) {
-            mChainees = new LinkedList<LoadFaviconTask>();
+        if (chainees == null) {
+            chainees = new LinkedList<LoadFaviconTask>();
         }
 
-        mChainees.add(aChainee);
+        chainees.add(aChainee);
     }
 
     int getId() {
-        return mId;
+        return id;
     }
 
     static void closeHTTPClient() {
         // This work must be done on a background thread because it shuts down
         // the connection pool, which typically involves closing a connection --
         // which counts as network activity.
         if (ThreadUtils.isOnBackgroundThread()) {
-            if (sHttpClient != null) {
-                sHttpClient.close();
+            if (httpClient != null) {
+                httpClient.close();
             }
             return;
         }
 
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 LoadFaviconTask.closeHTTPClient();
--- a/mobile/android/base/favicons/cache/FaviconCache.java
+++ b/mobile/android/base/favicons/cache/FaviconCache.java
@@ -15,17 +15,17 @@ import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * Implements a Least-Recently-Used cache for Favicons, keyed by Favicon URL.
  *
  * When a favicon at a particular URL is decoded, it will yield one or more bitmaps.
  * While in memory, these bitmaps are stored in a list, sorted in ascending order of size, in a
  * FaviconsForURL object.
- * The collection of FaviconsForURL objects currently in the cache is stored in mBackingMap, keyed
+ * The collection of FaviconsForURL objects currently in the cache is stored in backingMap, keyed
  * by favicon URL.
  *
  * A second map exists for permanent cache entries -- ones that are never expired. These entries
  * are assumed to be disjoint from those in the normal cache, and this map is checked first.
  *
  * FaviconsForURL provides a method for obtaining the smallest icon larger than a given size - the
  * most appropriate icon for a particular size.
  * It also distinguishes between "primary" favicons (Ones that have merely been extracted from a
@@ -54,17 +54,17 @@ import java.util.concurrent.atomic.Atomi
  * If that step fails, the object finally walks backwards through its sequence of favicons until it
  * finds the largest primary favicon smaller than the target. This is then upscaled by a maximum of
  * 2x towards the target size, and the result cached and returned as above.
  *
  * The bitmaps themselves are encapsulated inside FaviconCacheElement objects. These objects contain,
  * as well as the bitmap, a pointer to the encapsulating FaviconsForURL object (Used by the LRU
  * culler), the size of the encapsulated image, a flag indicating if this is a primary favicon, and
  * a flag indicating if the entry is invalid.
- * All FaviconCacheElement objects are tracked in the mOrdering LinkedList. This is used to record
+ * All FaviconCacheElement objects are tracked in the ordering LinkedList. This is used to record
  * LRU information about FaviconCacheElements. In particular, the most recently used FaviconCacheElement
  * will be at the start of the list, the least recently used at the end of the list.
  *
  * When the cache runs out of space, it removes FaviconCacheElements starting from the end of the list
  * until a sufficient amount of space has been freed.
  * When a secondary favicon is removed in this way, it is simply deleted from its parent FaviconsForURLs
  * object's list of available favicons.
  * The backpointer field on the FaviconCacheElement is used to remove the element from the encapsulating
@@ -93,208 +93,179 @@ import java.util.concurrent.atomic.Atomi
  */
 public class FaviconCache {
     private static final String LOGTAG = "FaviconCache";
 
     // The number of spaces to allocate for favicons in each node.
     private static final int NUM_FAVICON_SIZES = 4;
 
     // Dimensions of the largest favicon to store in the cache. Everything is downscaled to this.
-    public final int mMaxCachedWidth;
+    public final int maxCachedWidth;
 
     // Retry failed favicons after 20 minutes.
     public static final long FAILURE_RETRY_MILLISECONDS = 1000 * 60 * 20;
 
     // Map relating Favicon URLs with objects representing decoded favicons.
     // Since favicons may be container formats holding multiple icons, the underlying type holds a
     // sorted list of bitmap payloads in ascending order of size. The underlying type may be queried
     // for the least larger payload currently present.
-    private final ConcurrentHashMap<String, FaviconsForURL> mBackingMap = new ConcurrentHashMap<String, FaviconsForURL>();
+    private final ConcurrentHashMap<String, FaviconsForURL> backingMap = new ConcurrentHashMap<String, FaviconsForURL>();
 
     // And the same, but never evicted.
-    private final ConcurrentHashMap<String, FaviconsForURL> mPermanentBackingMap = new ConcurrentHashMap<String, FaviconsForURL>();
+    private final ConcurrentHashMap<String, FaviconsForURL> permanentBackingMap = new ConcurrentHashMap<String, FaviconsForURL>();
 
     // A linked list used to implement a queue, defining the LRU properties of the cache. Elements
     // contained within the various FaviconsForURL objects are held here, the least recently used
     // of which at the end of the list. When space needs to be reclaimed, the appropriate bitmap is
     // culled.
-    private final LinkedList<FaviconCacheElement> mOrdering = new LinkedList<FaviconCacheElement>();
+    private final LinkedList<FaviconCacheElement> ordering = new LinkedList<FaviconCacheElement>();
 
     // The above structures, if used correctly, enable this cache to exhibit LRU semantics across all
     // favicon payloads in the system, as well as enabling the dynamic selection from the cache of
     // the primary bitmap most suited to the requested size (in cases where multiple primary bitmaps
     // are provided by the underlying file format).
 
     // Current size, in bytes, of the bitmap data present in the LRU cache.
-    private final AtomicInteger mCurrentSize = new AtomicInteger(0);
+    private final AtomicInteger currentSize = new AtomicInteger(0);
 
     // The maximum quantity, in bytes, of bitmap data which may be stored in the cache.
-    private final int mMaxSizeBytes;
+    private final int maxSizeBytes;
 
     // Tracks the number of ongoing read operations. Enables the first one in to lock writers out and
     // the last one out to let them in.
-    private final AtomicInteger mOngoingReads = new AtomicInteger(0);
+    private final AtomicInteger ongoingReads = new AtomicInteger(0);
 
     // Used to ensure transaction fairness - each txn acquires and releases this as the first operation.
     // The effect is an orderly, inexpensive ordering enforced on txns to prevent writer starvation.
-    private final Semaphore mTurnSemaphore = new Semaphore(1);
+    private final Semaphore turnSemaphore = new Semaphore(1);
 
     // A deviation from the usual MRSW solution - this semaphore is used to guard modification to the
     // ordering map. This allows for read transactions to update the most-recently-used value without
     // needing to take out the write lock.
-    private final Semaphore mReorderingSemaphore = new Semaphore(1);
+    private final Semaphore reorderingSemaphore = new Semaphore(1);
 
     // The semaphore one must acquire in order to perform a write.
-    private final Semaphore mWriteLock = new Semaphore(1);
+    private final Semaphore writeLock = new Semaphore(1);
 
     /**
      * Called by txns performing only reads as they start. Prevents writer starvation with a turn
      * semaphore and locks writers out if this is the first concurrent reader txn starting up.
      */
     private void startRead() {
-        mTurnSemaphore.acquireUninterruptibly();
-        mTurnSemaphore.release();
-
-        if (mOngoingReads.incrementAndGet() == 1) {
-            // First one in. Wait for writers to finish and lock them out.
-            mWriteLock.acquireUninterruptibly();
-        }
-    }
+        turnSemaphore.acquireUninterruptibly();
+        turnSemaphore.release();
 
-    /**
-     * An alternative to startWrite to be used when in a read transaction and wanting to upgrade it
-     * to a write transaction. Such a transaction should be terminated with finishWrite.
-     */
-    private void upgradeReadToWrite() {
-        mTurnSemaphore.acquireUninterruptibly();
-        if (mOngoingReads.decrementAndGet() == 0) {
-            mWriteLock.release();
+        if (ongoingReads.incrementAndGet() == 1) {
+            // First one in. Wait for writers to finish and lock them out.
+            writeLock.acquireUninterruptibly();
         }
-        mWriteLock.acquireUninterruptibly();
     }
 
     /**
      * Called by transactions performing only reads as they finish. Ensures that if this is the last
      * concluding read transaction then then writers are subsequently allowed in.
      */
     private void finishRead() {
-        if (mOngoingReads.decrementAndGet() == 0) {
-            mWriteLock.release();
+        if (ongoingReads.decrementAndGet() == 0) {
+            writeLock.release();
         }
     }
 
     /**
      * Called by writer transactions upon start. Ensures fairness and then obtains the write lock.
      * Upon return, no other txns will be executing concurrently.
      */
     private void startWrite() {
-        mTurnSemaphore.acquireUninterruptibly();
-        mWriteLock.acquireUninterruptibly();
+        turnSemaphore.acquireUninterruptibly();
+        writeLock.acquireUninterruptibly();
     }
 
     /**
      * Called by a concluding write transaction - unlocks the structure.
      */
     private void finishWrite() {
-        mTurnSemaphore.release();
-        mWriteLock.release();
+        turnSemaphore.release();
+        writeLock.release();
     }
 
     public FaviconCache(int maxSize, int maxWidthToCache) {
-        mMaxSizeBytes = maxSize;
-        mMaxCachedWidth = maxWidthToCache;
+        maxSizeBytes = maxSize;
+        maxCachedWidth = maxWidthToCache;
     }
 
     /**
      * Determine if the provided favicon URL is marked as a failure (Has failed to load before -
      * such icons get blacklisted for a time to prevent us endlessly retrying.)
      *
      * @param faviconURL Favicon URL to check if failed in memcache.
      * @return true if this favicon is blacklisted, false otherwise.
      */
     public boolean isFailedFavicon(String faviconURL) {
         if (faviconURL == null) {
             return true;
         }
 
         startRead();
 
-        boolean isExpired = false;
-        boolean isAborting = false;
-
         try {
             // If we don't have it in the cache, it certainly isn't a known failure.
             // Non-evictable favicons are never failed, so we don't need to
-            // check mPermanentBackingMap.
-            if (!mBackingMap.containsKey(faviconURL)) {
+            // check permanentBackingMap.
+            if (!backingMap.containsKey(faviconURL)) {
                 return false;
             }
 
-            FaviconsForURL container = mBackingMap.get(faviconURL);
+            FaviconsForURL container = backingMap.get(faviconURL);
 
             // If the has failed flag is not set, it's certainly not a known failure.
-            if (!container.mHasFailed) {
+            if (!container.hasFailed) {
                 return false;
             }
 
-            final long failureTimestamp = container.mDownloadTimestamp;
+            final long failureTimestamp = container.downloadTimestamp;
 
             // Calculate elapsed time since the failing download.
             final long failureDiff = System.currentTimeMillis() - failureTimestamp;
 
-            // If long enough has passed, mark it as no longer a failure.
-            if (failureDiff > FAILURE_RETRY_MILLISECONDS) {
-                isExpired = true;
-            } else {
+            // If the expiry is still in effect, return. Otherwise, continue and unmark the failure.
+            if (failureDiff < FAILURE_RETRY_MILLISECONDS) {
                 return true;
             }
         } catch (Exception unhandled) {
-            // Handle any exception thrown and return the locks to a sensible state.
-            finishRead();
-
-            // Flag to prevent finally from doubly-unlocking.
-            isAborting = true;
             Log.e(LOGTAG, "FaviconCache exception!", unhandled);
             return true;
         }  finally {
-            if (!isAborting) {
-                if (isExpired) {
-                    // No longer expired.
-                    upgradeReadToWrite();
-                } else {
-                    finishRead();
-                }
-            }
+            finishRead();
         }
 
+        startWrite();
+
+        // If the entry is no longer failed, remove the record of it from the cache.
         try {
-            recordRemoved(mBackingMap.get(faviconURL));
-            mBackingMap.remove(faviconURL);
+            recordRemoved(backingMap.remove(faviconURL));
             return false;
         } finally {
             finishWrite();
         }
     }
 
     /**
      * Mark the indicated page URL as a failed Favicon until the provided time.
      *
      * @param faviconURL Page URL for which a Favicon load has failed.
      */
     public void putFailed(String faviconURL) {
         startWrite();
 
-        if (mBackingMap.containsKey(faviconURL)) {
-            recordRemoved(mBackingMap.get(faviconURL));
+        try {
+            FaviconsForURL container = new FaviconsForURL(0, true);
+            recordRemoved(backingMap.put(faviconURL, container));
+        } finally {
+            finishWrite();
         }
-
-        FaviconsForURL container = new FaviconsForURL(0, true);
-        mBackingMap.put(faviconURL, container);
-
-        finishWrite();
     }
 
     /**
      * Fetch a Favicon for the given URL as close as possible to the size provided.
      * If an icon of the given size is already in the cache, it is returned.
      * If an icon of the given size is not in the cache but a larger unscaled image does exist in
      * the cache, we downscale the larger image to the target size and cache the result.
      * If there is no image of the required size, null is returned.
@@ -304,88 +275,81 @@ public class FaviconCache {
      * @return A favicon of the requested size for the requested URL, or null if none cached.
      */
     public Bitmap getFaviconForDimensions(String faviconURL, int targetSize) {
         if (faviconURL == null) {
             Log.e(LOGTAG, "You passed a null faviconURL to getFaviconForDimensions. Don't.");
             return null;
         }
 
-        boolean doingWrites = false;
         boolean shouldComputeColour = false;
-        boolean isAborting = false;
         boolean wasPermanent = false;
         FaviconsForURL container;
         final Bitmap newBitmap;
 
         startRead();
 
         try {
-            container = mPermanentBackingMap.get(faviconURL);
+            container = permanentBackingMap.get(faviconURL);
             if (container == null) {
-                container = mBackingMap.get(faviconURL);
+                container = backingMap.get(faviconURL);
                 if (container == null) {
                     // We don't have it!
                     return null;
                 }
             } else {
                 wasPermanent = true;
             }
 
             FaviconCacheElement cacheElement;
 
             // If targetSize is -1, it means we want the largest possible icon.
             int cacheElementIndex = (targetSize == -1) ? -1 : container.getNextHighestIndex(targetSize);
 
             // cacheElementIndex now holds either the index of the next least largest bitmap from
             // targetSize, or -1 if targetSize > all bitmaps.
             if (cacheElementIndex != -1) {
-                // If cacheElementIndex is not the sentinel value, then it is a valid index into mFavicons.
-                cacheElement = container.mFavicons.get(cacheElementIndex);
+                // If cacheElementIndex is not the sentinel value, then it is a valid index into favicons.
+                cacheElement = container.favicons.get(cacheElementIndex);
 
-                if (cacheElement.mInvalidated) {
+                if (cacheElement.invalidated) {
                     return null;
                 }
 
                 // If we found exactly what we wanted - we're done.
-                if (cacheElement.mImageSize == targetSize) {
-                    setMostRecentlyUsed(cacheElement);
-                    return cacheElement.mFaviconPayload;
+                if (cacheElement.imageSize == targetSize) {
+                    setMostRecentlyUsedWithinRead(cacheElement);
+                    return cacheElement.faviconPayload;
                 }
             } else {
                 // We requested an image larger than all primaries. Set the element to start the search
                 // from to the element beyond the end of the array, so the search runs backwards.
-                cacheElementIndex = container.mFavicons.size();
+                cacheElementIndex = container.favicons.size();
             }
 
             // We did not find exactly what we wanted, but now have set cacheElementIndex to the index
             // where what we want should live in the list. We now request the next least larger primary
             // from the cache. We will downscale this to our target size.
 
             // If there is no such primary, we'll upscale the next least smaller one instead.
             cacheElement = container.getNextPrimary(cacheElementIndex);
 
             if (cacheElement == null) {
                 // The primary has been invalidated! Fail! Need to get it back from the database.
                 return null;
             }
 
             if (targetSize == -1) {
                 // We got the biggest primary, so that's what we'll return.
-                return cacheElement.mFaviconPayload;
+                return cacheElement.faviconPayload;
             }
 
-            // Having got this far, we'll be needing to write the new secondary to the cache, which
-            // involves us falling through to the next try block. This flag lets us do this (Other
-            // paths prior to this end in returns.)
-            doingWrites = true;
-
             // Scaling logic...
-            Bitmap largestElementBitmap = cacheElement.mFaviconPayload;
-            int largestSize = cacheElement.mImageSize;
+            Bitmap largestElementBitmap = cacheElement.faviconPayload;
+            int largestSize = cacheElement.imageSize;
 
             if (largestSize >= targetSize) {
                 // The largest we have is larger than the target - downsize to target.
                 newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, targetSize, targetSize, true);
             } else {
                 // Our largest primary is smaller than the desired size. Upscale by a maximum of 2x.
                 // largestSize now reflects the maximum size we can upscale to.
                 largestSize *= 2;
@@ -396,49 +360,42 @@ public class FaviconCache {
                 } else {
                     // We don't have enough information to make the target size look nonterrible. Best effort:
                     newBitmap = Bitmap.createScaledBitmap(largestElementBitmap, largestSize, largestSize, true);
 
                     shouldComputeColour = true;
                 }
             }
         } catch (Exception unhandled) {
-            isAborting = true;
-
             // Handle any exception thrown and return the locks to a sensible state.
-            finishRead();
 
             // Flag to prevent finally from doubly-unlocking.
             Log.e(LOGTAG, "FaviconCache exception!", unhandled);
             return null;
         } finally {
-            if (!isAborting) {
-                if (doingWrites) {
-                    upgradeReadToWrite();
-                } else {
-                    finishRead();
-                }
-            }
+            finishRead();
         }
 
+        startWrite();
         try {
             if (shouldComputeColour) {
                 // And since we failed, we'll need the dominant colour.
                 container.ensureDominantColor();
             }
 
             // While the image might not actually BE that size, we set the size field to the target
             // because this is the best image you can get for a request of that size using the Favicon
             // information provided by this website.
             // This way, subsequent requests hit straight away.
             FaviconCacheElement newElement = container.addSecondary(newBitmap, targetSize);
 
             if (!wasPermanent) {
-                setMostRecentlyUsed(newElement);
-                mCurrentSize.addAndGet(newElement.sizeOf());
+                if (setMostRecentlyUsedWithinWrite(newElement)) {
+                    currentSize.addAndGet(newElement.sizeOf());
+                }
             }
         } finally {
             finishWrite();
         }
 
         return newBitmap;
     }
 
@@ -447,83 +404,101 @@ public class FaviconCache {
      *
      * @param key The URL of the Favicon for which a dominant colour is desired.
      * @return The cached dominant colour, or null if none is cached.
      */
     public int getDominantColor(String key) {
         startRead();
 
         try {
-            FaviconsForURL element = mPermanentBackingMap.get(key);
+            FaviconsForURL element = permanentBackingMap.get(key);
             if (element == null) {
-                element = mBackingMap.get(key);
+                element = backingMap.get(key);
             }
 
             if (element == null) {
                 Log.w(LOGTAG, "Cannot compute dominant color of non-cached favicon. Cache fullness " +
-                              mCurrentSize.get() + '/' + mMaxSizeBytes);
+                              currentSize.get() + '/' + maxSizeBytes);
                 finishRead();
                 return 0xFFFFFF;
             }
 
-
             return element.ensureDominantColor();
         } finally {
             finishRead();
         }
     }
 
     /**
-     * Remove all payloads stored in the given container from the LRU cache. Must be called while
-     * holding the write lock.
+     * Remove all payloads stored in the given container from the LRU cache.
+     * Must be called while holding the write lock.
      *
      * @param wasRemoved The container to purge from the cache.
      */
     private void recordRemoved(FaviconsForURL wasRemoved) {
         // If there was an existing value, strip it from the insertion-order cache.
         if (wasRemoved == null) {
             return;
         }
 
         int sizeRemoved = 0;
 
-        for (FaviconCacheElement e : wasRemoved.mFavicons) {
+        for (FaviconCacheElement e : wasRemoved.favicons) {
             sizeRemoved += e.sizeOf();
-            mOrdering.remove(e);
+            ordering.remove(e);
         }
 
-        mCurrentSize.addAndGet(-sizeRemoved);
+        currentSize.addAndGet(-sizeRemoved);
     }
 
     private Bitmap produceCacheableBitmap(Bitmap favicon) {
         // Never cache the default Favicon, or the null Favicon.
-        if (favicon == Favicons.sDefaultFavicon || favicon == null) {
+        if (favicon == Favicons.defaultFavicon || favicon == null) {
             return null;
         }
 
         // Some sites serve up insanely huge Favicons (Seen 512x512 ones...)
         // While we want to cache nice big icons, we apply a limit based on screen density for the
         // sake of space.
-        if (favicon.getWidth() > mMaxCachedWidth) {
-            return Bitmap.createScaledBitmap(favicon, mMaxCachedWidth, mMaxCachedWidth, true);
+        if (favicon.getWidth() > maxCachedWidth) {
+            return Bitmap.createScaledBitmap(favicon, maxCachedWidth, maxCachedWidth, true);
         }
+
         return favicon;
     }
 
     /**
-     * Set an existing element as the most recently used element. May be called from either type of
-     * transaction.
+     * Set an existing element as the most recently used element. Intended for use from read transactions. While
+     * write transactions may safely use this method, it will perform slightly worse than its unsafe counterpart below.
      *
      * @param element The element that is to become the most recently used one.
+     * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.)
      */
-    private void setMostRecentlyUsed(FaviconCacheElement element) {
-        mReorderingSemaphore.acquireUninterruptibly();
-        mOrdering.remove(element);
-        mOrdering.offer(element);
-        mReorderingSemaphore.release();
+    private boolean setMostRecentlyUsedWithinRead(FaviconCacheElement element) {
+        reorderingSemaphore.acquireUninterruptibly();
+        try {
+            boolean contained = ordering.remove(element);
+            ordering.offer(element);
+            return contained;
+        } finally {
+            reorderingSemaphore.release();
+        }
+    }
+
+    /**
+     * Functionally equivalent to setMostRecentlyUsedWithinRead, but operates without taking the reordering semaphore.
+     * Only safe for use when called from a write transaction, or there is a risk of concurrent modification.
+     *
+     * @param element The element that is to become the most recently used one.
+     * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.)
+     */
+    private boolean setMostRecentlyUsedWithinWrite(FaviconCacheElement element) {
+        boolean contained = ordering.remove(element);
+        ordering.offer(element);
+        return contained;
     }
 
     /**
      * Add the provided bitmap to the cache as the only available primary for this URL.
      * Should never be called with scaled Favicons. The input is assumed to be an unscaled Favicon.
      *
      * @param faviconURL The URL of the Favicon being stored.
      * @param aFavicon The Favicon to store.
@@ -541,23 +516,23 @@ public class FaviconCache {
         FaviconsForURL toInsert = new FaviconsForURL(NUM_FAVICON_SIZES);
 
         // Create the cache element for the single element we are inserting, and configure it.
         FaviconCacheElement newElement = toInsert.addPrimary(favicon);
 
         startWrite();
         try {
             // Set the new element as the most recently used one.
-            setMostRecentlyUsed(newElement);
+            setMostRecentlyUsedWithinWrite(newElement);
 
-            mCurrentSize.addAndGet(newElement.sizeOf());
+            currentSize.addAndGet(newElement.sizeOf());
 
             // Update the value in the LruCache...
             FaviconsForURL wasRemoved;
-            wasRemoved = mBackingMap.put(faviconURL, toInsert);
+            wasRemoved = backingMap.put(faviconURL, toInsert);
 
             recordRemoved(wasRemoved);
         } finally {
             finishWrite();
         }
 
         cullIfRequired();
     }
@@ -579,97 +554,78 @@ public class FaviconCache {
             if (favicon == null) {
                 continue;
             }
 
             FaviconCacheElement newElement = toInsert.addPrimary(favicon);
             sizeGained += newElement.sizeOf();
         }
 
-        startRead();
-
-        boolean abortingRead = false;
-
-        // Not using setMostRecentlyUsed, because the elements are known to be new. This can be done
-        // without taking the write lock, via the magic of the reordering semaphore.
-        mReorderingSemaphore.acquireUninterruptibly();
-        try {
-            if (!permanently) {
-                for (FaviconCacheElement newElement : toInsert.mFavicons) {
-                    mOrdering.offer(newElement);
-                }
-            }
-        } catch (Exception e) {
-            abortingRead = true;
-            mReorderingSemaphore.release();
-            finishRead();
-
-            Log.e(LOGTAG, "Favicon cache exception!", e);
-            return;
-        } finally {
-            if (!abortingRead) {
-                mReorderingSemaphore.release();
-                upgradeReadToWrite();
-            }
-        }
-
+        startWrite();
         try {
             if (permanently) {
-                mPermanentBackingMap.put(faviconURL, toInsert);
-            } else {
-                mCurrentSize.addAndGet(sizeGained);
+                permanentBackingMap.put(faviconURL, toInsert);
+                return;
+            }
 
-                // Update the value in the LruCache...
-                recordRemoved(mBackingMap.put(faviconURL, toInsert));
+            for (FaviconCacheElement newElement : toInsert.favicons) {
+                setMostRecentlyUsedWithinWrite(newElement);
             }
+
+            // In the event this insertion is being made to a key that already held a value, the subsequent recordRemoved
+            // call will subtract the size of the old value, preventing double-counting.
+            currentSize.addAndGet(sizeGained);
+
+            // Update the value in the LruCache...
+            recordRemoved(backingMap.put(faviconURL, toInsert));
         } finally {
             finishWrite();
         }
 
         cullIfRequired();
     }
 
     /**
      * If cache too large, drop stuff from the cache to get the size back into the acceptable range.
      * Otherwise, do nothing.
      */
     private void cullIfRequired() {
-        Log.d(LOGTAG, "Favicon cache fullness: " + mCurrentSize.get() + '/' + mMaxSizeBytes);
+        Log.d(LOGTAG, "Favicon cache fullness: " + currentSize.get() + '/' + maxSizeBytes);
 
-        if (mCurrentSize.get() <= mMaxSizeBytes) {
+        if (currentSize.get() <= maxSizeBytes) {
             return;
         }
 
         startWrite();
         try {
-            while (mCurrentSize.get() > mMaxSizeBytes) {
+            while (currentSize.get() > maxSizeBytes) {
                 // Cull the least recently used element.
 
                 FaviconCacheElement victim;
-                victim = mOrdering.poll();
+                victim = ordering.poll();
 
-                mCurrentSize.addAndGet(-victim.sizeOf());
+                currentSize.addAndGet(-victim.sizeOf());
                 victim.onEvictedFromCache();
 
-                Log.d(LOGTAG, "After cull: " + mCurrentSize.get() + '/' + mMaxSizeBytes);
+                Log.d(LOGTAG, "After cull: " + currentSize.get() + '/' + maxSizeBytes);
             }
         } finally {
             finishWrite();
         }
     }
 
     /**
      * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory.
      */
     public void evictAll() {
         startWrite();
 
         // Note that we neither clear, nor track the size of, the permanent map.
         try {
-            mCurrentSize.set(0);
-            mBackingMap.clear();
-            mOrdering.clear();
+            currentSize.set(0);
+            backingMap.clear();
+            ordering.clear();
 
         } finally {
             finishWrite();
         }
     }
 }
--- a/mobile/android/base/favicons/cache/FaviconCacheElement.java
+++ b/mobile/android/base/favicons/cache/FaviconCacheElement.java
@@ -7,109 +7,109 @@ package org.mozilla.gecko.favicons.cache
 import android.graphics.Bitmap;
 
 /**
  * Objects stored in the Favicon cache - allow for the bitmap to be tagged to indicate if it has
  * been scaled. Unscaled bitmaps are not included in the scaled-bitmap cache's size calculation.
  */
 public class FaviconCacheElement implements Comparable<FaviconCacheElement> {
     // Was this Favicon computed via scaling another primary Favicon, or is this a primary Favicon?
-    final boolean mIsPrimary;
+    final boolean isPrimary;
 
     // The Favicon bitmap.
-    Bitmap mFaviconPayload;
+    Bitmap faviconPayload;
 
-    // If set, mFaviconPayload is absent. Since the underlying ICO may contain multiple primary
+    // If set, faviconPayload is absent. Since the underlying ICO may contain multiple primary
     // payloads, primary payloads are never truly deleted from the cache, but instead have their
     // payload deleted and this flag set on their FaviconCacheElement. That way, the cache always
     // has a record of the existence of a primary payload, even if it is no longer in the cache.
     // This means that when a request comes in that will be best served using a primary that is in
     // the database but no longer cached, we know that it exists and can go get it (Useful when ICO
     // support is added).
-    volatile boolean mInvalidated;
+    volatile boolean invalidated;
 
-    final int mImageSize;
+    final int imageSize;
 
     // Used for LRU pruning.
-    final FaviconsForURL mBackpointer;
+    final FaviconsForURL backpointer;
 
-    public FaviconCacheElement(Bitmap payload, boolean isPrimary, int imageSize, FaviconsForURL backpointer) {
-        mFaviconPayload = payload;
-        mIsPrimary = isPrimary;
-        mImageSize = imageSize;
-        mBackpointer = backpointer;
+    public FaviconCacheElement(Bitmap payload, boolean primary, int size, FaviconsForURL backpointer) {
+        this.faviconPayload = payload;
+        this.isPrimary = primary;
+        this.imageSize = size;
+        this.backpointer = backpointer;
     }
 
-    public FaviconCacheElement(Bitmap payload, boolean isPrimary, FaviconsForURL backpointer) {
-        mFaviconPayload = payload;
-        mIsPrimary = isPrimary;
-        mBackpointer = backpointer;
+    public FaviconCacheElement(Bitmap faviconPayload, boolean isPrimary, FaviconsForURL backpointer) {
+        this.faviconPayload = faviconPayload;
+        this.isPrimary = isPrimary;
+        this.backpointer = backpointer;
 
-        if (payload != null) {
-            mImageSize = payload.getWidth();
+        if (faviconPayload != null) {
+            imageSize = faviconPayload.getWidth();
         } else {
-            mImageSize = 0;
+            imageSize = 0;
         }
     }
 
     public int sizeOf() {
-        if (mInvalidated) {
+        if (invalidated) {
             return 0;
         }
-        return mFaviconPayload.getRowBytes() * mFaviconPayload.getHeight();
+        return faviconPayload.getRowBytes() * faviconPayload.getHeight();
     }
 
     /**
      * Establish an ordering on FaviconCacheElements based on size and validity. An element is
      * considered "greater than" another if it is valid and the other is not, or if it contains a
      * larger payload.
      *
      * @param another The FaviconCacheElement to compare to this one.
      * @return -1 if this element is less than the given one, 1 if the other one is larger than this
      *         and 0 if both are of equal value.
      */
     @Override
     public int compareTo(FaviconCacheElement another) {
-        if (mInvalidated && !another.mInvalidated) {
+        if (invalidated && !another.invalidated) {
             return -1;
         }
 
-        if (!mInvalidated && another.mInvalidated) {
+        if (!invalidated && another.invalidated) {
             return 1;
         }
 
-        if (mInvalidated) {
+        if (invalidated) {
             return 0;
         }
 
-        final int w1 = mImageSize;
-        final int w2 = another.mImageSize;
+        final int w1 = imageSize;
+        final int w2 = another.imageSize;
         if (w1 > w2) {
             return 1;
         } else if (w2 > w1) {
             return -1;
         }
         return 0;
     }
 
     /**
      * Called when this element is evicted from the cache.
      *
      * If primary, drop the payload and set invalid. If secondary, just unlink from parent node.
      */
     public void onEvictedFromCache() {
-        if (mIsPrimary) {
+        if (isPrimary) {
             // So we keep a record of which primaries exist in the database for this URL, we
             // don't actually delete the entry for primaries. Instead, we delete their payload
             // and flag them as invalid. This way, we can later figure out that what a request
             // really want is one of the primaries that have been dropped from the cache, and we
             // can go get it.
-            mInvalidated = true;
-            mFaviconPayload = null;
+            invalidated = true;
+            faviconPayload = null;
         } else {
             // Secondaries don't matter - just delete them.
-            if (mBackpointer == null) {
+            if (backpointer == null) {
                 return;
             }
-            mBackpointer.mFavicons.remove(this);
+            backpointer.favicons.remove(this);
         }
     }
 }
--- a/mobile/android/base/favicons/cache/FaviconsForURL.java
+++ b/mobile/android/base/favicons/cache/FaviconsForURL.java
@@ -9,152 +9,156 @@ import android.util.Log;
 import org.mozilla.gecko.gfx.BitmapUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
 
 public class FaviconsForURL {
     private static final String LOGTAG = "FaviconForURL";
 
-    private volatile int mDominantColor = -1;
+    private volatile int dominantColor = -1;
 
-    final long mDownloadTimestamp;
-    final ArrayList<FaviconCacheElement> mFavicons;
+    final long downloadTimestamp;
+    final ArrayList<FaviconCacheElement> favicons;
 
-    public final boolean mHasFailed;
+    public final boolean hasFailed;
 
     public FaviconsForURL(int size) {
         this(size, false);
     }
 
-    public FaviconsForURL(int size, boolean hasFailed) {
-        mHasFailed = hasFailed;
-        mDownloadTimestamp = System.currentTimeMillis();
-        mFavicons = new ArrayList<FaviconCacheElement>(size);
+    public FaviconsForURL(int size, boolean failed) {
+        hasFailed = failed;
+        downloadTimestamp = System.currentTimeMillis();
+        favicons = new ArrayList<FaviconCacheElement>(size);
     }
 
     public FaviconCacheElement addSecondary(Bitmap favicon, int imageSize) {
         return addInternal(favicon, false, imageSize);
     }
 
     public FaviconCacheElement addPrimary(Bitmap favicon) {
         return addInternal(favicon, true, favicon.getWidth());
     }
 
     private FaviconCacheElement addInternal(Bitmap favicon, boolean isPrimary, int imageSize) {
         FaviconCacheElement c = new FaviconCacheElement(favicon, isPrimary, imageSize, this);
 
-        int index = Collections.binarySearch(mFavicons, c);
+        int index = Collections.binarySearch(favicons, c);
+
+        // We've already got an equivalent one. We don't care about this new one. This only occurs in certain obscure
+        // case conditions.
+        if (index >= 0) {
+            return favicons.get(index);
+        }
 
         // binarySearch returns -x - 1 where x is the insertion point of the element. Convert
         // this to the actual insertion point..
-        if (index < 0) {
-            index++;
-            index = -index;
-        }
-        mFavicons.add(index, c);
+        index++;
+        index = -index;
+        favicons.add(index, c);
 
         return c;
     }
 
     /**
      * Get the index of the smallest image in this collection larger than or equal to
      * the given target size.
      *
      * @param targetSize Minimum size for the desired result.
      * @return The index of the smallest image larger than the target size, or -1 if none exists.
      */
     public int getNextHighestIndex(int targetSize) {
         // Create a dummy object to hold the target value for comparable.
         FaviconCacheElement dummy = new FaviconCacheElement(null, false, targetSize, null);
 
-        int index = Collections.binarySearch(mFavicons, dummy);
+        int index = Collections.binarySearch(favicons, dummy);
 
         // The search routine returns the index of an element equal to dummy, if present.
         // Otherwise, it returns -x - 1, where x is the index in the ArrayList where dummy would be
         // inserted if the list were to remain sorted.
         if (index < 0) {
             index++;
             index = -index;
         }
 
         // index is now 'x', as described above.
 
-        // The routine will return mFavicons.size() as the index iff dummy is larger than all elements
+        // The routine will return favicons.size() as the index iff dummy is larger than all elements
         // present (So the "index at which it should be inserted" is the index after the end.
         // In this case, we set the sentinel value -1 to indicate that we just requested something
         // larger than all primaries.
-        if (index == mFavicons.size()) {
+        if (index == favicons.size()) {
             index = -1;
         }
 
         return index;
     }
 
     /**
      * Get the next valid primary icon from this collection, starting at the given index.
      * If the appropriate icon is found, but is invalid, we return null - the proper response is to
      * reacquire the primary from the database.
      * If no icon is found, the search is repeated going backwards from the start index to find any
      * primary at all (The input index may be a secondary which is larger than the actual available
      * primary.)
      *
-     * @param fromIndex The index into mFavicons from which to start the search.
+     * @param fromIndex The index into favicons from which to start the search.
      * @return The FaviconCacheElement of the next valid primary from the given index. If none exists,
      *         then returns the previous valid primary. If none exists, returns null (Insanity.).
      */
     public FaviconCacheElement getNextPrimary(final int fromIndex) {
-        final int numIcons = mFavicons.size();
+        final int numIcons = favicons.size();
 
         int searchIndex = fromIndex;
         while (searchIndex < numIcons) {
-            FaviconCacheElement element = mFavicons.get(searchIndex);
+            FaviconCacheElement element = favicons.get(searchIndex);
 
-            if (element.mIsPrimary) {
-                if (element.mInvalidated) {
+            if (element.isPrimary) {
+                if (element.invalidated) {
                     // We return null here, despite the possible existence of other primaries,
                     // because we know the most suitable primary for this request exists, but is
                     // no longer in the cache. By returning null, we cause the caller to load the
                     // missing primary from the database and call again.
                     return null;
                 }
                 return element;
             }
             searchIndex++;
         }
 
         // No larger primary available. Let's look for smaller ones...
         searchIndex = fromIndex - 1;
         while (searchIndex >= 0) {
-            FaviconCacheElement element = mFavicons.get(searchIndex);
+            FaviconCacheElement element = favicons.get(searchIndex);
 
-            if (element.mIsPrimary) {
-                if (element.mInvalidated) {
+            if (element.isPrimary) {
+                if (element.invalidated) {
                     return null;
                 }
                 return element;
             }
             searchIndex--;
         }
 
         Log.e(LOGTAG, "No primaries found in Favicon cache structure. This is madness!");
 
         return null;
     }
 
     /**
      * Ensure the dominant colour field is populated for this favicon.
      */
     public int ensureDominantColor() {
-        if (mDominantColor == -1) {
+        if (dominantColor == -1) {
             // Find a payload, any payload, that is not invalidated.
-            for (FaviconCacheElement element : mFavicons) {
-                if (!element.mInvalidated) {
-                    mDominantColor = BitmapUtils.getDominantColor(element.mFaviconPayload);
-                    return mDominantColor;
+            for (FaviconCacheElement element : favicons) {
+                if (!element.invalidated) {
+                    dominantColor = BitmapUtils.getDominantColor(element.faviconPayload);
+                    return dominantColor;
                 }
             }
-            mDominantColor = 0xFFFFFF;
+            dominantColor = 0xFFFFFF;
         }
 
-        return mDominantColor;
+        return dominantColor;
     }
 }
--- a/mobile/android/base/favicons/decoders/FaviconDecoder.java
+++ b/mobile/android/base/favicons/decoders/FaviconDecoder.java
@@ -84,24 +84,24 @@ public class FaviconDecoder {
      * @param length The length of the region in the array to decode.
      * @return The decoded version of the bitmap in the described region, or null if none can be
      *         decoded.
      */
     public static LoadFaviconResult decodeFavicon(byte[] buffer, int offset, int length) {
         LoadFaviconResult result;
         if (isDecodableByAndroid(buffer, offset)) {
             result = new LoadFaviconResult();
-            result.mOffset = offset;
-            result.mLength = length;
-            result.mIsICO = false;
+            result.offset = offset;
+            result.length = length;
+            result.isICO = false;
 
             // We assume here that decodeByteArray doesn't hold on to the entire supplied
             // buffer -- worst case, each of our buffers will be twice the necessary size.
-            result.mBitmapsDecoded = new SingleBitmapIterator(BitmapUtils.decodeByteArray(buffer, offset, length));
-            result.mFaviconBytes = buffer;
+            result.bitmapsDecoded = new SingleBitmapIterator(BitmapUtils.decodeByteArray(buffer, offset, length));
+            result.faviconBytes = buffer;
 
             return result;
         }
 
         // If it's not decodable by Android, it might be an ICO. Let's try.
         ICODecoder decoder = new ICODecoder(buffer, offset, length);
 
         result = decoder.decode();
@@ -188,46 +188,46 @@ public class FaviconDecoder {
 
         return bitmap;
     }
 
     /**
      * Iterator to hold a single bitmap.
      */
     static class SingleBitmapIterator implements Iterator<Bitmap> {
-        private Bitmap mBitmap;
+        private Bitmap bitmap;
 
         public SingleBitmapIterator(Bitmap b) {
-            mBitmap = b;
+            bitmap = b;
         }
 
         /**
          * Slightly cheating here - this iterator supports peeking (Handy in a couple of obscure
          * places where the runtime type of the Iterator under consideration is known and
          * destruction of it is discouraged.
          *
          * @return The bitmap carried by this SingleBitmapIterator.
          */
         public Bitmap peek() {
-            return mBitmap;
+            return bitmap;
         }
 
         @Override
         public boolean hasNext() {
-            return mBitmap != null;
+            return bitmap != null;
         }
 
         @Override
         public Bitmap next() {
-            if (mBitmap == null) {
+            if (bitmap == null) {
                 throw new NoSuchElementException("Element already returned from SingleBitmapIterator.");
             }
 
-            Bitmap ret = mBitmap;
-            mBitmap = null;
+            Bitmap ret = bitmap;
+            bitmap = null;
             return ret;
         }
 
         @Override
         public void remove() {
             throw new UnsupportedOperationException("remove() not supported on SingleBitmapIterator.");
         }
     }
--- a/mobile/android/base/favicons/decoders/ICODecoder.java
+++ b/mobile/android/base/favicons/decoders/ICODecoder.java
@@ -5,17 +5,16 @@
 package org.mozilla.gecko.favicons.decoders;
 
 import android.graphics.Bitmap;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.gfx.BitmapUtils;
 
 import android.util.SparseArray;
 
-import java.util.Collection;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
 /**
  * Utility class for determining the region of a provided array which contains the largest bitmap,
  * assuming the provided array is a valid ICO and the bitmap desired is square, and for pruning
  * unwanted entries from ICO files, if desired.
  *
@@ -74,229 +73,229 @@ public class ICODecoder implements Itera
     // The number of bytes that compacting will save for us to bother doing it.
     public static final int COMPACT_THRESHOLD = 4000;
 
     // Some geometry of an ICO file.
     public static final int ICO_HEADER_LENGTH_BYTES = 6;
     public static final int ICO_ICONDIRENTRY_LENGTH_BYTES = 16;
 
     // The buffer containing bytes to attempt to decode.
-    private byte[] mDecodand;
+    private byte[] decodand;
 
     // The region of the decodand to decode.
-    private int mOffset;
-    private int mLen;
+    private int offset;
+    private int len;
 
-    private IconDirectoryEntry[] mIconDirectory;
-    private boolean mIsValid;
-    private boolean mHasDecoded;
+    private IconDirectoryEntry[] iconDirectory;
+    private boolean isValid;
+    private boolean hasDecoded;
 
-    public ICODecoder(byte[] buffer, int offset, int len) {
-        mDecodand = buffer;
-        mOffset = offset;
-        mLen = len;
+    public ICODecoder(byte[] decodand, int offset, int len) {
+        this.decodand = decodand;
+        this.offset = offset;
+        this.len = len;
     }
 
     /**
-     * Decode the Icon Directory for this ICO and store the result in mIconDirectory.
+     * Decode the Icon Directory for this ICO and store the result in iconDirectory.
      *
      * @return true if ICO decoding was considered to probably be a success, false if it certainly
      *         was a failure.
      */
     private boolean decodeIconDirectoryAndPossiblyPrune() {
-        mHasDecoded = true;
+        hasDecoded = true;
 
         // Fail if the end of the described range is out of bounds.
-        if (mOffset + mLen > mDecodand.length) {
+        if (offset + len > decodand.length) {
             return false;
         }
 
         // Fail if we don't have enough space for the header.
-        if (mLen < ICO_HEADER_LENGTH_BYTES) {
+        if (len < ICO_HEADER_LENGTH_BYTES) {
             return false;
         }
 
         // Check that the reserved fields in the header are indeed zero, and that the type field
         // specifies ICO. If not, we've probably been given something that isn't really an ICO.
-        if (mDecodand[mOffset] != 0 ||
-            mDecodand[mOffset + 1] != 0 ||
-            mDecodand[mOffset + 2] != 1 ||
-            mDecodand[mOffset + 3] != 0) {
+        if (decodand[offset] != 0 ||
+            decodand[offset + 1] != 0 ||
+            decodand[offset + 2] != 1 ||
+            decodand[offset + 3] != 0) {
             return false;
         }
 
         // Here, and in many other places, byte values are ANDed with 0xFF. This is because Java
         // bytes are signed - to obtain a numerical value of a longer type which holds the unsigned
         // interpretation of the byte of interest, we do this.
-        int numEncodedImages = (mDecodand[mOffset + 4] & 0xFF) |
-                               (mDecodand[mOffset + 5] & 0xFF) << 8;
+        int numEncodedImages = (decodand[offset + 4] & 0xFF) |
+                               (decodand[offset + 5] & 0xFF) << 8;
 
 
         // Fail if there are no images or the field is corrupt.
         if (numEncodedImages <= 0) {
             return false;
         }
 
         final int headerAndDirectorySize = ICO_HEADER_LENGTH_BYTES + (numEncodedImages * ICO_ICONDIRENTRY_LENGTH_BYTES);
 
         // Fail if there is not enough space in the buffer for the stated number of icondir entries,
         // let alone the data.
-        if (mLen < headerAndDirectorySize) {
+        if (len < headerAndDirectorySize) {
             return false;
         }
 
         // Put the pointer on the first byte of the first Icon Directory Entry.
-        int bufferIndex = mOffset + ICO_HEADER_LENGTH_BYTES;
+        int bufferIndex = offset + ICO_HEADER_LENGTH_BYTES;
 
         // We now iterate over the Icon Directory, decoding each entry as we go. We also need to
         // discard all entries except one >= the maximum interesting size.
 
         // Size of the smallest image larger than the limit encountered.
         int minimumMaximum = Integer.MAX_VALUE;
 
         // Used to track the best entry for each size. The entries we want to keep.
         SparseArray<IconDirectoryEntry> preferenceArray = new SparseArray<IconDirectoryEntry>();
 
         for (int i = 0; i < numEncodedImages; i++, bufferIndex += ICO_ICONDIRENTRY_LENGTH_BYTES) {
             // Decode the Icon Directory Entry at this offset.
-            IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(mDecodand, mOffset, mLen, bufferIndex);
-            newEntry.mIndex = i;
+            IconDirectoryEntry newEntry = IconDirectoryEntry.createFromBuffer(decodand, offset, len, bufferIndex);
+            newEntry.index = i;
 
-            if (newEntry.mIsErroneous) {
+            if (newEntry.isErroneous) {
                 continue;
             }
 
-            if (newEntry.mWidth > Favicons.sLargestFaviconSize) {
+            if (newEntry.width > Favicons.largestFaviconSize) {
                 // If we already have a smaller image larger than the maximum size of interest, we
                 // don't care about the new one which is larger than the smallest image larger than
                 // the maximum size.
-                if (newEntry.mWidth >= minimumMaximum) {
+                if (newEntry.width >= minimumMaximum) {
                     continue;
                 }
 
                 // Remove the previous minimum-maximum.
                 preferenceArray.delete(minimumMaximum);
 
-                minimumMaximum = newEntry.mWidth;
+                minimumMaximum = newEntry.width;
             }
 
-            IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.mWidth);
+            IconDirectoryEntry oldEntry = preferenceArray.get(newEntry.width);
             if (oldEntry == null) {
-                preferenceArray.put(newEntry.mWidth, newEntry);
+                preferenceArray.put(newEntry.width, newEntry);
                 continue;
             }
 
             if (oldEntry.compareTo(newEntry) < 0) {
-                preferenceArray.put(newEntry.mWidth, newEntry);
+                preferenceArray.put(newEntry.width, newEntry);
             }
         }
 
         final int count = preferenceArray.size();
 
         // Abort if no entries are desired (Perhaps all are corrupt?)
         if (count == 0) {
             return false;
         }
 
         // Allocate space for the icon directory entries in the decoded directory.
-        mIconDirectory = new IconDirectoryEntry[count];
+        iconDirectory = new IconDirectoryEntry[count];
 
         // The size of the data in the buffer that we find useful.
         int retainedSpace = ICO_HEADER_LENGTH_BYTES;
 
         for (int i = 0; i < count; i++) {
             IconDirectoryEntry e = preferenceArray.valueAt(i);
-            retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.mPayloadSize;
-            mIconDirectory[i] = e;
+            retainedSpace += ICO_ICONDIRENTRY_LENGTH_BYTES + e.payloadSize;
+            iconDirectory[i] = e;
         }
 
-        mIsValid = true;
+        isValid = true;
 
         // Set the number of images field in the buffer to reflect the number of retained entries.
-        mDecodand[mOffset + 4] = (byte) mIconDirectory.length;
-        mDecodand[mOffset + 5] = (byte) (mIconDirectory.length >>> 8);
+        decodand[offset + 4] = (byte) iconDirectory.length;
+        decodand[offset + 5] = (byte) (iconDirectory.length >>> 8);
 
-        if ((mLen - retainedSpace) > COMPACT_THRESHOLD) {
+        if ((len - retainedSpace) > COMPACT_THRESHOLD) {
             compactingCopy(retainedSpace);
         }
 
         return true;
     }
 
     /**
      * Copy the buffer into a new array of exactly the required size, omitting any unwanted data.
      */
     private void compactingCopy(int spaceRetained) {
         byte[] buf = new byte[spaceRetained];
 
         // Copy the header.
-        System.arraycopy(mDecodand, mOffset, buf, 0, ICO_HEADER_LENGTH_BYTES);
+        System.arraycopy(decodand, offset, buf, 0, ICO_HEADER_LENGTH_BYTES);
 
         int headerPtr = ICO_HEADER_LENGTH_BYTES;
 
-        int payloadPtr = ICO_HEADER_LENGTH_BYTES + (mIconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
+        int payloadPtr = ICO_HEADER_LENGTH_BYTES + (iconDirectory.length * ICO_ICONDIRENTRY_LENGTH_BYTES);
 
         int ind = 0;
-        for (IconDirectoryEntry entry : mIconDirectory) {
+        for (IconDirectoryEntry entry : iconDirectory) {
             // Copy this entry.
-            System.arraycopy(mDecodand, mOffset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
+            System.arraycopy(decodand, offset + entry.getOffset(), buf, headerPtr, ICO_ICONDIRENTRY_LENGTH_BYTES);
 
             // Copy its payload.
-            System.arraycopy(mDecodand, mOffset + entry.mPayloadOffset, buf, payloadPtr, entry.mPayloadSize);
+            System.arraycopy(decodand, offset + entry.payloadOffset, buf, payloadPtr, entry.payloadSize);
 
             // Update the offset field.
             buf[headerPtr + 12] = (byte) payloadPtr;
             buf[headerPtr + 13] = (byte) (payloadPtr >>> 8);
             buf[headerPtr + 14] = (byte) (payloadPtr >>> 16);
             buf[headerPtr + 15] = (byte) (payloadPtr >>> 24);
 
-            entry.mPayloadOffset = payloadPtr;
-            entry.mIndex = ind;
+            entry.payloadOffset = payloadPtr;
+            entry.index = ind;
 
-            payloadPtr += entry.mPayloadSize;
+            payloadPtr += entry.payloadSize;
             headerPtr += ICO_ICONDIRENTRY_LENGTH_BYTES;
             ind++;
         }
 
-        mDecodand = buf;
-        mOffset = 0;
-        mLen = spaceRetained;
+        decodand = buf;
+        offset = 0;
+        len = spaceRetained;
     }
 
     /**
      * Decode and return the bitmap represented by the given index in the Icon Directory, if valid.
      *
      * @param index The index into the Icon Directory of the image of interest.
      * @return The decoded Bitmap object for this image, or null if the entry is invalid or decoding
      *         fails.
      */
     public Bitmap decodeBitmapAtIndex(int index) {
-        final IconDirectoryEntry iconDirEntry = mIconDirectory[index];
+        final IconDirectoryEntry iconDirEntry = iconDirectory[index];
 
-        if (iconDirEntry.mPayloadIsPNG) {
+        if (iconDirEntry.payloadIsPNG) {
             // PNG payload. Simply extract it and decode it.
-            return BitmapUtils.decodeByteArray(mDecodand, mOffset + iconDirEntry.mPayloadOffset, iconDirEntry.mPayloadSize);
+            return BitmapUtils.decodeByteArray(decodand, offset + iconDirEntry.payloadOffset, iconDirEntry.payloadSize);
         }
 
         // The payload is a BMP, so we need to do some magic to get the decoder to do what we want.
         // We construct an ICO containing just the image we want, and let Android do the rest.
-        byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.mPayloadSize];
+        byte[] decodeTarget = new byte[ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES + iconDirEntry.payloadSize];
 
         // Set the type field in the ICO header.
         decodeTarget[2] = 1;
 
         // Set the num-images field in the header to 1.
         decodeTarget[4] = 1;
 
         // Copy the ICONDIRENTRY we need into the new buffer.
-        System.arraycopy(mDecodand, mOffset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
+        System.arraycopy(decodand, offset + iconDirEntry.getOffset(), decodeTarget, ICO_HEADER_LENGTH_BYTES, ICO_ICONDIRENTRY_LENGTH_BYTES);
 
         // Copy the payload into the new buffer.
         final int singlePayloadOffset =  ICO_HEADER_LENGTH_BYTES + ICO_ICONDIRENTRY_LENGTH_BYTES;
-        System.arraycopy(mDecodand, mOffset + iconDirEntry.mPayloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.mPayloadSize);
+        System.arraycopy(decodand, offset + iconDirEntry.payloadOffset, decodeTarget, singlePayloadOffset, iconDirEntry.payloadSize);
 
         // Update the offset field of the ICONDIRENTRY to make the new ICO valid.
         decodeTarget[ICO_HEADER_LENGTH_BYTES + 12] = (byte) singlePayloadOffset;
         decodeTarget[ICO_HEADER_LENGTH_BYTES + 13] = (byte) (singlePayloadOffset >>> 8);
         decodeTarget[ICO_HEADER_LENGTH_BYTES + 14] = (byte) (singlePayloadOffset >>> 16);
         decodeTarget[ICO_HEADER_LENGTH_BYTES + 15] = (byte) (singlePayloadOffset >>> 24);
 
         // Decode the newly-constructed singleton-ICO.
@@ -306,22 +305,22 @@ public class ICODecoder implements Itera
     /**
      * Fetch an iterator over the images in this ICO, or null if this ICO seems to be invalid.
      *
      * @return An iterator over the Bitmaps stored in this ICO, or null if decoding fails.
      */
     @Override
     public ICOIterator iterator() {
         // If a previous call to decode concluded this ICO is invalid, abort.
-        if (mHasDecoded && !mIsValid) {
+        if (hasDecoded && !isValid) {
             return null;
         }
 
         // If we've not been decoded before, but now fail to make any sense of the ICO, abort.
-        if (!mHasDecoded) {
+        if (!hasDecoded) {
             if (!decodeIconDirectoryAndPossiblyPrune()) {
                 return null;
             }
         }
 
         // If decoding was a success, return an iterator over the images in this ICO.
         return new ICOIterator();
     }
@@ -334,45 +333,45 @@ public class ICODecoder implements Itera
         // The call to iterator returns null if decoding fails.
         Iterator<Bitmap> bitmaps = iterator();
         if (bitmaps == null) {
             return null;
         }
 
         LoadFaviconResult result = new LoadFaviconResult();
 
-        result.mBitmapsDecoded = bitmaps;
-        result.mFaviconBytes = mDecodand;
-        result.mOffset = mOffset;
-        result.mLength = mLen;
-        result.mIsICO = true;
+        result.bitmapsDecoded = bitmaps;
+        result.faviconBytes = decodand;
+        result.offset = offset;
+        result.length = len;
+        result.isICO = true;
 
         return result;
     }
 
     /**
      * Inner class to iterate over the elements in the ICO represented by the enclosing instance.
      */
     private class ICOIterator implements Iterator<Bitmap> {
         private int mIndex = 0;
 
         @Override
         public boolean hasNext() {
-            return mIndex < mIconDirectory.length;
+            return mIndex < iconDirectory.length;
         }
 
         @Override
         public Bitmap next() {
-            if (mIndex > mIconDirectory.length) {
+            if (mIndex > iconDirectory.length) {
                 throw new NoSuchElementException("No more elements in this ICO.");
             }
             return decodeBitmapAtIndex(mIndex++);
         }
 
         @Override
         public void remove() {
-            if (mIconDirectory[mIndex] == null) {
+            if (iconDirectory[mIndex] == null) {
                 throw new IllegalStateException("Remove already called for element " + mIndex);
             }
-            mIconDirectory[mIndex] = null;
+            iconDirectory[mIndex] = null;
         }
     }
 }
--- a/mobile/android/base/favicons/decoders/IconDirectoryEntry.java
+++ b/mobile/android/base/favicons/decoders/IconDirectoryEntry.java
@@ -4,48 +4,48 @@
 
 package org.mozilla.gecko.favicons.decoders;
 
 /**
  * Representation of an ICO file ICONDIRENTRY structure.
  */
 public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
 
-    public static int sMaxBPP;
+    public static int maxBPP;
 
-    int mWidth;
-    int mHeight;
-    int mPaletteSize;
-    int mBitsPerPixel;
-    int mPayloadSize;
-    int mPayloadOffset;
-    boolean mPayloadIsPNG;
+    int width;
+    int height;
+    int paletteSize;
+    int bitsPerPixel;
+    int payloadSize;
+    int payloadOffset;
+    boolean payloadIsPNG;
 
     // Tracks the index in the Icon Directory of this entry. Useful only for pruning.
-    int mIndex;
-    boolean mIsErroneous;
+    int index;
+    boolean isErroneous;
 
     public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
-        mWidth = width;
-        mHeight = height;
-        mPaletteSize = paletteSize;
-        mBitsPerPixel = bitsPerPixel;
-        mPayloadSize = payloadSize;
-        mPayloadOffset = payloadOffset;
-        mPayloadIsPNG = payloadIsPNG;
+        this.width = width;
+        this.height = height;
+        this.paletteSize = paletteSize;
+        this.bitsPerPixel = bitsPerPixel;
+        this.payloadSize = payloadSize;
+        this.payloadOffset = payloadOffset;
+        this.payloadIsPNG = payloadIsPNG;
     }
 
     /**
      * Method to get a dummy Icon Directory Entry with the Erroneous bit set.
      *
      * @return An erroneous placeholder Icon Directory Entry.
      */
     public static IconDirectoryEntry getErroneousEntry() {
         IconDirectoryEntry ret = new IconDirectoryEntry(-1, -1, -1, -1, -1, -1, false);
-        ret.mIsErroneous = true;
+        ret.isErroneous = true;
 
         return ret;
     }
 
     /**
      * Create an IconDirectoryEntry object from a byte[]. Interprets the buffer starting at the given
      * offset as an IconDirectoryEntry and returns the result.
      *
@@ -113,89 +113,89 @@ public class IconDirectoryEntry implemen
 
         return new IconDirectoryEntry(imageWidth, imageHeight, paletteSize, bitsPerPixel, entryLength, payloadOffset, payloadIsPNG);
     }
 
     /**
      * Get the number of bytes from the start of the ICO file to the beginning of this entry.
      */
     public int getOffset() {
-        return ICODecoder.ICO_HEADER_LENGTH_BYTES + (mIndex * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
+        return ICODecoder.ICO_HEADER_LENGTH_BYTES + (index * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
     }
 
     @Override
     public int compareTo(IconDirectoryEntry another) {
-        if (mWidth > another.mWidth) {
+        if (width > another.width) {
             return 1;
         }
 
-        if (mWidth < another.mWidth) {
+        if (width < another.width) {
             return -1;
         }
 
         // Where both images exceed the max BPP, take the smaller of the two BPP values.
-        if (mBitsPerPixel >= sMaxBPP && another.mBitsPerPixel >= sMaxBPP) {
-            if (mBitsPerPixel < another.mBitsPerPixel) {
+        if (bitsPerPixel >= maxBPP && another.bitsPerPixel >= maxBPP) {
+            if (bitsPerPixel < another.bitsPerPixel) {
                 return 1;
             }
 
-            if (mBitsPerPixel > another.mBitsPerPixel) {
+            if (bitsPerPixel > another.bitsPerPixel) {
                 return -1;
             }
         }
 
         // Otherwise, take the larger of the BPP values.
-        if (mBitsPerPixel > another.mBitsPerPixel) {
+        if (bitsPerPixel > another.bitsPerPixel) {
             return 1;
         }
 
-        if (mBitsPerPixel < another.mBitsPerPixel) {
+        if (bitsPerPixel < another.bitsPerPixel) {
             return -1;
         }
 
         // Prefer large palettes.
-        if (mPaletteSize > another.mPaletteSize) {
+        if (paletteSize > another.paletteSize) {
             return 1;
         }
 
-        if (mPaletteSize < another.mPaletteSize) {
+        if (paletteSize < another.paletteSize) {
             return -1;
         }
 
         // Prefer smaller payloads.
-        if (mPayloadSize < another.mPayloadSize) {
+        if (payloadSize < another.payloadSize) {
             return 1;
         }
 
-        if (mPayloadSize > another.mPayloadSize) {
+        if (payloadSize > another.payloadSize) {
             return -1;
         }
 
         // If all else fails, prefer PNGs over BMPs. They tend to be smaller.
-        if (mPayloadIsPNG && !another.mPayloadIsPNG) {
+        if (payloadIsPNG && !another.payloadIsPNG) {
             return 1;
         }
 
-        if (!mPayloadIsPNG && another.mPayloadIsPNG) {
+        if (!payloadIsPNG && another.payloadIsPNG) {
             return -1;
         }
 
         return 0;
     }
 
     public static void setMaxBPP(int maxBPP) {
-        sMaxBPP = maxBPP;
+        IconDirectoryEntry.maxBPP = maxBPP;
     }
 
     @Override
     public String toString() {
         return "IconDirectoryEntry{" +
-                "\nmWidth=" + mWidth +
-                ", \nmHeight=" + mHeight +
-                ", \nmPaletteSize=" + mPaletteSize +
-                ", \nmBitsPerPixel=" + mBitsPerPixel +
-                ", \nmPayloadSize=" + mPayloadSize +
-                ", \nmPayloadOffset=" + mPayloadOffset +
-                ", \nmPayloadIsPNG=" + mPayloadIsPNG +
-                ", \nmIndex=" + mIndex +
+                "\nwidth=" + width +
+                ", \nheight=" + height +
+                ", \npaletteSize=" + paletteSize +
+                ", \nbitsPerPixel=" + bitsPerPixel +
+                ", \npayloadSize=" + payloadSize +
+                ", \npayloadOffset=" + payloadOffset +
+                ", \npayloadIsPNG=" + payloadIsPNG +
+                ", \nindex=" + index +
                 '}';
     }
 }
--- a/mobile/android/base/favicons/decoders/LoadFaviconResult.java
+++ b/mobile/android/base/favicons/decoders/LoadFaviconResult.java
@@ -16,46 +16,46 @@ import java.util.Iterator;
  * It is necessary to model single favicons differently to a collection of one favicon (An entity
  * that may not exist with this scheme) since the in-database representation of these things differ.
  * (In particular, collections of favicons are stored in encoded ICO format, whereas single icons are
  * stored as decoded bitmap blobs.)
  */
 public class LoadFaviconResult {
     private static final String LOGTAG = "LoadFaviconResult";
 
-    byte[] mFaviconBytes;
-    int mOffset;
-    int mLength;
+    byte[] faviconBytes;
+    int offset;
+    int length;
 
-    boolean mIsICO;
-    Iterator<Bitmap> mBitmapsDecoded;
+    boolean isICO;
+    Iterator<Bitmap> bitmapsDecoded;
 
     public Iterator<Bitmap> getBitmaps() {
-        return mBitmapsDecoded;
+        return bitmapsDecoded;
     }
 
     /**
      * Return a representation of this result suitable for storing in the database.
      * For
      *
      * @return A byte array containing the bytes from which this result was decoded.
      */
     public byte[] getBytesForDatabaseStorage() {
         // Begin by normalising the buffer.
-        if (mOffset != 0 || mLength != mFaviconBytes.length) {
-            final byte[] normalised = new byte[mLength];
-            System.arraycopy(mFaviconBytes, mOffset, normalised, 0, mLength);
-            mOffset = 0;
-            mFaviconBytes = normalised;
+        if (offset != 0 || length != faviconBytes.length) {
+            final byte[] normalised = new byte[length];
+            System.arraycopy(faviconBytes, offset, normalised, 0, length);
+            offset = 0;
+            faviconBytes = normalised;
         }
 
         // For results containing a single image, we re-encode the result as a PNG in an effort to
         // save space.
-        if (!mIsICO) {
-            Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) mBitmapsDecoded).peek();
+        if (!isICO) {
+            Bitmap favicon = ((FaviconDecoder.SingleBitmapIterator) bitmapsDecoded).peek();
             byte[] data = null;
             ByteArrayOutputStream stream = new ByteArrayOutputStream();
 
             if (favicon.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
                 data = stream.toByteArray();
             } else {
                 Log.w(LOGTAG, "Favicon compression failed.");
             }
@@ -63,12 +63,12 @@ public class LoadFaviconResult {
             return data;
         }
 
         // For results containing multiple images, we store the result verbatim. (But cutting the
         // buffer to size first).
         // We may instead want to consider re-encoding the entire ICO as a collection of efficiently
         // encoded PNGs. This may not be worth the CPU time (Indeed, the encoding of single-image
         // favicons may also not be worth the time/space tradeoff.).
-        return mFaviconBytes;
+        return faviconBytes;
     }
 
 }
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -563,17 +563,17 @@ public class BrowserSearch extends HomeF
         // View hierarchy so a second call to findViewById will return null.
         if (mSuggestionsOptInPrompt != null) {
             return;
         }
 
         mSuggestionsOptInPrompt = ((ViewStub) mView.findViewById(R.id.suggestions_opt_in_prompt)).inflate();
 
         TextView promptText = (TextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title);
-        promptText.setText(getResources().getString(R.string.suggestions_prompt, mSearchEngines.get(0).name));
+        promptText.setText(getResources().getString(R.string.suggestions_prompt));
 
         final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes);
         final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no);
 
         final OnClickListener listener = new OnClickListener() {
             @Override
             public void onClick(View v) {
                 // Prevent the buttons from being clicked multiple times (bug 816902)
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -373,19 +373,17 @@ just addresses the organization to follo
 <!ENTITY bookmarkhistory_import_both "Importing bookmarks and history
                                       from Android">
 <!ENTITY bookmarkhistory_import_bookmarks "Importing bookmarks
                                            from Android">
 <!ENTITY bookmarkhistory_import_history "Importing history
                                          from Android">
 <!ENTITY bookmarkhistory_import_wait "Please wait...">
 
-<!-- Localization note (suggestions_prompt2): The placeholder &formatS; will be
-     replaced with the name of the search engine. -->
-<!ENTITY suggestions_prompt2 "Would you like to turn on &formatS; search suggestions?">
+<!ENTITY suggestions_prompt3 "Would you like to turn on search suggestions?">
 
 <!-- Localization note (suggestion_for_engine): The placeholder &formatS1; will be
      replaced with the name of the search engine. The placeholder &formatS2; will be
      replaced with the search query. -->
 <!ENTITY suggestion_for_engine "Search &formatS1; for &formatS2;">
 
 <!ENTITY webapp_generic_name "App">
 
--- a/mobile/android/base/preferences/SearchEnginePreference.java
+++ b/mobile/android/base/preferences/SearchEnginePreference.java
@@ -128,23 +128,23 @@ public class SearchEnginePreference exte
 
         final String iconURI = geckoEngineJSON.getString("iconURI");
         // Keep a reference to the bitmap - we'll need it later in onBindView.
         try {
             final int desiredWidth;
             if (mFaviconView != null) {
                 desiredWidth = mFaviconView.getWidth();
             } else {
-                // sLargestFaviconSize is initialized when Favicons is attached to a
+                // largestFaviconSize is initialized when Favicons is attached to a
                 // context, which occurs during GeckoApp.onCreate. That might not
                 // ever happen (leaving it at 0), so we fall back.
-                if (Favicons.sLargestFaviconSize == 0) {
+                if (Favicons.largestFaviconSize == 0) {
                     desiredWidth = 128;
                 } else {
-                    desiredWidth = Favicons.sLargestFaviconSize;
+                    desiredWidth = Favicons.largestFaviconSize;
                 }
             }
 
             // TODO: use the cache. Bug 961600.
             mIconBitmap = FaviconDecoder.getMostSuitableBitmapFromDataURI(iconURI, desiredWidth);
 
         } catch (IllegalArgumentException e) {
             Log.e(LOGTAG, "IllegalArgumentException creating Bitmap. Most likely a zero-length bitmap.", e);
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -365,17 +365,17 @@
   <string name="updater_downloading_title_failed">&updater_downloading_title_failed2;</string>
   <string name="updater_downloading_select">&updater_downloading_select2;</string>
   <string name="updater_downloading_retry">&updater_downloading_retry2;</string>
 
   <string name="updater_apply_title">&updater_apply_title2;</string>
   <string name="updater_apply_select">&updater_apply_select2;</string>
 
   <!-- Search suggestions opt-in -->
-  <string name="suggestions_prompt">&suggestions_prompt2;</string>
+  <string name="suggestions_prompt">&suggestions_prompt3;</string>
 
   <string name="suggestion_for_engine">&suggestion_for_engine;</string>
 
   <!-- Set Image Notifications -->
   <string name="set_image_fail">&set_image_fail;</string>
   <string name="set_image_chooser_title">&set_image_chooser_title;</string>
 
   <!-- Contacts API -->
--- a/mobile/android/base/tests/testHomeProvider.js
+++ b/mobile/android/base/tests/testHomeProvider.js
@@ -8,16 +8,17 @@ const { utils: Cu } = Components;
 Cu.import("resource://gre/modules/HomeProvider.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Sqlite.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 const TEST_DATASET_ID = "test-dataset-id";
 const TEST_URL = "http://test.com";
+const TEST_TITLE = "Test";
 
 const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
 const TEST_INTERVAL_SECS = 1;
 
 const DB_PATH = OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
 
 add_test(function test_request_sync() {
   // The current implementation of requestSync is synchronous.
@@ -42,17 +43,17 @@ add_test(function test_periodic_sync() {
     do_check_eq(datasetId, TEST_DATASET_ID);
     run_next_test();
   });
 });
 
 add_task(function test_save_and_delete() {
   // Use the HomeProvider API to save some data.
   let storage = HomeProvider.getStorage(TEST_DATASET_ID);
-  yield storage.save([{ url: TEST_URL }]);
+  yield storage.save([{ title: TEST_TITLE, url: TEST_URL }]);
 
   // Peek in the DB to make sure we have the right data.
   let db = yield Sqlite.openConnection({ path: DB_PATH });
 
   // Make sure the items table was created.
   do_check_true(yield db.tableExists("items"));
 
   // Make sure the correct values for the item ended up in there.
@@ -66,9 +67,66 @@ add_task(function test_save_and_delete()
 
   // Make sure the data was deleted.
   let result = yield db.execute("SELECT * FROM items");
   do_check_eq(result.length, 0);
 
   db.close();
 });
 
+add_task(function test_row_validation() {
+  // Use the HomeProvider API to save some data.
+  let storage = HomeProvider.getStorage(TEST_DATASET_ID);
+
+  let invalidRows = [
+    { url: "url" },
+    { title: "title" },
+    { description: "description" },
+    { image_url: "image_url" }
+  ];
+
+  // None of these save calls should save anything
+  for (let row of invalidRows) {
+    try {
+      yield storage.save([row]);
+    } catch (e if e instanceof HomeProvider.ValidationError) {
+      // Just catch and ignore validation errors
+    }
+  }
+
+  // Peek in the DB to make sure we have the right data.
+  let db = yield Sqlite.openConnection({ path: DB_PATH });
+
+  // Make sure no data has been saved.
+  let result = yield db.execute("SELECT * FROM items");
+  do_check_eq(result.length, 0);
+
+  db.close();
+});
+
+add_task(function test_save_transaction() {
+  // Use the HomeProvider API to save some data.
+  let storage = HomeProvider.getStorage(TEST_DATASET_ID);
+
+  // One valid, one invalid
+  let rows = [
+    { title: TEST_TITLE, url: TEST_URL },
+    { image_url: "image_url" }
+  ];
+
+  // Try to save all the rows at once
+  try {
+    yield storage.save(rows);
+  } catch (e if e instanceof HomeProvider.ValidationError) {
+    // Just catch and ignore validation errors
+  }
+
+  // Peek in the DB to make sure we have the right data.
+  let db = yield Sqlite.openConnection({ path: DB_PATH });
+
+  // Make sure no data has been saved.
+  let result = yield db.execute("SELECT * FROM items");
+  do_check_eq(result.length, 0);
+
+  db.close();
+});
+
 run_next_test();
--- a/mobile/android/components/HelperAppDialog.js
+++ b/mobile/android/components/HelperAppDialog.js
@@ -38,65 +38,58 @@ HelperAppLauncherDialog.prototype = {
 
   /**
    * Returns false if `url` represents a local or special URL that we don't
    * wish to ever download.
    *
    * Returns true otherwise.
    */
   _canDownload: function (url, alreadyResolved=false) {
-    Services.console.logStringMessage("_canDownload: " + url);
     // The common case.
     if (url.schemeIs("http") ||
         url.schemeIs("https") ||
         url.schemeIs("ftp")) {
-      Services.console.logStringMessage("_canDownload: true\n");
       return true;
     }
 
     // The less-common opposite case.
     if (url.schemeIs("chrome") ||
         url.schemeIs("jar") ||
         url.schemeIs("resource") ||
         url.schemeIs("wyciwyg")) {
-      Services.console.logStringMessage("_canDownload: false\n");
       return false;
     }
 
     // For all other URIs, try to resolve them to an inner URI, and check that.
     if (!alreadyResolved) {
       let ioSvc = Cc["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
       let innerURI = ioSvc.newChannelFromURI(url).URI;
       if (!url.equals(innerURI)) {
-        Services.console.logStringMessage("_canDownload: recursing.\n");
         return this._canDownload(innerURI, true);
       }
     }
 
     if (url.schemeIs("file")) {
       // If it's in our app directory or profile directory, we never ever
       // want to do anything with it, including saving to disk or passing the
       // file to another application.
       let file = url.QueryInterface(Ci.nsIFileURL).file;
 
       // TODO: pref blacklist?
 
       let appRoot = FileUtils.getFile("XREExeF", []);
       if (appRoot.contains(file, true)) {
-        Services.console.logStringMessage("_canDownload: appRoot.\n");
         return false;
       }
 
       let profileRoot = FileUtils.getFile("ProfD", []);
       if (profileRoot.contains(file, true)) {
-        Services.console.logStringMessage("_canDownload: prof dir.\n");
         return false;
       }
 
-      Services.console.logStringMessage("_canDownload: safe.\n");
       return true;
     }
 
     // Anything else is fine to download.
     return true;
   },
 
   /**
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -377,16 +377,17 @@
 @BINPATH@/components/messageWakeupService.manifest
 #ifdef MOZ_ENABLE_DBUS
 @BINPATH@/components/@DLL_PREFIX@dbusservice@DLL_SUFFIX@
 #endif
 @BINPATH@/components/nsINIProcessor.manifest
 @BINPATH@/components/nsINIProcessor.js
 @BINPATH@/components/nsPrompter.manifest
 @BINPATH@/components/nsPrompter.js
+@BINPATH@/components/servicesComponents.manifest
 @BINPATH@/components/TelemetryStartup.js
 @BINPATH@/components/TelemetryStartup.manifest
 @BINPATH@/components/Webapps.js
 @BINPATH@/components/Webapps.manifest
 @BINPATH@/components/AppsService.js
 @BINPATH@/components/AppsService.manifest
 
 @BINPATH@/components/Push.js
--- a/mobile/android/modules/HomeProvider.jsm
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -107,17 +107,30 @@ function syncTimerCallback(timer) {
       let success = HomeProvider.requestSync(datasetId, callback);
       if (success) {
         Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
       }
     }
   }
 }
 
+this.HomeStorage = function(datasetId) {
+  this.datasetId = datasetId;
+};
+
+this.ValidationError = function(message) {
+  this.name = "ValidationError";
+  this.message = message;
+};
+ValidationError.prototype = new Error();
+ValidationError.prototype.constructor = ValidationError;
+
 this.HomeProvider = Object.freeze({
+  ValidationError: ValidationError,
+
   /**
    * Returns a storage associated with a given dataset identifer.
    *
    * @param datasetId
    *        (string) Unique identifier for the dataset.
    *
    * @return HomeStorage
    */
@@ -244,48 +257,66 @@ function getDatabaseConnection() {
       throw e;
     }
 
     gDatabaseEnsured = true;
     throw new Task.Result(db);
   });
 }
 
-this.HomeStorage = function(datasetId) {
-  this.datasetId = datasetId;
-};
+/**
+ * Validates an item to be saved to the DB.
+ *
+ * @param item
+ *        (object) item object to be validated.
+ */
+function validateItem(datasetId, item) {
+  if (!item.url) {
+    throw new ValidationError('HomeStorage: All rows must have an URL: datasetId = ' +
+                              datasetId);
+  }
+
+  if (!item.image_url && !item.title && !item.description) {
+    throw new ValidationError('HomeStorage: All rows must have at least an image URL, ' +
+                              'or a title or a description: datasetId = ' + datasetId);
+  }
+}
 
 HomeStorage.prototype = {
   /**
    * Saves data rows to the DB.
    *
    * @param data
    *        (array) JSON array of row items
    *
    * @return Promise
    * @resolves When the operation has completed.
    */
   save: function(data) {
     return Task.spawn(function save_task() {
       let db = yield getDatabaseConnection();
       try {
-        // Insert data into DB.
-        for (let item of data) {
-          // XXX: Directly pass item as params? More validation for item? Batch insert?
-          let params = {
-            dataset_id: this.datasetId,
-            url: item.url,
-            title: item.title,
-            description: item.description,
-            image_url: item.image_url,
-            filter: item.filter,
-            created: Date.now()
-          };
-          yield db.executeCached(SQL.insertItem, params);
-        }
+        yield db.executeTransaction(function save_transaction() {
+          // Insert data into DB.
+          for (let item of data) {
+            validateItem(this.datasetId, item);
+
+            // XXX: Directly pass item as params? More validation for item?
+            let params = {
+              dataset_id: this.datasetId,
+              url: item.url,
+              title: item.title,
+              description: item.description,
+              image_url: item.image_url,
+              filter: item.filter,
+              created: Date.now()
+            };
+            yield db.executeCached(SQL.insertItem, params);
+          }
+        }.bind(this));
       } finally {
         yield db.close();
       }
     }.bind(this));
   },
 
   /**
    * Deletes all rows associated with this storage.
--- a/netwerk/base/src/Dashboard.cpp
+++ b/netwerk/base/src/Dashboard.cpp
@@ -13,216 +13,512 @@
 #include "nsIInputStream.h"
 #include "nsISocketTransport.h"
 #include "nsIThread.h"
 #include "nsSocketTransportService2.h"
 #include "nsThreadUtils.h"
 #include "nsURLHelper.h"
 
 using mozilla::AutoSafeJSContext;
+using mozilla::dom::Sequence;
+
 namespace mozilla {
 namespace net {
 
-NS_IMPL_ISUPPORTS5(Dashboard, nsIDashboard, nsIDashboardEventNotifier,
-                              nsITransportEventSink, nsITimerCallback,
-                              nsIDNSListener)
-using mozilla::dom::Sequence;
+class SocketData
+    : public nsISupports
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    SocketData()
+    {
+        mTotalSent = 0;
+        mTotalRecv = 0;
+        mThread = nullptr;
+    }
+
+    virtual ~SocketData()
+    {
+    }
+
+    uint64_t mTotalSent;
+    uint64_t mTotalRecv;
+    nsTArray<SocketInfo> mData;
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+};
+
+NS_IMPL_ISUPPORTS0(SocketData)
+
+
+class HttpData
+    : public nsISupports
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    HttpData()
+    {
+        mThread = nullptr;
+    }
+
+    virtual ~HttpData()
+    {
+    }
+
+    nsTArray<HttpRetParams> mData;
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+};
+
+NS_IMPL_ISUPPORTS0(HttpData)
+
+
+class WebSocketRequest
+    : public nsISupports
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    WebSocketRequest()
+    {
+        mThread = nullptr;
+    }
+
+    virtual ~WebSocketRequest()
+    {
+    }
+
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+};
+
+NS_IMPL_ISUPPORTS0(WebSocketRequest)
+
+
+class DnsData
+    : public nsISupports
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    DnsData()
+    {
+        mThread = nullptr;
+    }
+
+    virtual ~DnsData()
+    {
+    }
+
+    nsTArray<DNSCacheEntries> mData;
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+};
+
+NS_IMPL_ISUPPORTS0(DnsData)
+
+
+class ConnectionData
+    : public nsITransportEventSink
+    , public nsITimerCallback
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSITRANSPORTEVENTSINK
+    NS_DECL_NSITIMERCALLBACK
+
+    void StartTimer(uint32_t aTimeout);
+    void StopTimer();
+
+    ConnectionData(Dashboard *target)
+    {
+        mThread = nullptr;
+        mDashboard = target;
+    }
+
+    virtual ~ConnectionData()
+    {
+        if (mTimer) {
+            mTimer->Cancel();
+        }
+    }
+
+    nsCOMPtr<nsISocketTransport> mSocket;
+    nsCOMPtr<nsIInputStream> mStreamIn;
+    nsCOMPtr<nsITimer> mTimer;
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+    Dashboard *mDashboard;
+
+    nsCString mHost;
+    uint32_t mPort;
+    const char *mProtocol;
+    uint32_t mTimeout;
+
+    nsString mStatus;
+};
+
+NS_IMPL_ISUPPORTS2(ConnectionData, nsITransportEventSink, nsITimerCallback)
+
+NS_IMETHODIMP
+ConnectionData::OnTransportStatus(nsITransport *aTransport, nsresult aStatus,
+                                  uint64_t aProgress, uint64_t aProgressMax)
+{
+    if (aStatus == NS_NET_STATUS_CONNECTED_TO) {
+        StopTimer();
+    }
+
+    CopyASCIItoUTF16(Dashboard::GetErrorString(aStatus), mStatus);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<ConnectionData> >
+        (mDashboard, &Dashboard::GetConnectionStatus, this);
+    mThread->Dispatch(event, NS_DISPATCH_NORMAL);
 
-struct ConnStatus
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+ConnectionData::Notify(nsITimer *aTimer)
+{
+    MOZ_ASSERT(aTimer == mTimer);
+
+    if (mSocket) {
+        mSocket->Close(NS_ERROR_ABORT);
+        mSocket = nullptr;
+        mStreamIn = nullptr;
+    }
+
+    mTimer = nullptr;
+
+    mStatus.Assign(NS_LITERAL_STRING("NS_ERROR_NET_TIMEOUT"));
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<ConnectionData> >
+        (mDashboard, &Dashboard::GetConnectionStatus, this);
+    mThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+    return NS_OK;
+}
+
+void
+ConnectionData::StartTimer(uint32_t aTimeout)
+{
+    if (!mTimer) {
+        mTimer = do_CreateInstance("@mozilla.org/timer;1");
+    }
+
+    mTimer->InitWithCallback(this, aTimeout * 1000,
+        nsITimer::TYPE_ONE_SHOT);
+}
+
+void
+ConnectionData::StopTimer()
+{
+    if (mTimer) {
+        mTimer->Cancel();
+        mTimer = nullptr;
+    }
+}
+
+
+class LookupHelper;
+
+class LookupArgument
+    : public nsISupports
+{
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+
+    LookupArgument(nsIDNSRecord *aRecord, LookupHelper *aHelper)
+    {
+        mRecord = aRecord;
+        mHelper = aHelper;
+    }
+
+    virtual ~LookupArgument()
+    {
+    }
+
+    nsCOMPtr<nsIDNSRecord> mRecord;
+    nsRefPtr<LookupHelper> mHelper;
+};
+
+NS_IMPL_ISUPPORTS0(LookupArgument)
+
+
+class LookupHelper
+    : public nsIDNSListener
 {
-    nsString creationSts;
+public:
+    NS_DECL_THREADSAFE_ISUPPORTS
+    NS_DECL_NSIDNSLISTENER
+
+    LookupHelper() {
+    }
+
+    virtual ~LookupHelper()
+    {
+        if (mCancel) {
+            mCancel->Cancel(NS_ERROR_ABORT);
+        }
+    }
+
+    nsresult ConstructAnswer(LookupArgument *aArgument);
+public:
+    nsCOMPtr<nsICancelable> mCancel;
+    nsCOMPtr<NetDashboardCallback> mCallback;
+    nsIThread *mThread;
+    nsresult mStatus;
 };
 
+NS_IMPL_ISUPPORTS1(LookupHelper, nsIDNSListener)
+
+NS_IMETHODIMP
+LookupHelper::OnLookupComplete(nsICancelable *aRequest,
+                               nsIDNSRecord *aRecord, nsresult aStatus)
+{
+    MOZ_ASSERT(aRequest == mCancel);
+    mCancel = nullptr;
+    mStatus = aStatus;
+
+    nsRefPtr<LookupArgument> arg = new LookupArgument(aRecord, this);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<LookupArgument> >(
+        this, &LookupHelper::ConstructAnswer, arg);
+    mThread->Dispatch(event, NS_DISPATCH_NORMAL);
+
+    return NS_OK;
+}
+
+nsresult
+LookupHelper::ConstructAnswer(LookupArgument *aArgument)
+{
+
+    nsIDNSRecord *aRecord = aArgument->mRecord;
+    AutoSafeJSContext cx;
+
+    mozilla::dom::DNSLookupDict dict;
+    dict.mAddress.Construct();
+
+    Sequence<nsString> &addresses = dict.mAddress.Value();
+
+    if (NS_SUCCEEDED(mStatus)) {
+        dict.mAnswer = true;
+        bool hasMore;
+        aRecord->HasMore(&hasMore);
+        while (hasMore) {
+           nsCString nextAddress;
+           aRecord->GetNextAddrAsString(nextAddress);
+           CopyASCIItoUTF16(nextAddress, *addresses.AppendElement());
+           aRecord->HasMore(&hasMore);
+        }
+    } else {
+        dict.mAnswer = false;
+        CopyASCIItoUTF16(Dashboard::GetErrorString(mStatus), dict.mError);
+    }
+
+    JS::RootedValue val(cx);
+    if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
+        return NS_ERROR_FAILURE;
+    }
+
+    this->mCallback->OnDashboardDataAvailable(val);
+
+    return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS2(Dashboard, nsIDashboard, nsIDashboardEventNotifier)
+
 Dashboard::Dashboard()
 {
     mEnableLogging = false;
 }
 
 Dashboard::~Dashboard()
 {
-    if (mDnsup.cancel)
-        mDnsup.cancel->Cancel(NS_ERROR_ABORT);
 }
 
 NS_IMETHODIMP
-Dashboard::RequestSockets(NetDashboardCallback* cb)
+Dashboard::RequestSockets(NetDashboardCallback *aCallback)
 {
-    if (mSock.cb)
-        return NS_ERROR_FAILURE;
-    mSock.cb = cb;
-    mSock.data.Clear();
-    mSock.thread = NS_GetCurrentThread();
-
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetSocketsDispatch);
+    nsRefPtr<SocketData> socketData = new SocketData();
+    socketData->mCallback = aCallback;
+    socketData->mThread = NS_GetCurrentThread();
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<SocketData> >
+        (this, &Dashboard::GetSocketsDispatch, socketData);
     gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
     return NS_OK;
 }
 
-void
-Dashboard::GetSocketsDispatch()
+nsresult
+Dashboard::GetSocketsDispatch(SocketData *aSocketData)
 {
+    nsRefPtr<SocketData> socketData = aSocketData;
     if (gSocketTransportService) {
-        gSocketTransportService->GetSocketConnections(&mSock.data);
-        mSock.totalSent = gSocketTransportService->GetSentBytes();
-        mSock.totalRecv = gSocketTransportService->GetReceivedBytes();
+        gSocketTransportService->GetSocketConnections(&socketData->mData);
+        socketData->mTotalSent = gSocketTransportService->GetSentBytes();
+        socketData->mTotalRecv = gSocketTransportService->GetReceivedBytes();
     }
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetSockets);
-    mSock.thread->Dispatch(event, NS_DISPATCH_NORMAL);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<SocketData> >
+        (this, &Dashboard::GetSockets, socketData);
+    socketData->mThread->Dispatch(event, NS_DISPATCH_NORMAL);
+    return NS_OK;
 }
 
 nsresult
-Dashboard::GetSockets()
+Dashboard::GetSockets(SocketData *aSocketData)
 {
+    nsRefPtr<SocketData> socketData = aSocketData;
     AutoSafeJSContext cx;
 
     mozilla::dom::SocketsDict dict;
     dict.mSockets.Construct();
     dict.mSent = 0;
     dict.mReceived = 0;
 
     Sequence<mozilla::dom::SocketElement> &sockets = dict.mSockets.Value();
 
-    uint32_t length = mSock.data.Length();
+    uint32_t length = socketData->mData.Length();
     if (!sockets.SetCapacity(length)) {
-            mSock.cb = nullptr;
-            mSock.data.Clear();
             JS_ReportOutOfMemory(cx);
             return NS_ERROR_OUT_OF_MEMORY;
     }
 
-    for (uint32_t i = 0; i < mSock.data.Length(); i++) {
-        mozilla::dom::SocketElement &socket = *sockets.AppendElement();
-        CopyASCIItoUTF16(mSock.data[i].host, socket.mHost);
-        socket.mPort = mSock.data[i].port;
-        socket.mActive = mSock.data[i].active;
-        socket.mTcp = mSock.data[i].tcp;
-        socket.mSent = (double) mSock.data[i].sent;
-        socket.mReceived = (double) mSock.data[i].received;
-        dict.mSent += mSock.data[i].sent;
-        dict.mReceived += mSock.data[i].received;
+    for (uint32_t i = 0; i < socketData->mData.Length(); i++) {
+        mozilla::dom::SocketElement &mSocket = *sockets.AppendElement();
+        CopyASCIItoUTF16(socketData->mData[i].host, mSocket.mHost);
+        mSocket.mPort = socketData->mData[i].port;
+        mSocket.mActive = socketData->mData[i].active;
+        mSocket.mTcp = socketData->mData[i].tcp;
+        mSocket.mSent = (double) socketData->mData[i].sent;
+        mSocket.mReceived = (double) socketData->mData[i].received;
+        dict.mSent += socketData->mData[i].sent;
+        dict.mReceived += socketData->mData[i].received;
     }
 
-    dict.mSent += mSock.totalSent;
-    dict.mReceived += mSock.totalRecv;
+    dict.mSent += socketData->mTotalSent;
+    dict.mReceived += socketData->mTotalRecv;
     JS::RootedValue val(cx);
-    if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mSock.cb = nullptr;
-        mSock.data.Clear();
+    if (!dict.ToObject(cx, JS::NullPtr(), &val))
         return NS_ERROR_FAILURE;
-    }
-    mSock.cb->OnDashboardDataAvailable(val);
-    mSock.cb = nullptr;
+    socketData->mCallback->OnDashboardDataAvailable(val);
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
-Dashboard::RequestHttpConnections(NetDashboardCallback* cb)
+Dashboard::RequestHttpConnections(NetDashboardCallback *aCallback)
 {
-    if (mHttp.cb)
-        return NS_ERROR_FAILURE;
-    mHttp.cb = cb;
-    mHttp.data.Clear();
-    mHttp.thread = NS_GetCurrentThread();
+    nsRefPtr<HttpData> httpData = new HttpData();
+    httpData->mCallback = aCallback;
+    httpData->mThread = NS_GetCurrentThread();
 
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetHttpDispatch);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<HttpData> >
+        (this, &Dashboard::GetHttpDispatch, httpData);
     gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
     return NS_OK;
 }
 
-void
-Dashboard::GetHttpDispatch()
+nsresult
+Dashboard::GetHttpDispatch(HttpData *aHttpData)
 {
-    HttpInfo::GetHttpConnectionData(&mHttp.data);
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetHttpConnections);
-    mHttp.thread->Dispatch(event, NS_DISPATCH_NORMAL);
+    nsRefPtr<HttpData> httpData = aHttpData;
+    HttpInfo::GetHttpConnectionData(&httpData->mData);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<HttpData> >
+        (this, &Dashboard::GetHttpConnections, httpData);
+    httpData->mThread->Dispatch(event, NS_DISPATCH_NORMAL);
+    return NS_OK;
 }
 
 
 nsresult
-Dashboard::GetHttpConnections()
+Dashboard::GetHttpConnections(HttpData *aHttpData)
 {
+    nsRefPtr<HttpData> httpData = aHttpData;
     AutoSafeJSContext cx;
 
     mozilla::dom::HttpConnDict dict;
     dict.mConnections.Construct();
 
     using mozilla::dom::HalfOpenInfoDict;
     using mozilla::dom::HttpConnectionElement;
     using mozilla::dom::HttpConnInfo;
     Sequence<HttpConnectionElement> &connections = dict.mConnections.Value();
 
-    uint32_t length = mHttp.data.Length();
+    uint32_t length = httpData->mData.Length();
     if (!connections.SetCapacity(length)) {
-            mHttp.cb = nullptr;
-            mHttp.data.Clear();
+            httpData->mCallback = nullptr;
+            httpData->mData.Clear();
             JS_ReportOutOfMemory(cx);
             return NS_ERROR_OUT_OF_MEMORY;
     }
 
-    for (uint32_t i = 0; i < mHttp.data.Length(); i++) {
+    for (uint32_t i = 0; i < httpData->mData.Length(); i++) {
         HttpConnectionElement &connection = *connections.AppendElement();
 
-        CopyASCIItoUTF16(mHttp.data[i].host, connection.mHost);
-        connection.mPort = mHttp.data[i].port;
-        connection.mSpdy = mHttp.data[i].spdy;
-        connection.mSsl = mHttp.data[i].ssl;
+        CopyASCIItoUTF16(httpData->mData[i].host, connection.mHost);
+        connection.mPort = httpData->mData[i].port;
+        connection.mSpdy = httpData->mData[i].spdy;
+        connection.mSsl = httpData->mData[i].ssl;
 
         connection.mActive.Construct();
         connection.mIdle.Construct();
         connection.mHalfOpens.Construct();
 
         Sequence<HttpConnInfo> &active = connection.mActive.Value();
         Sequence<HttpConnInfo> &idle = connection.mIdle.Value();
         Sequence<HalfOpenInfoDict> &halfOpens = connection.mHalfOpens.Value();
 
-        if (!active.SetCapacity(mHttp.data[i].active.Length()) ||
-            !idle.SetCapacity(mHttp.data[i].idle.Length()) ||
-            !halfOpens.SetCapacity(mHttp.data[i].halfOpens.Length())) {
-                mHttp.cb = nullptr;
-                mHttp.data.Clear();
+        if (!active.SetCapacity(httpData->mData[i].active.Length()) ||
+            !idle.SetCapacity(httpData->mData[i].idle.Length()) ||
+            !halfOpens.SetCapacity(httpData->mData[i].halfOpens.Length())) {
+                httpData->mCallback = nullptr;
+                httpData->mData.Clear();
                 JS_ReportOutOfMemory(cx);
                 return NS_ERROR_OUT_OF_MEMORY;
         }
 
-        for (uint32_t j = 0; j < mHttp.data[i].active.Length(); j++) {
+        for (uint32_t j = 0; j < httpData->mData[i].active.Length(); j++) {
             HttpConnInfo &info = *active.AppendElement();
-            info.mRtt = mHttp.data[i].active[j].rtt;
-            info.mTtl = mHttp.data[i].active[j].ttl;
-            info.mProtocolVersion = mHttp.data[i].active[j].protocolVersion;
+            info.mRtt = httpData->mData[i].active[j].rtt;
+            info.mTtl = httpData->mData[i].active[j].ttl;
+            info.mProtocolVersion =
+                httpData->mData[i].active[j].protocolVersion;
         }
 
-        for (uint32_t j = 0; j < mHttp.data[i].idle.Length(); j++) {
+        for (uint32_t j = 0; j < httpData->mData[i].idle.Length(); j++) {
             HttpConnInfo &info = *idle.AppendElement();
-            info.mRtt = mHttp.data[i].idle[j].rtt;
-            info.mTtl = mHttp.data[i].idle[j].ttl;
-            info.mProtocolVersion = mHttp.data[i].idle[j].protocolVersion;
+            info.mRtt = httpData->mData[i].idle[j].rtt;
+            info.mTtl = httpData->mData[i].idle[j].ttl;
+            info.mProtocolVersion = httpData->mData[i].idle[j].protocolVersion;
         }
 
-        for (uint32_t j = 0; j < mHttp.data[i].halfOpens.Length(); j++) {
+        for (uint32_t j = 0; j < httpData->mData[i].halfOpens.Length(); j++) {
             HalfOpenInfoDict &info = *halfOpens.AppendElement();
-            info.mSpeculative = mHttp.data[i].halfOpens[j].speculative;
+            info.mSpeculative = httpData->mData[i].halfOpens[j].speculative;
         }
     }
 
     JS::RootedValue val(cx);
     if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mHttp.cb = nullptr;
-        mHttp.data.Clear();
         return NS_ERROR_FAILURE;
     }
-    mHttp.cb->OnDashboardDataAvailable(val);
-    mHttp.cb = nullptr;
+
+    httpData->mCallback->OnDashboardDataAvailable(val);
 
     return NS_OK;
 }
 
-
 NS_IMETHODIMP
-Dashboard::GetEnableLogging(bool* value)
+Dashboard::GetEnableLogging(bool *value)
 {
     *value = mEnableLogging;
     return NS_OK;
 }
 
 NS_IMETHODIMP
 Dashboard::SetEnableLogging(const bool value)
 {
@@ -230,21 +526,23 @@ Dashboard::SetEnableLogging(const bool v
     return NS_OK;
 }
 
 NS_IMETHODIMP
 Dashboard::AddHost(const nsACString& aHost, uint32_t aSerial, bool aEncrypted)
 {
     if (mEnableLogging) {
         mozilla::MutexAutoLock lock(mWs.lock);
-        LogData data(nsCString(aHost), aSerial, aEncrypted);
-        if (mWs.data.Contains(data))
+        LogData mData(nsCString(aHost), aSerial, aEncrypted);
+        if (mWs.data.Contains(mData)) {
             return NS_OK;
-        if (!mWs.data.AppendElement(data))
+        }
+        if (!mWs.data.AppendElement(mData)) {
             return NS_ERROR_OUT_OF_MEMORY;
+        }
         return NS_OK;
     }
     return NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP
 Dashboard::RemoveHost(const nsACString& aHost, uint32_t aSerial)
 {
@@ -285,212 +583,173 @@ Dashboard::NewMsgReceived(const nsACStri
         mWs.data[index].mMsgReceived++;
         mWs.data[index].mSizeReceived += aLength;
         return NS_OK;
     }
     return NS_ERROR_FAILURE;
 }
 
 NS_IMETHODIMP
-Dashboard::RequestWebsocketConnections(NetDashboardCallback* cb)
+Dashboard::RequestWebsocketConnections(NetDashboardCallback *aCallback)
 {
-    if (mWs.cb)
-        return NS_ERROR_FAILURE;
-    mWs.cb = cb;
-    mWs.thread = NS_GetCurrentThread();
+    nsRefPtr<WebSocketRequest> wsRequest = new WebSocketRequest();
+    wsRequest->mCallback = aCallback;
+    wsRequest->mThread = NS_GetCurrentThread();
 
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetWebSocketConnections);
-    mWs.thread->Dispatch(event, NS_DISPATCH_NORMAL);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<WebSocketRequest> >
+        (this, &Dashboard::GetWebSocketConnections, wsRequest);
+    wsRequest->mThread->Dispatch(event, NS_DISPATCH_NORMAL);
     return NS_OK;
 }
 
 nsresult
-Dashboard::GetWebSocketConnections()
+Dashboard::GetWebSocketConnections(WebSocketRequest *aWsRequest)
 {
+    nsRefPtr<WebSocketRequest> wsRequest = aWsRequest;
     AutoSafeJSContext cx;
 
     mozilla::dom::WebSocketDict dict;
     dict.mWebsockets.Construct();
-    Sequence<mozilla::dom::WebSocketElement> &websockets = dict.mWebsockets.Value();
+    Sequence<mozilla::dom::WebSocketElement> &websockets =
+        dict.mWebsockets.Value();
 
     mozilla::MutexAutoLock lock(mWs.lock);
     uint32_t length = mWs.data.Length();
     if (!websockets.SetCapacity(length)) {
-        mWs.cb = nullptr;
-        mWs.data.Clear();
         JS_ReportOutOfMemory(cx);
         return NS_ERROR_OUT_OF_MEMORY;
     }
 
     for (uint32_t i = 0; i < mWs.data.Length(); i++) {
         mozilla::dom::WebSocketElement &websocket = *websockets.AppendElement();
         CopyASCIItoUTF16(mWs.data[i].mHost, websocket.mHostport);
         websocket.mMsgsent = mWs.data[i].mMsgSent;
         websocket.mMsgreceived = mWs.data[i].mMsgReceived;
         websocket.mSentsize = mWs.data[i].mSizeSent;
         websocket.mReceivedsize = mWs.data[i].mSizeReceived;
         websocket.mEncrypted = mWs.data[i].mEncrypted;
     }
 
     JS::RootedValue val(cx);
     if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mWs.cb = nullptr;
-        mWs.data.Clear();
         return NS_ERROR_FAILURE;
     }
-    mWs.cb->OnDashboardDataAvailable(val);
-    mWs.cb = nullptr;
+    wsRequest->mCallback->OnDashboardDataAvailable(val);
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
-Dashboard::RequestDNSInfo(NetDashboardCallback* cb)
+Dashboard::RequestDNSInfo(NetDashboardCallback *aCallback)
 {
-    if (mDns.cb)
-        return NS_ERROR_FAILURE;
-    mDns.cb = cb;
+    nsRefPtr<DnsData> dnsData = new DnsData();
+    dnsData->mCallback = aCallback;
+
     nsresult rv;
-    mDns.data.Clear();
-    mDns.thread = NS_GetCurrentThread();
+    dnsData->mData.Clear();
+    dnsData->mThread = NS_GetCurrentThread();
 
-    if (!mDns.serv) {
-        mDns.serv = do_GetService("@mozilla.org/network/dns-service;1", &rv);
-        if (NS_FAILED(rv))
+    if (!mDnsService) {
+        mDnsService = do_GetService("@mozilla.org/network/dns-service;1", &rv);
+        if (NS_FAILED(rv)) {
             return rv;
+        }
     }
 
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetDnsInfoDispatch);
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<DnsData> >
+        (this, &Dashboard::GetDnsInfoDispatch, dnsData);
     gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL);
     return NS_OK;
 }
 
-void
-Dashboard::GetDnsInfoDispatch()
+nsresult
+Dashboard::GetDnsInfoDispatch(DnsData *aDnsData)
 {
-    if (mDns.serv)
-        mDns.serv->GetDNSCacheEntries(&mDns.data);
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethod(this, &Dashboard::GetDNSCacheEntries);
-    mDns.thread->Dispatch(event, NS_DISPATCH_NORMAL);
+    nsRefPtr<DnsData> dnsData = aDnsData;
+    if (mDnsService) {
+        mDnsService->GetDNSCacheEntries(&dnsData->mData);
+    }
+    nsCOMPtr<nsIRunnable> event =
+        NS_NewRunnableMethodWithArg<nsRefPtr<DnsData> >
+        (this, &Dashboard::GetDNSCacheEntries, dnsData);
+    dnsData->mThread->Dispatch(event, NS_DISPATCH_NORMAL);
+    return NS_OK;
 }
 
 nsresult
-Dashboard::GetDNSCacheEntries()
+Dashboard::GetDNSCacheEntries(DnsData *dnsData)
 {
     AutoSafeJSContext cx;
 
     mozilla::dom::DNSCacheDict dict;
     dict.mEntries.Construct();
     Sequence<mozilla::dom::DnsCacheEntry> &entries = dict.mEntries.Value();
 
-    uint32_t length = mDns.data.Length();
+    uint32_t length = dnsData->mData.Length();
     if (!entries.SetCapacity(length)) {
-        mDns.cb = nullptr;
-        mDns.data.Clear();
         JS_ReportOutOfMemory(cx);
         return NS_ERROR_OUT_OF_MEMORY;
     }
 
-    for (uint32_t i = 0; i < mDns.data.Length(); i++) {
+    for (uint32_t i = 0; i < dnsData->mData.Length(); i++) {
         mozilla::dom::DnsCacheEntry &entry = *entries.AppendElement();
         entry.mHostaddr.Construct();
 
         Sequence<nsString> &addrs = entry.mHostaddr.Value();
-        if (!addrs.SetCapacity(mDns.data[i].hostaddr.Length())) {
-            mDns.cb = nullptr;
-            mDns.data.Clear();
+        if (!addrs.SetCapacity(dnsData->mData[i].hostaddr.Length())) {
             JS_ReportOutOfMemory(cx);
             return NS_ERROR_OUT_OF_MEMORY;
         }
 
-        CopyASCIItoUTF16(mDns.data[i].hostname, entry.mHostname);
-        entry.mExpiration = mDns.data[i].expiration;
+        CopyASCIItoUTF16(dnsData->mData[i].hostname, entry.mHostname);
+        entry.mExpiration = dnsData->mData[i].expiration;
 
-        for (uint32_t j = 0; j < mDns.data[i].hostaddr.Length(); j++) {
-            CopyASCIItoUTF16(mDns.data[i].hostaddr[j], *addrs.AppendElement());
+        for (uint32_t j = 0; j < dnsData->mData[i].hostaddr.Length(); j++) {
+            CopyASCIItoUTF16(dnsData->mData[i].hostaddr[j],
+                *addrs.AppendElement());
         }
 
-        if (mDns.data[i].family == PR_AF_INET6)
+        if (dnsData->mData[i].family == PR_AF_INET6) {
             CopyASCIItoUTF16("ipv6", entry.mFamily);
-        else
+        } else {
             CopyASCIItoUTF16("ipv4", entry.mFamily);
+        }
     }
 
     JS::RootedValue val(cx);
     if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mDns.cb = nullptr;
-        mDns.data.Clear();
         return NS_ERROR_FAILURE;
     }
-    mDns.cb->OnDashboardDataAvailable(val);
-    mDns.cb = nullptr;
+    dnsData->mCallback->OnDashboardDataAvailable(val);
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
-Dashboard::RequestDNSLookup(const nsACString &aHost, NetDashboardCallback *cb)
+Dashboard::RequestDNSLookup(const nsACString &aHost,
+                            NetDashboardCallback *mCallback)
 {
-    if (mDnsup.cb)
-        return NS_ERROR_FAILURE;
     nsresult rv;
 
-    if (!mDnsup.serv) {
-        mDnsup.serv = do_GetService("@mozilla.org/network/dns-service;1", &rv);
-        if (NS_FAILED(rv))
+    if (!mDnsService) {
+        mDnsService = do_GetService("@mozilla.org/network/dns-service;1", &rv);
+        if (NS_FAILED(rv)) {
             return rv;
-    }
-
-    mDnsup.cb = cb;
-    rv = mDnsup.serv->AsyncResolve(aHost, 0, this, NS_GetCurrentThread(), getter_AddRefs(mDnsup.cancel));
-    if (NS_FAILED(rv)) {
-        mDnsup.cb = nullptr;
-        return rv;
+        }
     }
 
-    return NS_OK;
-}
-
-NS_IMETHODIMP
-Dashboard::OnLookupComplete(nsICancelable *aRequest, nsIDNSRecord *aRecord, nsresult aStatus)
-{
-    MOZ_ASSERT(aRequest == mDnsup.cancel);
-    mDnsup.cancel = nullptr;
-
-    AutoSafeJSContext cx;
-
-    mozilla::dom::DNSLookupDict dict;
-    dict.mAddress.Construct();
-
-    Sequence<nsString> &addresses = dict.mAddress.Value();
-
-    if (NS_SUCCEEDED(aStatus)) {
-        dict.mAnswer = true;
-        bool hasMore;
-        aRecord->HasMore(&hasMore);
-        while(hasMore) {
-           nsCString nextAddress;
-           aRecord->GetNextAddrAsString(nextAddress);
-           CopyASCIItoUTF16(nextAddress, *addresses.AppendElement());
-           aRecord->HasMore(&hasMore);
-        }
-    } else {
-        dict.mAnswer = false;
-        CopyASCIItoUTF16(GetErrorString(aStatus), dict.mError);
-    }
-
-    JS::RootedValue val(cx);
-    if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mDnsup.cb = nullptr;
-        return NS_ERROR_FAILURE;
-    }
-    mDnsup.cb->OnDashboardDataAvailable(val);
-    mDnsup.cb = nullptr;
-
-    return NS_OK;
+    nsRefPtr<LookupHelper> helper = new LookupHelper();
+    helper->mCallback = mCallback;
+    helper->mThread = NS_GetCurrentThread();
+    rv = mDnsService->AsyncResolve(aHost, 0, helper.get(),
+                                   NS_GetCurrentThread(),
+                                   getter_AddRefs(helper->mCancel));
+    return rv;
 }
 
 void
 HttpConnInfo::SetHTTP1ProtocolVersion(uint8_t pv)
 {
     switch (pv) {
     case NS_HTTP_VERSION_0_9:
         protocolVersion.Assign(NS_LITERAL_STRING("http/0.9"));
@@ -520,137 +779,104 @@ HttpConnInfo::SetHTTP2ProtocolVersion(ui
         MOZ_ASSERT (pv == NS_HTTP2_DRAFT_VERSION);
         protocolVersion.Assign(NS_LITERAL_STRING(NS_HTTP2_DRAFT_TOKEN));
     }
 }
 
 NS_IMETHODIMP
 Dashboard::RequestConnection(const nsACString& aHost, uint32_t aPort,
                              const char *aProtocol, uint32_t aTimeout,
-                             NetDashboardCallback* cb)
+                             NetDashboardCallback *aCallback)
 {
     nsresult rv;
-    mConn.cb = cb;
-    mConn.thread = NS_GetCurrentThread();
+    nsRefPtr<ConnectionData> connectionData = new ConnectionData(this);
+    connectionData->mHost = aHost;
+    connectionData->mPort = aPort;
+    connectionData->mProtocol = aProtocol;
+    connectionData->mTimeout = aTimeout;
 
-    rv = TestNewConnection(aHost, aPort, aProtocol, aTimeout);
+    connectionData->mCallback = aCallback;
+    connectionData->mThread = NS_GetCurrentThread();
+
+    rv = TestNewConnection(connectionData);
     if (NS_FAILED(rv)) {
-        ConnStatus status;
-        CopyASCIItoUTF16(GetErrorString(rv), status.creationSts);
+        CopyASCIItoUTF16(GetErrorString(rv), connectionData->mStatus);
         nsCOMPtr<nsIRunnable> event =
-            NS_NewRunnableMethodWithArg<ConnStatus>(this, &Dashboard::GetConnectionStatus, status);
-        mConn.thread->Dispatch(event, NS_DISPATCH_NORMAL);
+            NS_NewRunnableMethodWithArg<nsRefPtr<ConnectionData> >
+            (this, &Dashboard::GetConnectionStatus, connectionData);
+        connectionData->mThread->Dispatch(event, NS_DISPATCH_NORMAL);
         return rv;
     }
 
     return NS_OK;
 }
 
 nsresult
-Dashboard::GetConnectionStatus(ConnStatus aStatus)
+Dashboard::GetConnectionStatus(ConnectionData *aConnectionData)
 {
+    nsRefPtr<ConnectionData> connectionData = aConnectionData;
     AutoSafeJSContext cx;
 
     mozilla::dom::ConnStatusDict dict;
-    dict.mStatus = aStatus.creationSts;
+    dict.mStatus = connectionData->mStatus;
 
     JS::RootedValue val(cx);
-    if (!dict.ToObject(cx, JS::NullPtr(), &val)) {
-        mConn.cb = nullptr;
+    if (!dict.ToObject(cx, JS::NullPtr(), &val))
         return NS_ERROR_FAILURE;
-    }
-    mConn.cb->OnDashboardDataAvailable(val);
+
+    connectionData->mCallback->OnDashboardDataAvailable(val);
 
     return NS_OK;
 }
 
 nsresult
-Dashboard::TestNewConnection(const nsACString& aHost, uint32_t aPort,
-                             const char *aProtocol, uint32_t aTimeout)
+Dashboard::TestNewConnection(ConnectionData *aConnectionData)
 {
+    nsRefPtr<ConnectionData> connectionData = aConnectionData;
+
     nsresult rv;
-    if (!aHost.Length() || !net_IsValidHostName(aHost))
+    if (!connectionData->mHost.Length() ||
+        !net_IsValidHostName(connectionData->mHost)) {
         return NS_ERROR_UNKNOWN_HOST;
+    }
 
-    if (aProtocol && NS_LITERAL_STRING("ssl").EqualsASCII(aProtocol))
-        rv = gSocketTransportService->CreateTransport(&aProtocol, 1, aHost,
-                                                      aPort, nullptr,
-                                                      getter_AddRefs(mConn.socket));
-    else
-        rv = gSocketTransportService->CreateTransport(nullptr, 0, aHost,
-                                                      aPort, nullptr,
-                                                      getter_AddRefs(mConn.socket));
-    if (NS_FAILED(rv))
+    if (connectionData->mProtocol &&
+        NS_LITERAL_STRING("ssl").EqualsASCII(connectionData->mProtocol)) {
+        rv = gSocketTransportService->CreateTransport(
+            &connectionData->mProtocol, 1, connectionData->mHost,
+            connectionData->mPort, nullptr,
+            getter_AddRefs(connectionData->mSocket));
+    } else {
+        rv = gSocketTransportService->CreateTransport(
+            nullptr, 0, connectionData->mHost,
+            connectionData->mPort, nullptr,
+            getter_AddRefs(connectionData->mSocket));
+    }
+    if (NS_FAILED(rv)) {
         return rv;
+    }
 
-    rv = mConn.socket->SetEventSink(this, NS_GetCurrentThread());
-    if (NS_FAILED(rv))
+    rv = connectionData->mSocket->SetEventSink(connectionData,
+        NS_GetCurrentThread());
+    if (NS_FAILED(rv)) {
         return rv;
+    }
 
-    rv = mConn.socket->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0,
-                                       getter_AddRefs(mConn.streamIn));
-    if (NS_FAILED(rv))
+    rv = connectionData->mSocket->OpenInputStream(
+        nsITransport::OPEN_BLOCKING, 0, 0,
+        getter_AddRefs(connectionData->mStreamIn));
+    if (NS_FAILED(rv)) {
         return rv;
+    }
 
-    StartTimer(aTimeout);
+    connectionData->StartTimer(connectionData->mTimeout);
 
     return rv;
 }
 
-NS_IMETHODIMP
-Dashboard::OnTransportStatus(nsITransport *aTransport, nsresult aStatus,
-                             uint64_t aProgress, uint64_t aProgressMax)
-{
-    if (aStatus == NS_NET_STATUS_CONNECTED_TO)
-        StopTimer();
-
-    ConnStatus status;
-    CopyASCIItoUTF16(GetErrorString(aStatus), status.creationSts);
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethodWithArg<ConnStatus>(this, &Dashboard::GetConnectionStatus, status);
-    mConn.thread->Dispatch(event, NS_DISPATCH_NORMAL);
-
-    return NS_OK;
-}
-
-NS_IMETHODIMP
-Dashboard::Notify(nsITimer *timer)
-{
-    if (mConn.socket) {
-        mConn.socket->Close(NS_ERROR_ABORT);
-        mConn.socket = nullptr;
-        mConn.streamIn = nullptr;
-    }
-
-    mConn.timer = nullptr;
-
-    ConnStatus status;
-    status.creationSts.Assign(NS_LITERAL_STRING("NS_ERROR_NET_TIMEOUT"));
-    nsCOMPtr<nsIRunnable> event = NS_NewRunnableMethodWithArg<ConnStatus>(this, &Dashboard::GetConnectionStatus, status);
-    mConn.thread->Dispatch(event, NS_DISPATCH_NORMAL);
-
-    return NS_OK;
-}
-
-void
-Dashboard::StartTimer(uint32_t aTimeout)
-{
-    if (!mConn.timer)
-        mConn.timer = do_CreateInstance("@mozilla.org/timer;1");
-    mConn.timer->InitWithCallback(this, aTimeout * 1000, nsITimer::TYPE_ONE_SHOT);
-}
-
-void
-Dashboard::StopTimer()
-{
-    if (mConn.timer) {
-        mConn.timer->Cancel();
-        mConn.timer = nullptr;
-    }
-}
-
 typedef struct
 {
     nsresult key;
     const char *error;
 } ErrorEntry;
 
 #undef ERROR
 #define ERROR(key, val) {key, #key}
@@ -670,20 +896,22 @@ ErrorEntry socketTransportStatuses[] = {
 };
 #undef ERROR
 
 const char *
 Dashboard::GetErrorString(nsresult rv)
 {
     int length = sizeof(socketTransportStatuses) / sizeof(ErrorEntry);
     for (int i = 0;i < length;i++)
-        if (socketTransportStatuses[i].key == rv)
+        if (socketTransportStatuses[i].key == rv) {
             return socketTransportStatuses[i].error;
+        }
 
     length = sizeof(errors) / sizeof(ErrorEntry);
     for (int i = 0;i < length;i++)
-        if (errors[i].key == rv)
+        if (errors[i].key == rv) {
             return errors[i].error;
+        }
 
     return nullptr;
 }
 
 } } // namespace mozilla::net
--- a/netwerk/base/src/Dashboard.h
+++ b/netwerk/base/src/Dashboard.h
@@ -16,67 +16,36 @@
 
 class nsIDNSService;
 class nsISocketTransport;
 class nsIThread;
 
 namespace mozilla {
 namespace net {
 
-class Dashboard:
-    public nsIDashboard,
-    public nsIDashboardEventNotifier,
-    public nsITransportEventSink,
-    public nsITimerCallback,
-    public nsIDNSListener
+class SocketData;
+class HttpData;
+class DnsData;
+class WebSocketRequest;
+class ConnectionData;
+
+class Dashboard
+    : public nsIDashboard
+    , public nsIDashboardEventNotifier
 {
 public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSIDASHBOARD
     NS_DECL_NSIDASHBOARDEVENTNOTIFIER
-    NS_DECL_NSITRANSPORTEVENTSINK
-    NS_DECL_NSITIMERCALLBACK
-    NS_DECL_NSIDNSLISTENER
 
     Dashboard();
-    friend class DashConnStatusRunnable;
     static const char *GetErrorString(nsresult rv);
-private:
-    virtual ~Dashboard();
-
-    void GetSocketsDispatch();
-    void GetHttpDispatch();
-    void GetDnsInfoDispatch();
-    void StartTimer(uint32_t aTimeout);
-    void StopTimer();
-    nsresult TestNewConnection(const nsACString& aHost, uint32_t aPort,
-                               const char *aProtocol, uint32_t aTimeout);
-
-    /* Helper methods that pass the JSON to the callback function. */
-    nsresult GetSockets();
-    nsresult GetHttpConnections();
-    nsresult GetWebSocketConnections();
-    nsresult GetDNSCacheEntries();
-    nsresult GetConnectionStatus(struct ConnStatus aStatus);
+    nsresult GetConnectionStatus(ConnectionData *aConnectionData);
 
 private:
-    struct SocketData
-    {
-        uint64_t totalSent;
-        uint64_t totalRecv;
-        nsTArray<SocketInfo> data;
-        nsCOMPtr<NetDashboardCallback> cb;
-        nsIThread* thread;
-    };
-
-    struct HttpData {
-        nsTArray<HttpRetParams> data;
-        nsCOMPtr<NetDashboardCallback> cb;
-        nsIThread* thread;
-    };
 
     struct LogData
     {
         LogData(nsCString host, uint32_t serial, bool encryption):
             mHost(host),
             mSerial(serial),
             mMsgSent(0),
             mMsgReceived(0),
@@ -104,48 +73,33 @@ private:
         }
         uint32_t IndexOf(nsCString hostname, uint32_t mSerial)
         {
             LogData temp(hostname, mSerial, false);
             return data.IndexOf(temp);
         }
         nsTArray<LogData> data;
         mozilla::Mutex lock;
-        nsCOMPtr<NetDashboardCallback> cb;
-        nsIThread* thread;
-    };
-
-    struct DnsData
-    {
-        nsCOMPtr<nsIDNSService> serv;
-        nsTArray<DNSCacheEntries> data;
-        nsCOMPtr<NetDashboardCallback> cb;
-        nsIThread* thread;
     };
 
-    struct DnsLookup
-    {
-        nsCOMPtr<nsIDNSService> serv;
-        nsCOMPtr<nsICancelable> cancel;
-        nsCOMPtr<NetDashboardCallback> cb;
-    };
-
-    struct ConnectionData
-    {
-        nsCOMPtr<nsISocketTransport> socket;
-        nsCOMPtr<nsIInputStream> streamIn;
-        nsCOMPtr<nsITimer> timer;
-        nsCOMPtr<NetDashboardCallback> cb;
-        nsIThread* thread;
-    };
 
     bool mEnableLogging;
+    WebSocketData mWs;
 
-    struct SocketData mSock;
-    struct HttpData mHttp;
-    struct WebSocketData mWs;
-    struct DnsData mDns;
-    struct DnsLookup mDnsup;
-    struct ConnectionData mConn;
+private:
+    virtual ~Dashboard();
+
+    nsresult GetSocketsDispatch(SocketData *);
+    nsresult GetHttpDispatch(HttpData *);
+    nsresult GetDnsInfoDispatch(DnsData *);
+    nsresult TestNewConnection(ConnectionData *);
+
+    /* Helper methods that pass the JSON to the callback function. */
+    nsresult GetSockets(SocketData *);
+    nsresult GetHttpConnections(HttpData *);
+    nsresult GetDNSCacheEntries(DnsData *);
+    nsresult GetWebSocketConnections(WebSocketRequest *);
+
+    nsCOMPtr<nsIDNSService> mDnsService;
 };
 
 } } // namespace mozilla::net
 #endif // nsDashboard_h__
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -726,20 +726,20 @@ BookmarksStore.prototype = {
       let spinningCb = Async.makeSpinningCallback();
 
       let livemarkObj = {title: record.title,
                          parentId: record._parent,
                          index: PlacesUtils.bookmarks.DEFAULT_INDEX,
                          feedURI: Utils.makeURI(record.feedUri),
                          siteURI: siteURI,
                          guid: record.id};
-      PlacesUtils.livemarks.addLivemark(livemarkObj,
-        function (aStatus, aLivemark) {
-          spinningCb(null, [aStatus, aLivemark]);
-        });
+      PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+        aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+        () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+      );
 
       let [status, livemark] = spinningCb.wait();
       if (!Components.isSuccessCode(status)) {
         throw status;
       }
 
       this._log.debug("Created livemark " + livemark.id + " under " +
                       livemark.parentId + " as " + livemark.title +
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -33,17 +33,24 @@ SyncScheduler.prototype = {
   /**
    * The nsITimer object that schedules the next sync. See scheduleNextSync().
    */
   syncTimer: null,
 
   setDefaults: function setDefaults() {
     this._log.trace("Setting SyncScheduler policy values to defaults.");
 
-    this.singleDeviceInterval = Svc.Prefs.get("scheduler.singleDeviceInterval") * 1000;
+    let service = Cc["@mozilla.org/weave/service;1"]
+                    .getService(Ci.nsISupports)
+                    .wrappedJSObject;
+
+    let part = service.fxAccountsEnabled ? "fxa" : "sync11";
+    let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
+    this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
+
     this.idleInterval         = Svc.Prefs.get("scheduler.idleInterval")         * 1000;
     this.activeInterval       = Svc.Prefs.get("scheduler.activeInterval")       * 1000;
     this.immediateInterval    = Svc.Prefs.get("scheduler.immediateInterval")    * 1000;
     this.eolInterval          = Svc.Prefs.get("scheduler.eolInterval")          * 1000;
 
     // A user is non-idle on startup by default.
     this.idle = false;
 
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -9,22 +9,24 @@ pref("services.sync.termsURL", "https://
 pref("services.sync.privacyURL", "https://services.mozilla.com/privacy-policy/");
 pref("services.sync.statusURL", "https://services.mozilla.com/status/");
 pref("services.sync.syncKeyHelpURL", "https://services.mozilla.com/help/synckey");
 
 pref("services.sync.lastversion", "firstrun");
 pref("services.sync.sendVersionInfo", true);
 
 pref("services.sync.scheduler.eolInterval",         604800); // 1 week
-pref("services.sync.scheduler.singleDeviceInterval", 86400); // 1 day
 pref("services.sync.scheduler.idleInterval",         3600);  // 1 hour
 pref("services.sync.scheduler.activeInterval",       600);   // 10 minutes
 pref("services.sync.scheduler.immediateInterval",    90);    // 1.5 minutes
 pref("services.sync.scheduler.idleTime",             300);   // 5 minutes
 
+pref("services.sync.scheduler.fxa.singleDeviceInterval",     3600); // 1 hour
+pref("services.sync.scheduler.sync11.singleDeviceInterval", 86400); // 1 day
+
 pref("services.sync.errorhandler.networkFailureReportTimeout", 1209600); // 2 weeks
 
 pref("services.sync.engine.addons", true);
 pref("services.sync.engine.bookmarks", true);
 pref("services.sync.engine.history", true);
 pref("services.sync.engine.passwords", true);
 pref("services.sync.engine.prefs", true);
 pref("services.sync.engine.tabs", true);
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -120,26 +120,26 @@ add_test(function test_prefAttributes() 
   do_check_eq(Svc.Prefs.get('globalScore'), 0);
   do_check_eq(scheduler.globalScore, 0);
   scheduler.globalScore = SCORE;
   do_check_eq(scheduler.globalScore, SCORE);
   do_check_eq(Svc.Prefs.get('globalScore'), SCORE);
 
   _("Intervals correspond to default preferences.");
   do_check_eq(scheduler.singleDeviceInterval,
-              Svc.Prefs.get("scheduler.singleDeviceInterval") * 1000);
+              Svc.Prefs.get("scheduler.sync11.singleDeviceInterval") * 1000);
   do_check_eq(scheduler.idleInterval,
               Svc.Prefs.get("scheduler.idleInterval") * 1000);
   do_check_eq(scheduler.activeInterval,
               Svc.Prefs.get("scheduler.activeInterval") * 1000);
   do_check_eq(scheduler.immediateInterval,
               Svc.Prefs.get("scheduler.immediateInterval") * 1000);
 
   _("Custom values for prefs will take effect after a restart.");
-  Svc.Prefs.set("scheduler.singleDeviceInterval", 42);
+  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
   Svc.Prefs.set("scheduler.idleInterval", 23);
   Svc.Prefs.set("scheduler.activeInterval", 18);
   Svc.Prefs.set("scheduler.immediateInterval", 31415);
   scheduler.setDefaults();
   do_check_eq(scheduler.idleInterval, 23000);
   do_check_eq(scheduler.singleDeviceInterval, 42000);
   do_check_eq(scheduler.activeInterval, 18000);
   do_check_eq(scheduler.immediateInterval, 31415000);
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/modules/bookmarks.jsm
@@ -779,20 +779,20 @@ Livemark.prototype = {
                        title: this.props.livemark,
                        siteURI: siteURI,
                        feedURI: Services.io.newURI(this.props.feedUri, null, null),
                        index: PlacesUtils.bookmarks.DEFAULT_INDEX};
 
     // Until this can handle asynchronous creation, we need to spin.
     let spinningCb = Async.makeSpinningCallback();
 
-    PlacesUtils.livemarks.addLivemark(livemarkObj,
-      function (aStatus, aLivemark) {
-        spinningCb(null, [aStatus, aLivemark]);
-      });
+    PlacesUtils.livemarks.addLivemark(livemarkObj).then(
+      aLivemark => { spinningCb(null, [Components.results.NS_OK, aLivemark]) },
+      () => { spinningCb(null, [Components.results.NS_ERROR_UNEXPECTED, aLivemark]) }
+    );
 
     let [status, livemark] = spinningCb.wait();
     if (!Components.isSuccessCode(status)) {
       throw status;
     }
 
     this.props.item_id = livemark.id;
     return this.props.item_id;
--- a/toolkit/components/places/BookmarkHTMLUtils.jsm
+++ b/toolkit/components/places/BookmarkHTMLUtils.jsm
@@ -664,17 +664,17 @@ BookmarkImporter.prototype = {
         // The is a live bookmark.  We create it here since in HandleLinkBegin we
         // don't know the title.
         PlacesUtils.livemarks.addLivemark({
           "title": frame.previousText,
           "parentId": frame.containerId,
           "index": PlacesUtils.bookmarks.DEFAULT_INDEX,
           "feedURI": frame.previousFeed,
           "siteURI": frame.previousLink,
-        });
+        }).then(null, Cu.reportError);
       } else if (frame.previousLink) {
         // This is a common bookmark.
         PlacesUtils.bookmarks.setItemTitle(frame.previousId,
                                            frame.previousText);
       }
     } catch(e) {
     }
 
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -381,25 +381,23 @@ BookmarkImporter.prototype = {
           if (feedURI) {
             PlacesUtils.livemarks.addLivemark({
               title: aData.title,
               feedURI: feedURI,
               parentId: aContainer,
               index: aIndex,
               lastModified: aData.lastModified,
               siteURI: siteURI
-            }, function(aStatus, aLivemark) {
-              if (Components.isSuccessCode(aStatus)) {
-                let id = aLivemark.id;
-                if (aData.dateAdded)
-                  PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
-                if (aData.annos && aData.annos.length)
-                  PlacesUtils.setAnnotationsForItem(id, aData.annos);
-              }
-            });
+            }).then(function (aLivemark) {
+              let id = aLivemark.id;
+              if (aData.dateAdded)
+                PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded);
+              if (aData.annos && aData.annos.length)
+                PlacesUtils.setAnnotationsForItem(id, aData.annos);
+            }, Cu.reportError);
           }
         } else {
           id = PlacesUtils.bookmarks.createFolder(
                  aContainer, aData.title, aIndex);
           folderIdMap[aData.id] = id;
           // Process children
           if (aData.children) {
             for (let i = 0; i < aData.children.length; i++) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -0,0 +1,1182 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
+
+/**
+ * Overview
+ * --------
+ * This modules serves as the transactions manager for Places, and implements
+ * all the standard transactions for its UI commands (creating items, editing
+ * various properties, etc.). It shares most of its semantics with common
+ * command pattern implementations, the HTML5 Undo Manager in particular.
+ * However, the asynchronous design of [future] Places APIs, combined with the
+ * commitment to serialize all UI operations, makes things a little bit
+ * different.  For example, when |undo| is called in order to undo the top undo
+ * entry, the caller cannot tell for sure what entry would it be because the
+ * execution of some transaction is either in process, or queued.
+ *
+ * GUIDs and item-ids
+ * -------------------
+ * The Bookmarks API still relies heavily on item-ids, but since those do not
+ * play nicely with the concept of undo and redo (especially not in an
+ * asynchronous environment), this API only accepts bookmark GUIDs, both for
+ * input (e.g. for specifying the parent folder for a new bookmark) and for
+ * output (when the GUID for such a bookmark is propagated).
+ *
+ * GUIDs are readily available when dealing with the "output" of this API and
+ * when result nodes are used (see nsINavHistoryResultNode::bookmarkGUID).
+ * If you only have item-ids in hand, use PlacesUtils.promiseItemGUID for
+ * converting them.  Should you need to convert them back into itemIds, use
+ * PlacesUtils.promiseItemId.
+ *
+ * The Standard Transactions
+ * -------------------------
+ * At the bottom of this module you will find implementations for all Places UI
+ * commands (One should almost never fallback to raw Places APIs.  Please file
+ * a bug if you find anything uncovered). The transactions' constructors are
+ * set on the PlacesTransactions object (e.g. PlacesTransactions.NewFolder).
+ * The input for this constructors is taken in the form of a single argument
+ * plain object.  Input properties may be either required (e.g. the |keyword|
+ * property for the EditKeyword transaction) or optional (e.g. the |keyword|
+ * property for NewBookmark).  Once a transaction is created, you may pass it
+ * to |transact| or use it in the for batching (see next section).
+ *
+ * The constructors throw right away when any required input is missing or when
+ * some input is invalid "on the surface" (e.g. GUID values are validated to be
+ * 12-characters strings, but are not validated to point to existing item.  Such
+ * an error will reveal when the transaction is executed).
+ *
+ * To make things simple, a given input property has the same basic meaning and
+ * valid values across all transactions which accept it in the input object.
+ * Here is a list of all supported input properties along with their expected
+ * values:
+ *  - uri: an nsIURI object.
+ *  - feedURI: an nsIURI object, holding the url for a live bookmark.
+ *  - siteURI: an nsIURI object, holding the url for the site with which
+ *             a live bookmark is associated.
+ *  - GUID, parentGUID, newParentGUID: a valid places GUID string.
+ *  - title: a string
+ *  - index, newIndex: the position of an item in its containing folder,
+ *    starting from 0.
+ *    integer and PlacesUtils.bookmarks.DEFAULT_INDEX
+ *  - annotationObject: see PlacesUtils.setAnnotationsForItem
+ *  - annotations: an array of annotation objects as above.
+ *  - tags: an array of strings.
+ *
+ * Batching transactions
+ * ---------------------
+ * Sometimes it is useful to "batch" or "merge" transactions. For example,
+ * "Bookmark All Tabs" may be implemented as one NewFolder transaction followed
+ * by numerous NewBookmark transactions - all to be undone or redone in a single
+ * command.  The |transact| method makes this possible using a generator
+ * function as an input.  These generators have the same semantics as in
+ * Task.jsm except that when you yield a transaction, it's executed, and the
+ * resolution (e.g. the new bookmark GUID) is sent to the generator so you can
+ * use it as the input for another transaction.  See |transact| for the details.
+ *
+ * "Custom" transactions
+ * ----------------------
+ * While it is technically possible to pass in your own transactions to
+ * |transact|, it is highly recommended to avoid that for two reasons:
+ * (*) Slippery memory leak - A transaction implemented in the context of a
+ *     window reference that window through its prototype (and may also
+ *     do so due to js closures, prototypes for non-primitives and so on).
+ *     Because this module strongly references all transactions until they
+ *     are cleared from the transactions history (if at all), such a transaction
+ *     would block the release of the window object (along with a lot of other
+ *     stuff) from memory when the window is closed.
+ * (*) The Places Bookmarks API is due for a major overhaul - We expect
+ *     nsINavBookmarks to be replaced by an asynchronous interface sometime in
+ *     the near future.  This API is expected to remain mostly unchanged in
+ *     that process.
+ *
+ * It's almost always possible to avoid a custom transaction by batching the
+ * standard transactions.  However, if you must implement a custom transaction,
+ * do so in a JS module.
+ *
+ * The transactions-history structure
+ * ----------------------------------
+ * The transactions-history is a two-dimensional stack of transactions: the
+ * transactions are ordered in reverse to the order they were committed.
+ * It's two-dimensional because the undo manager allows batching transactions
+ * together for the purpose of undo or redo (batched transactions can never be
+ * undone or redone partially).
+ *
+ * The undoPosition property is set to the index of the top entry. If there is
+ * no entry at that index, there is nothing to undo.
+ * Entries prior to undoPosition, if any, are redo entries, the first one being
+ * the top redo entry.
+ *
+ * [ [2nd redo txn, 1st redo txn],  <= 2nd redo entry
+ *   [2nd redo txn, 1st redo txn],  <= 1st redo entry
+ *   [1st undo txn, 2nd undo txn],  <= 1st undo entry
+ *   [1st undo txn, 2nd undo txn]   <= 2nd undo entry ]
+ * undoPostion: 2.
+ *
+ * Note that when a new entry is created, all redo entries are removed.
+ */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "console",
+                                  "resource://gre/modules/devtools/Console.jsm");
+
+// The internal object for managing the transactions history.
+// The public API is included in PlacesTransactions.
+// TODO bug 982099: extending the array "properly" makes it painful to implement
+// getters.  If/when ES6 gets proper array subclassing we can revise this.
+let TransactionsHistory = [];
+TransactionsHistory.__proto__ = {
+  __proto__: Array.prototype,
+
+  // The index of the first undo entry (if any) - See the documentation
+  // at the top of this file.
+  _undoPosition: 0,
+  get undoPosition() this._undoPosition,
+
+  // Handy shortcuts
+  get topUndoEntry() this.undoPosition < this.length ?
+                     this[this.undoPosition] : null,
+  get topRedoEntry() this.undoPosition > 0 ?
+                     this[this.undoPosition - 1] : null,
+
+  /**
+   * Undo the top undo entry, if any, and update the undo position accordingly.
+   */
+  undo: function* () {
+    let entry = this.topUndoEntry;
+    if (!entry)
+      return;
+
+    for (let transaction of entry) {
+      try {
+        yield transaction.undo();
+      }
+      catch(ex) {
+        // If one transaction is broken, it's not safe to work with any other
+        // undo entry.  Report the error and clear the undo history.
+        console.error(ex,
+                      "Couldn't undo a transaction, clearing all undo entries.");
+        this.clearUndoEntries();
+        return;
+      }
+    }
+    this._undoPosition++;
+  },
+
+  /**
+   * Redo the top redo entry, if any, and update the undo position accordingly.
+   */
+  redo: function* () {
+    let entry = this.topRedoEntry;
+    if (!entry)
+      return;
+
+    for (let i = entry.length - 1; i >= 0; i--) {
+      let transaction = entry[i];
+      try {
+        if (transaction.redo)
+          yield transaction.redo();
+        else
+          yield transaction.execute();
+      }
+      catch(ex) {
+        // If one transaction is broken, it's not safe to work with any other
+        // redo entry. Report the error and clear the undo history.
+        console.error(ex,
+                      "Couldn't redo a transaction, clearing all redo entries.");
+        this.clearRedoEntries();
+        return;
+      }
+    }
+    this._undoPosition--;
+  },
+
+  /**
+   * Add a transaction either as a new entry, if forced or if there are no undo
+   * entries, or to the top undo entry.
+   *
+   * @param aTransaction
+   *        the transaction object to be added to the transaction history.
+   * @param [optional] aForceNewEntry
+   *        Force a new entry for the transaction. Default: false.
+   *        If false, an entry will we created only if there's no undo entry
+   *        to extend.
+   */
+  add: function (aTransaction, aForceNewEntry = false) {
+    if (this.length == 0 || aForceNewEntry) {
+      this.clearRedoEntries();
+      this.unshift([aTransaction]);
+    }
+    else {
+      this[this.undoPosition].unshift(aTransaction);
+    }
+  },
+
+  /**
+   * Clear all undo entries.
+   */
+  clearUndoEntries: function () {
+    if (this.undoPosition < this.length)
+      this.splice(this.undoPosition);
+  },
+
+  /**
+   * Clear all redo entries.
+   */
+  clearRedoEntries: function () {
+    if (this.undoPosition > 0) {
+      this.splice(0, this.undoPosition);
+      this._undoPosition = 0;
+    }
+  },
+
+  /**
+   * Clear all entries.
+   */
+  clearAllEntries: function () {
+    if (this.length > 0) {
+      this.splice(0);
+      this._undoPosition = 0;
+    }
+  }
+};
+
+
+// Our transaction manager is asynchronous in the sense that all of its methods
+// don't execute synchronously. However, all actions must be serialized.
+let currentTask = Promise.resolve();
+function Serialize(aTask) {
+  // Ignore failures.
+  return currentTask = currentTask.then( () => Task.spawn(aTask) )
+                                  .then(null, Components.utils.reportError);
+}
+
+let PlacesTransactions = {
+  /**
+   * Asynchronously transact either a single transaction, or a sequence of
+   * transactions that would be treated as a single entry in the transactions
+   * history.
+   *
+   * @param aTransactionOrGeneratorFunction
+   *        Either a transaction object or a generator function (ES6-style only)
+   *        that yields transaction objects.
+   *
+   *        Generator mode how-to: when a transaction is yielded, it's executed.
+   *        Then, if it was executed successfully, the resolution of |execute|
+   *        is sent to the generator.  If |execute| threw or rejected, the
+   *        exception is propagated to the generator.
+   *        Any other value yielded by a generator function is handled the
+   *        same way as in a Task (see Task.jsm).
+   *
+   * @return {Promise}
+   * @resolves either to the resolution of |execute|, in single-transaction mode,
+   * or to the return value of the generator, in generator-mode.
+   * @rejects either if |execute| threw, in single-transaction mode, or if
+   * the generator function threw (or didn't handle) an exception, in generator
+   * mode.
+   *
+   * @note If no transaction was executed successfully, the transactions history
+   * is not affected.
+   *
+   * @note All PlacesTransactions operations are serialized. This means that the
+   * transactions history state may change by the time the transaction/generator
+   * is processed.  It's guaranteed, however, that a generator function "blocks"
+   * the queue: that is, it is assured that no other operations are performed
+   * by or on PlacesTransactions until the generator returns.  Keep in mind you
+   * are not protected from consumers who use the raw places APIs directly.
+   */
+  transact: function (aTransactionOrGeneratorFunction) {
+    return Serialize(function* () {
+      // The entry in the transactions history is created once the first
+      // transaction is committed. This means that if |transact| is called
+      // in its "generator mode" and no transactions are committed by the
+      // generator, the transactions history is left unchanged.
+      // Bug 982115: Depending on how this API is actually used we may revise
+      // this decision and make it so |transact| always forces a new entry.
+      let forceNewEntry = true;
+      function* transactOneTransaction(aTransaction) {
+        let retval = yield aTransaction.execute();
+        TransactionsHistory.add(aTransaction, forceNewEntry);
+        forceNewEntry = false;
+        return retval;
+      }
+
+      function* transactBatch(aGeneratorFunction) {
+        let generator = aGeneratorFunction(), sendValue = undefined;
+        while (true) {
+          let next = generator.next(sendValue);
+          sendValue = next.value;
+          if (typeof(sendValue) == "function") {
+            sendValue = yield transactBatch(sendValue);
+          }
+          else if (typeof(sendValue) == "object" && sendValue) {
+            if ("execute" in sendValue)
+              sendValue = yield transactOneTransaction(sendValue);
+            else if ("then" in sendValue)
+              sendValue = yield sendValue;
+          }
+          if (next.done)
+            break;
+        }
+        return sendValue;
+      }
+
+      if (typeof(aTransactionOrGeneratorFunction) == "function")
+        return yield transactBatch(aTransactionOrGeneratorFunction);
+      else
+        return yield transactOneTransaction(aTransactionOrGeneratorFunction);
+    }.bind(this));
+  },
+
+  /**
+   * Asynchronously undo the transaction immediately after the current undo
+   * position in the transactions history in the reverse order, if any, and
+   * adjusts the undo position.
+   *
+   * @return {Promises).  The promise always resolves.
+   * @note All undo manager operations are queued. This means that transactions
+   * history may change by the time your request is fulfilled.
+   */
+  undo: function () Serialize(() => TransactionsHistory.undo()),
+
+  /**
+   * Asynchronously redo the transaction immediately before the current undo
+   * position in the transactions history, if any, and adjusts the undo
+   * position.
+   *
+   * @return {Promises).  The promise always resolves.
+   * @note All undo manager operations are queued. This means that transactions
+   * history may change by the time your request is fulfilled.
+   */
+  redo: function () Serialize(() => TransactionsHistory.redo()),
+
+  /**
+   * Asynchronously clear the undo, redo, or all entries from the transactions
+   * history.
+   *
+   * @param [optional] aUndoEntries
+   *        Whether or not to clear undo entries.  Default: true.
+   * @param [optional] aRedoEntries
+   *        Whether or not to clear undo entries.  Default: true.
+   *
+   * @return {Promises).  The promise always resolves.
+   * @throws if both aUndoEntries and aRedoEntries are false.
+   * @note All undo manager operations are queued. This means that transactions
+   * history may change by the time your request is fulfilled.
+   */
+  clearTransactionsHistory:
+  function (aUndoEntries = true, aRedoEntries = true) {
+    return Serialize(function* () {
+      if (aUndoEntries && aRedoEntries)
+        TransactionsHistory.clearAllEntries();
+      else if (aUndoEntries)
+        TransactionsHistory.clearUndoEntries();
+      else if (aRedoEntries)
+        TransactionsHistory.clearRedoEntries();
+      else
+        throw new Error("either aUndoEntries or aRedoEntries should be true");
+    });
+  },
+
+  /**
+   * The numbers of entries in the transactions history.
+   */
+  get length() TransactionsHistory.length,
+
+  /**
+   * Get the transaction history entry at a given index.  Each entry consists
+   * of one or more transaction objects.
+   *
+   * THIS METHOD SHOULD BE USED ONLY AS A WAY TO MONITOR THE TRANSACTIONS
+   * HISTORY STATE.  NEVER CALL THE TRANSACTION METHODS (execute, undo, redo)
+   * DIRECTLY.
+   *
+   * @param aIndex
+   *        the index of the entry to retrieve.
+   * @return an array of transaction objects in their undo order (that is,
+   * reversely to the order they were executed).
+   * @throw if aIndex is invalid (< 0 or >= length).
+   * @note the returned array is a clone of the history entry and is not
+   * kept in sync with the original entry if it changes.
+   */
+  item: function (aIndex) {
+    if (!Number.isInteger(aIndex) || aIndex < 0 || aIndex >= this.length)
+      throw new Error("Invalid index");
+
+    return TransactionsHistory[aIndex];
+  },
+
+  /**
+   * The index of the top undo entry in the transactions history.
+   * If there are no undo entries, it equals to |length|.
+   * Entries past this point
+   * Entries at and past this point are redo entries.
+   */
+  get undoPosition() TransactionsHistory.undoPosition,
+};
+
+/**
+ * Internal helper for defining the standard transactions and their input.
+ * It takes the required and optional properties, and generates the public
+ * constructor (which takes the input in the form of a plain object) which,
+ * when called, creates the argument-less "public" |execute| method by binding
+ * the input properties to the function arguments (required properties first,
+ * then the optional properties).
+ *
+ * If this seems confusing, look at the consumers.
+ *
+ * This magic serves two purposes:
+ * (1) It completely hides the transactions' internals from the module
+ *     consumers.
+ * (2) It keeps each transaction implementation to what is about, bypassing
+ *     all this bureaucracy while still validating input appropriately.
+ */
+function DefineTransaction(aRequiredProps = [], aOptionalProps = []) {
+  for (let prop of [...aRequiredProps, ...aOptionalProps]) {
+    if (!DefineTransaction.inputProps.has(prop))
+      throw new Error("Property '" + prop + "' is not defined");
+  }
+
+  let ctor = function (aInput) {
+    // We want to support both syntaxes:
+    // let t = new PlacesTransactions.NewBookmark(),
+    // let t = PlacesTransactions.NewBookmark()
+    if (this == PlacesTransactions)
+      return new ctor(aInput);
+
+    if (aRequiredProps.length > 0 || aOptionalProps.length > 0) {
+      // Bind the input properties to the arguments of execute.
+      let input = DefineTransaction.verifyInput(aInput, aRequiredProps,
+                                                aOptionalProps);
+      let executeArgs = [this,
+                         ...[input[prop] for (prop of aRequiredProps)],
+                         ...[input[prop] for (prop of aOptionalProps)]];
+      this.execute = Function.bind.apply(this.execute, executeArgs);
+    }
+    return this;
+  };
+  return ctor;
+}
+
+DefineTransaction.isStr = v => typeof(v) == "string";
+DefineTransaction.isURI = v => v instanceof Components.interfaces.nsIURI;
+DefineTransaction.isIndex = v => Number.isInteger(v) &&
+                                 v >= PlacesUtils.bookmarks.DEFAULT_INDEX;
+DefineTransaction.isGUID = v => /^[a-zA-Z0-9\-_]{12}$/.test(v);
+DefineTransaction.isPrimitive = v => v === null || (typeof(v) != "object" &&
+                                                    typeof(v) != "function");
+DefineTransaction.isAnnotationObject = function (obj) {
+  let checkProperty = (aPropName, aRequired, aCheckFunc) => {
+    if (aPropName in obj)
+      return aCheckFunc(obj[aPropName]);
+
+    return !aRequired;
+  };
+
+  if (obj &&
+      checkProperty("name",    true,  DefineTransaction.isStr)      &&
+      checkProperty("expires", false, Number.isInteger) &&
+      checkProperty("flags",   false, Number.isInteger) &&
+      checkProperty("value",   false, DefineTransaction.isPrimitive) ) {
+    // Nothing else should be set
+    let validKeys = ["name", "value", "flags", "expires"];
+    if (Object.keys(obj).every( (k) => validKeys.indexOf(k) != -1 ))
+      return true;
+  }
+  return false;
+};
+
+DefineTransaction.inputProps = new Map();
+DefineTransaction.defineInputProps =
+function (aNames, aValidationFunction, aDefaultValue) {
+  for (let name of aNames) {
+    this.inputProps.set(name, {
+      validate:     aValidationFunction,
+      defaultValue: aDefaultValue,
+      isGUIDProp:   false
+    });
+  }
+};
+
+DefineTransaction.defineArrayInputProp =
+function (aName, aValidationFunction, aDefaultValue) {
+  this.inputProps.set(aName, {
+    validate:     (v) => Array.isArray(v) && v.every(aValidationFunction),
+    defaultValue: aDefaultValue,
+    isGUIDProp:   false
+  });
+};
+
+DefineTransaction.verifyPropertyValue =
+function (aProp, aValue, aRequired) {
+  if (aValue === undefined) {
+    if (aRequired)
+      throw new Error("Required property is missing: " + aProp);
+    return this.inputProps.get(aProp).defaultValue;
+  }
+
+  if (!this.inputProps.get(aProp).validate(aValue))
+    throw new Error("Invalid value for property: " + aProp);
+
+  if (Array.isArray(aValue)) {
+    // The original array cannot be referenced by this module because it would
+    // then implicitly reference its global as well.
+    return Components.utils.cloneInto(aValue, {});
+  }
+
+  return aValue;
+};
+
+DefineTransaction.verifyInput =
+function (aInput, aRequired = [], aOptional = []) {
+  if (aRequired.length == 0 && aOptional.length == 0)
+    return {};
+
+  // If there's just a single required/optional property, we allow passing it
+  // as is, so, for example, one could do PlacesTransactions.RemoveItem(myGUID)
+  // rather than PlacesTransactions.RemoveItem({ GUID: myGUID}).
+  // This shortcut isn't supported for "complex" properties - e.g. one cannot
+  // pass an annotation object this way (note there is no use case for this at
+  // the moment anyway).
+  let isSinglePropertyInput =
+    this.isPrimitive(aInput) ||
+    (aInput instanceof Components.interfaces.nsISupports);
+  let fixedInput = { };
+  if (aRequired.length > 0) {
+    if (isSinglePropertyInput) {
+      if (aRequired.length == 1) {
+        let prop = aRequired[0], value = aInput;
+        value = this.verifyPropertyValue(prop, value, true);
+        fixedInput[prop] = value;
+      }
+      else {
+        throw new Error("Transaction input isn't an object");
+      }
+    }
+    else {
+      for (let prop of aRequired) {
+        let value = this.verifyPropertyValue(prop, aInput[prop], true);
+        fixedInput[prop] = value;
+      }
+    }
+  }
+
+  if (aOptional.length > 0) {
+    if (isSinglePropertyInput && !aRequired.length > 0) {
+      if (aOptional.length == 1) {
+        let prop = aOptional[0], value = aInput;
+        value = this.verifyPropertyValue(prop, value, true);
+        fixedInput[prop] = value;
+      }
+      else if (aInput !== null && aInput !== undefined) {
+        throw new Error("Transaction input isn't an object");
+      }
+    }
+    else {
+      for (let prop of aOptional) {
+        let value = this.verifyPropertyValue(prop, aInput[prop], false);
+        if (value !== undefined)
+          fixedInput[prop] = value;
+        else
+          fixedInput[prop] = this.defaultValues[prop];
+      }
+    }
+  }
+
+  return fixedInput;
+};
+
+// Update the documentation at the top of this module if you add or
+// remove properties.
+DefineTransaction.defineInputProps(["uri", "feedURI", "siteURI"],
+                                   DefineTransaction.isURI, null);
+DefineTransaction.defineInputProps(["GUID", "parentGUID", "newParentGUID"],
+                                   DefineTransaction.isGUID);
+DefineTransaction.defineInputProps(["title", "keyword", "postData"],
+                                   DefineTransaction.isStr, "");
+DefineTransaction.defineInputProps(["index", "newIndex"],
+                                   DefineTransaction.isIndex,
+                                   PlacesUtils.bookmarks.DEFAULT_INDEX);
+DefineTransaction.defineInputProps(["annotationObject"],
+                                   DefineTransaction.isAnnotationObject);
+DefineTransaction.defineArrayInputProp("tags",
+                                       DefineTransaction.isStr, null);
+DefineTransaction.defineArrayInputProp("annotations",
+                                       DefineTransaction.isAnnotationObject,
+                                       null);
+
+/**
+ * Internal helper for implementing the execute method of NewBookmark, NewFolder
+ * and NewSeparator.
+ *
+ * @param aTransaction
+ *        The transaction object
+ * @param aParentGUID
+ *        The guid of the parent folder
+ * @param aCreateItemFunction(aParentId, aGUIDToRestore)
+ *        The function to be called for creating the item on execute and redo.
+ *        It should return the itemId for the new item
+ *        - aGUIDToRestore - the GUID to set for the item (used for redo).
+ * @param [optional] aOnUndo
+ *        an additional function to call after undo
+ * @param [optional] aOnRedo
+ *        an additional function to call after redo
+ */
+function* ExecuteCreateItem(aTransaction, aParentGUID, aCreateItemFunction,
+                            aOnUndo = null, aOnRedo = null) {
+  let parentId = yield PlacesUtils.promiseItemId(aParentGUID),
+      itemId = yield aCreateItemFunction(parentId, ""),
+      guid = yield PlacesUtils.promiseItemGUID(itemId);
+
+  // On redo, we'll restore the date-added and last-modified properties.
+  let dateAdded = 0, lastModified = 0;
+  aTransaction.undo = function* () {
+    if (dateAdded == 0) {
+      dateAdded = PlacesUtils.bookmarks.getItemDateAdded(itemId);
+      lastModified = PlacesUtils.bookmarks.getItemLastModified(itemId);
+    }
+    PlacesUtils.bookmarks.removeItem(itemId);
+    if (aOnUndo) {
+      yield aOnUndo();
+    }
+  };
+  aTransaction.redo = function* () {
+    parentId = yield PlacesUtils.promiseItemId(aParentGUID);
+    itemId = yield aCreateItemFunction(parentId, guid);
+    if (aOnRedo)
+      yield aOnRedo();
+
+    // aOnRedo is called first to make sure it doesn't override
+    // lastModified.
+    PlacesUtils.bookmarks.setItemDateAdded(itemId, dateAdded);
+    PlacesUtils.bookmarks.setItemLastModified(itemId, lastModified);
+  };
+  return guid;
+}
+
+/*****************************************************************************
+ * The Standard Places Transactions.
+ *
+ * See the documentation at the top of this file. The valid values for input
+ * are also documented there.
+ *****************************************************************************/
+
+let PT = PlacesTransactions;
+
+/**
+ * Transaction for creating a bookmark.
+ *
+ * Required Input Properties: uri, parentGUID.
+ * Optional Input Properties: index, title, keyword, annotations, tags.
+ *
+ * When this transaction is executed, it's resolved to the new bookmark's GUID.
+ */
+PT.NewBookmark = DefineTransaction(["parentGUID", "uri"],
+                                   ["index", "title", "keyword", "postData",
+                                    "annotations", "tags"]);
+PT.NewBookmark.prototype = Object.seal({
+  execute: function (aParentGUID, aURI, aIndex, aTitle,
+                     aKeyword, aPostData, aAnnos, aTags) {
+    return ExecuteCreateItem(this, aParentGUID,
+      function (parentId, guidToRestore = "") {
+        let itemId = PlacesUtils.bookmarks.insertBookmark(
+          parentId, aURI, aIndex, aTitle, guidToRestore);
+        if (aKeyword)
+          PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
+        if (aPostData)
+          PlacesUtils.setPostDataForBookmark(itemId, aPostData);
+        if (aAnnos)
+          PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
+        if (aTags && aTags.length > 0) {
+          let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
+          aTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)];
+          PlacesUtils.tagging.tagURI(aURI, aTags);
+        }
+
+        return itemId;
+      },
+      function _additionalOnUndo() {
+        if (aTags && aTags.length > 0)
+          PlacesUtils.tagging.untagURI(aURI, aTags);
+      });
+  }
+});
+
+/**
+ * Transaction for creating a folder.
+ *
+ * Required Input Properties: title, parentGUID.
+ * Optional Input Properties: index, annotations.
+ *
+ * When this transaction is executed, it's resolved to the new folder's GUID.
+ */
+PT.NewFolder = DefineTransaction(["parentGUID", "title"],
+                                 ["index", "annotations"]);
+PT.NewFolder.prototype = Object.seal({
+  execute: function (aParentGUID, aTitle, aIndex, aAnnos) {
+    return ExecuteCreateItem(this,  aParentGUID,
+      function(parentId, guidToRestore = "") {
+        let itemId = PlacesUtils.bookmarks.createFolder(
+          parentId, aTitle, aIndex, guidToRestore);
+        if (aAnnos)
+          PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
+        return itemId;
+      });
+  }
+});
+
+/**
+ * Transaction for creating a separator.
+ *
+ * Required Input Properties: parentGUID.
+ * Optional Input Properties: index.
+ *
+ * When this transaction is executed, it's resolved to the new separator's
+ * GUID.
+ */
+PT.NewSeparator = DefineTransaction(["parentGUID"], ["index"]);
+PT.NewSeparator.prototype = Object.seal({
+  execute: function (aParentGUID, aIndex) {
+    return ExecuteCreateItem(this, aParentGUID,
+      function (parentId, guidToRestore = "") {
+        let itemId = PlacesUtils.bookmarks.insertSeparator(
+          parentId, aIndex, guidToRestore);
+        return itemId;
+      });
+  }
+});
+
+/**
+ * Transaction for creating a live bookmark (see mozIAsyncLivemarks for the
+ * semantics).
+ *
+ * Required Input Properties: feedURI, title, parentGUID.
+ * Optional Input Properties: siteURI, index, annotations.
+ *
+ * When this transaction is executed, it's resolved to the new separators's
+ * GUID.
+ */
+PT.NewLivemark = DefineTransaction(["feedURI", "title", "parentGUID"],
+                                   ["siteURI", "index", "annotations"]);
+PT.NewLivemark.prototype = Object.seal({
+  execute: function* (aFeedURI, aTitle, aParentGUID, aSiteURI, aIndex, aAnnos) {
+    let createItem = function* (aGUID = "") {
+      let parentId = yield PlacesUtils.promiseItemId(aParentGUID);
+      let livemarkInfo = {
+        title: aTitle
+      , feedURI: aFeedURI
+      , parentId: parentId
+      , index: aIndex
+      , siteURI: aSiteURI };
+      if (aGUID)
+        livemarkInfo.guid = aGUID;
+
+      let livemark = yield PlacesUtils.livemarks.addLivemark(livemarkInfo);
+      if (aAnnos)
+        PlacesUtils.setAnnotationsForItem(livemark.id, aAnnos);
+
+      return livemark;
+    };
+
+    let guid = (yield createItem()).guid;
+    this.undo = function* () {
+      yield PlacesUtils.livemarks.removeLivemark({ guid: guid });
+    };
+    this.redo = function* () {
+      yield createItem(guid);
+    };
+    return guid;
+  }
+});
+
+/**
+ * Transaction for moving an item.
+ *
+ * Required Input Properties: GUID, newParentGUID, newIndex.
+ */
+PT.MoveItem = DefineTransaction(["GUID", "newParentGUID", "newIndex"]);
+PT.MoveItem.prototype = Object.seal({
+  execute: function* (aGUID, aNewParentGUID, aNewIndex) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID),
+        oldParentId = PlacesUtils.bookmarks.getFolderIdForItem(itemId),
+        oldIndex = PlacesUtils.bookmarks.getItemIndex(itemId),
+        newParentId = yield PlacesUtils.promiseItemId(aNewParentGUID);
+
+    PlacesUtils.bookmarks.moveItem(itemId, newParentId, aNewIndex);
+
+    let undoIndex = PlacesUtils.bookmarks.getItemIndex(itemId);
+    this.undo = () => {
+      // Moving down in the same parent takes in count removal of the item
+      // so to revert positions we must move to oldIndex + 1
+      if (newParentId == oldParentId && oldIndex > undoIndex)
+        PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex + 1);
+      else
+        PlacesUtils.bookmarks.moveItem(itemId, oldParentId, oldIndex);
+    };
+  }
+});
+
+/**
+ * Transaction for setting the title for an item.
+ *
+ * Required Input Properties: GUID, title.
+ */
+PT.EditTitle = DefineTransaction(["GUID", "title"]);
+PT.EditTitle.prototype = Object.seal({
+  execute: function* (aGUID, aTitle) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID),
+        oldTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
+    PlacesUtils.bookmarks.setItemTitle(itemId, aTitle);
+    this.undo = () => { PlacesUtils.bookmarks.setItemTitle(itemId, oldTitle); };
+  }
+});
+
+/**
+ * Transaction for setting the URI for an item.
+ *
+ * Required Input Properties: GUID, uri.
+ */
+PT.EditURI = DefineTransaction(["GUID", "uri"]);
+PT.EditURI.prototype = Object.seal({
+  execute: function* (aGUID, aURI) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID),
+        oldURI = PlacesUtils.bookmarks.getBookmarkURI(itemId),
+        oldURITags = PlacesUtils.tagging.getTagsForURI(oldURI),
+        newURIAdditionalTags = null;
+    PlacesUtils.bookmarks.changeBookmarkURI(itemId, aURI);
+
+    // Move tags from old URI to new URI.
+    if (oldURITags.length > 0) {
+      // Only untag the old URI if this is the only bookmark.
+      if (PlacesUtils.getBookmarksForURI(oldURI, {}).length == 0)
+        PlacesUtils.tagging.untagURI(oldURI, oldURITags);
+
+      let currentNewURITags = PlacesUtils.tagging.getTagsForURI(aURI);
+      newURIAdditionalTags = [t for (t of oldURITags)
+                              if (currentNewURITags.indexOf(t) == -1)];
+      if (newURIAdditionalTags)
+        PlacesUtils.tagging.tagURI(aURI, newURIAdditionalTags);
+    }
+
+    this.undo = () => {
+      PlacesUtils.bookmarks.changeBookmarkURI(itemId, oldURI);
+      // Move tags from new URI to old URI.
+      if (oldURITags.length > 0) {
+        // Only untag the new URI if this is the only bookmark.
+        if (newURIAdditionalTags && newURIAdditionalTags.length > 0 &&
+            PlacesUtils.getBookmarksForURI(aURI, {}).length == 0) {
+          PlacesUtils.tagging.untagURI(aURI, newURIAdditionalTags);
+        }
+
+        PlacesUtils.tagging.tagURI(oldURI, oldURITags);
+      }
+    };
+  }
+});
+
+/**
+ * Transaction for setting an annotation for an item.
+ *
+ * Required Input Properties: GUID, annotationObject
+ */
+PT.SetItemAnnotation = DefineTransaction(["GUID", "annotationObject"]);
+PT.SetItemAnnotation.prototype = {
+  execute: function* (aGUID, aAnno) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID), oldAnno;
+    if (PlacesUtils.annotations.itemHasAnnotation(itemId, aAnno.name)) {
+      // Fill the old anno if it is set.
+      let flags = {}, expires = {};
+      PlacesUtils.annotations.getItemAnnotationInfo(itemId, aAnno.name, flags,
+                                                    expires, { });
+      let value = PlacesUtils.annotations.getItemAnnotation(itemId, aAnno.name);
+      oldAnno = { name: aAnno.name, flags: flags.value,
+                  value: value, expires: expires.value };
+    }
+    else {
+      // An unset value removes the annoation.
+      oldAnno = { name: aAnno.name };
+    }
+
+    PlacesUtils.setAnnotationsForItem(itemId, [aAnno]);
+    this.undo = () => { PlacesUtils.setAnnotationsForItem(itemId, [oldAnno]); };
+  }
+};
+
+/**
+ * Transaction for setting the keyword for a bookmark.
+ *
+ * Required Input Properties: GUID, keyword.
+ */
+PT.EditKeyword = DefineTransaction(["GUID", "keyword"]);
+PT.EditKeyword.prototype = Object.seal({
+  execute: function* (aGUID, aKeyword) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID),
+        oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
+    PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
+    this.undo = () => {
+      PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword);
+    };
+  }
+});
+
+/**
+ * Transaction for sorting a folder by name.
+ *
+ * Required Input Properties: GUID.
+ */
+PT.SortByName = DefineTransaction(["GUID"]);
+PT.SortByName.prototype = {
+  execute: function* (aGUID) {
+    let itemId = yield PlacesUtils.promiseItemId(aGUID),
+        oldOrder = [],  // [itemId] = old index
+        contents = PlacesUtils.getFolderContents(itemId, false, false).root,
+        count = contents.childCount;
+
+    // Sort between separators.
+    let newOrder = [], // nodes, in the new order.
+        preSep   = []; // Temporary array for sorting each group of nodes.
+    let sortingMethod = (a, b) => {
+      if (PlacesUtils.nodeIsContainer(a) && !PlacesUtils.nodeIsContainer(b))
+        return -1;
+      if (!PlacesUtils.nodeIsContainer(a) && PlacesUtils.nodeIsContainer(b))
+        return 1;
+      return a.title.localeCompare(b.title);
+    };
+
+    for (let i = 0; i < count; ++i) {
+      let node = contents.getChild(i);
+      oldOrder[node.itemId] = i;
+      if (PlacesUtils.nodeIsSeparator(node)) {
+        if (preSep.length > 0) {
+          preSep.sort(sortingMethod);
+          newOrder = newOrder.concat(preSep);
+          preSep.splice(0, preSep.length);
+        }
+        newOrder.push(node);
+      }
+      else
+        preSep.push(node);
+    }
+    contents.containerOpen = false;
+
+    if (preSep.length > 0) {
+      preSep.sort(sortingMethod);
+      newOrder = newOrder.concat(preSep);
+    }
+
+    // Set the nex indexes.
+    let callback = {
+      runBatched: function() {
+        for (let i = 0; i < newOrder.length; ++i) {
+          PlacesUtils.bookmarks.setItemIndex(newOrder[i].itemId, i);
+        }
+      }
+    };
+    PlacesUtils.bookmarks.runInBatchMode(callback, null);
+
+    this.undo = () => {
+      let callback = {
+        runBatched: function() {
+          for (let item in oldOrder) {
+            PlacesUtils.bookmarks.setItemIndex(item, oldOrder[item]);
+          }
+        }
+      };
+      PlacesUtils.bookmarks.runInBatchMode(callback, null);
+    };
+  }
+};
+
+/**
+ * Transaction for removing an item (any type).
+ *
+ * Required Input Properties: GUID.
+ */
+PT.RemoveItem = DefineTransaction(["GUID"]);
+PT.RemoveItem.prototype = {
+  execute: function* (aGUID) {
+    const bms = PlacesUtils.bookmarks;
+
+    let itemsToRestoreOnUndo = [];
+    function* saveItemRestoreData(aItem, aNode = null) {
+      if (!aItem || !aItem.GUID)
+        throw new Error("invalid item object");
+
+      let itemId = aNode ?
+                   aNode.itemId : yield PlacesUtils.promiseItemId(aItem.GUID);
+      if (itemId == -1)
+        throw new Error("Unexpected non-bookmarks node");
+
+      aItem.itemType = function() {
+        if (aNode) {
+          switch (aNode.type) {
+            case aNode.RESULT_TYPE_SEPARATOR:
+              return bms.TYPE_SEPARATOR;
+            case aNode.RESULT_TYPE_URI:   // regular bookmarks
+            case aNode.RESULT_TYPE_FOLDER_SHORTCUT:  // place:folder= bookmarks
+            case aNode.RESULT_TYPE_QUERY: // smart bookmarks
+              return bms.TYPE_BOOKMARK;
+            case aNode.RESULT_TYPE_FOLDER:
+              return bms.TYPE_FOLDER;
+            default:
+              throw new Error("Unexpected node type");
+          }
+        }
+        return bms.getItemType(itemId);
+      }();
+
+      let node = aNode;
+      if (!node && aItem.itemType == bms.TYPE_FOLDER)
+        node = PlacesUtils.getFolderContents(itemId).root;
+
+      // dateAdded, lastModified and annotations apply to all types.
+      aItem.dateAdded = node ? node.dateAdded : bms.getItemDateAdded(itemId);
+      aItem.lastModified = node ?
+                           node.lastModified : bms.getItemLastModified(itemId);
+      aItem.annotations = PlacesUtils.getAnnotationsForItem(itemId);
+
+      // For the first-level item, we don't have the parent.
+      if (!aItem.parentGUID) {
+        let parentId     = PlacesUtils.bookmarks.getFolderIdForItem(itemId);
+        aItem.parentGUID = yield PlacesUtils.promiseItemGUID(parentId);
+        // For the first-level item, we also need the index.
+        // Note: node.bookmarkIndex doesn't work for root nodes.
+        aItem.index      = bms.getItemIndex(itemId);
+      }
+
+      // Separators don't have titles.
+      if (aItem.itemType != bms.TYPE_SEPARATOR) {
+        aItem.title = node ? node.title : bms.getItemTitle(itemId);
+
+        if (aItem.itemType == bms.TYPE_BOOKMARK) {
+          aItem.uri =
+            node ? NetUtil.newURI(node.uri) : bms.getBookmarkURI(itemId);
+          aItem.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
+
+          // This may be the last bookmark (excluding the tag-items themselves)
+          // for the URI, so we need to preserve the tags.
+          let tags = PlacesUtils.tagging.getTagsForURI(aItem.uri);;
+          if (tags.length > 0)
+            aItem.tags = tags;
+        }
+        else { // folder
+          // We always have the node for folders
+          aItem.readOnly = node.childrenReadOnly;
+          for (let i = 0; i < node.childCount; i++) {
+            let childNode = node.getChild(i);
+            let childItem =
+              { GUID: yield PlacesUtils.promiseItemGUID(childNode.itemId)
+              , parentGUID: aItem.GUID };
+            itemsToRestoreOnUndo.push(childItem);
+            yield saveItemRestoreData(childItem, childNode);
+          }
+          node.containerOpen = false;
+        }
+      }
+    }
+
+    let item = { GUID: aGUID, parentGUID: null };
+    itemsToRestoreOnUndo.push(item);
+    yield saveItemRestoreData(item);
+
+    let itemId = yield PlacesUtils.promiseItemId(aGUID);
+    PlacesUtils.bookmarks.removeItem(itemId);
+    this.undo = function() {
+      for (let item of itemsToRestoreOnUndo) {
+        let parentId = yield PlacesUtils.promiseItemId(item.parentGUID);
+        let index = "index" in item ?
+                    index : PlacesUtils.bookmarks.DEFAULT_INDEX;
+        let itemId;
+        if (item.itemType == bms.TYPE_SEPARATOR) {
+          itemId = bms.insertSeparator(parentId, index, item.GUID);
+        }
+        else if (item.itemType == bms.TYPE_BOOKMARK) {
+          itemId = bms.insertBookmark(parentId, item.uri, index, item.title,
+                                       item.GUID);
+        }
+        else { // folder
+          itemId = bms.createFolder(parentId, item.title, index, item.GUID);
+        }
+
+        if (item.itemType == bms.TYPE_BOOKMARK) {
+          if (item.keyword)
+            bms.setKeywordForBookmark(itemId, item.keyword);
+          if ("tags" in item)
+            PlacesUtils.tagging.tagURI(item.uri, item.tags);
+        }
+        else if (item.readOnly === true) {
+          bms.setFolderReadonly(itemId, true);
+        }
+
+        PlacesUtils.setAnnotationsForItem(itemId, item.annotations);
+        PlacesUtils.bookmarks.setItemDateAdded(itemId, item.dateAdded);
+        PlacesUtils.bookmarks.setItemLastModified(itemId, item.lastModified);
+      }
+    };
+  }
+};
+
+/**
+ * Transaction for tagging a URI.
+ *
+ * Required Input Properties: uri, tags.
+ */
+PT.TagURI = DefineTransaction(["uri", "tags"]);
+PT.TagURI.prototype = {
+  execute: function* (aURI, aTags) {
+    if (PlacesUtils.getMostRecentBookmarkForURI(aURI) == -1) {
+      // Tagging is only allowed for bookmarked URIs.
+      let unfileGUID =
+        yield PlacesUtils.promiseItemGUID(PlacesUtils.unfiledBookmarksFolderId);
+      let createTxn = PT.NewBookmark({ uri: aURI
+                                     , tags: aTags
+                                     , parentGUID: unfileGUID });
+      yield createTxn.execute();
+      this.undo = createTxn.undo.bind(createTxn);
+      this.redo = createTxn.redo.bind(createTxn);
+    }
+    else {
+      let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
+      let newTags = [t for (t of aTags) if (currentTags.indexOf(t) == -1)];
+      PlacesUtils.tagging.tagURI(aURI, newTags);
+      this.undo = () => { PlacesUtils.tagging.untagURI(aURI, newTags); };
+      this.redo = () => { PlacesUtils.tagging.tagURI(aURI, newTags); };
+    }
+  }
+};
+
+/**
+ * Transaction for removing tags from a URI.
+ *
+ * Required Input Properties: uri.
+ * Optional Input Properties: tags.
+ *
+ * If |tags| is not set, all tags set for |uri| are removed.
+ */
+PT.UntagURI = DefineTransaction(["uri"], ["tags"]);
+PT.UntagURI.prototype = {
+  execute: function* (aURI, aTags) {
+    let tagsSet = PlacesUtils.tagging.getTagsForURI(aURI);
+
+    if (aTags && aTags.length > 0)
+      aTags = [t for (t of aTags) if (tagsSet.indexOf(t) != -1)];
+    else
+      aTags = tagsSet;
+
+    PlacesUtils.tagging.untagURI(aURI, aTags);
+    this.undo = () => { PlacesUtils.tagging.tagURI(aURI, aTags); };
+    this.redo = () => { PlacesUtils.tagging.untagURI(aURI, aTags); };
+  }
+};
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1518,17 +1518,41 @@ this.PlacesUtils = {
                              dataLen: aDataLen,
                              data: aData,
                              mimeType: aMimeType });
         } else {
           deferred.reject();
         }
       });
     return deferred.promise;
-  }
+  },
+
+  /**
+   * Get the unique id for an item (a bookmark, a folder or a separator) given
+   * its item id.
+   *
+   * @param aItemId
+   *        an item id
+   * @return {Promise}
+   * @resolves to the GUID.
+   * @rejects if aItemId is invalid.
+   */
+  promiseItemGUID: function (aItemId) GUIDHelper.getItemGUID(aItemId),
+
+  /**
+   * Get the item id for an item (a bookmark, a folder or a separator) given
+   * its unique id.
+   *
+   * @param aGUID
+   *        an item GUID
+   * @retrun {Promise}
+   * @resolves to the GUID.
+   * @rejects if there's no item for the given GUID.
+   */
+  promiseItemId: function (aGUID) GUIDHelper.getItemId(aGUID)
 };
 
 /**
  * Wraps the provided statement so that invoking cancel() on the pending
  * statement object will always cause a REASON_CANCELED.
  */
 function AsyncStatementCancelWrapper(aStmt) {
   this._stmt = aStmt;
@@ -1635,16 +1659,148 @@ XPCOMUtils.defineLazyGetter(this, "bundl
          getService(Ci.nsIStringBundleService).
          createBundle(PLACES_STRING_BUNDLE_URI);
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "focusManager",
                                    "@mozilla.org/focus-manager;1",
                                    "nsIFocusManager");
 
+// Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
+// itemIds will be deprecated in favour of GUIDs, which play much better
+// with multiple undo/redo operations.  Because these GUIDs are already stored,
+// and because we don't want to revise the transactions API once more when this
+// happens, transactions are set to work with GUIDs exclusively, in the sense
+// that they may never expose itemIds, nor do they accept them as input.
+// More importantly, transactions which add or remove items guarantee to
+// restore the guids on undo/redo, so that the following transactions that may
+// done or undo can assume the items they're interested in are stil accessible
+// through the same GUID.
+// The current bookmarks API, however, doesn't expose the necessary means for
+// working with GUIDs.  So, until it does, this helper object accesses the
+// Places database directly in order to switch between GUIDs and itemIds, and
+// "restore" GUIDs on items re-created items.
+const REASON_FINISHED = Ci.mozIStorageStatementCallback.REASON_FINISHED;
+let GUIDHelper = {
+  // Cache for guid<->itemId paris.
+  GUIDsForIds: new Map(),
+  idsForGUIDs: new Map(),
+
+  getItemId: function (aGUID) {
+    if (this.idsForGUIDs.has(aGUID))
+      return Promise.resolve(this.idsForGUIDs.get(aGUID));
+
+    let deferred = Promise.defer();
+    let itemId = -1;
+
+    this._getIDStatement.params.guid = aGUID;
+    this._getIDStatement.executeAsync({
+      handleResult: function (aResultSet) {
+        let row = aResultSet.getNextRow();
+        if (row)
+          itemId = row.getResultByIndex(0);
+      },
+      handleCompletion: function (aReason) {
+        if (aReason == REASON_FINISHED && itemId != -1) {
+          deferred.resolve(itemId);
+
+          this.ensureObservingRemovedItems();
+          this.idsForGUIDs.set(aGUID, itemId);
+        }
+        else if (itemId != -1) {
+          deferred.reject("no item found for the given guid");
+        }
+        else {
+          deferred.reject("SQLite Error: " + aReason);
+        }
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  getItemGUID: function (aItemId) {
+    if (this.GUIDsForIds.has(aItemId))
+      return Promise.resolve(this.GUIDsForIds.has(aItemId));
+
+    let deferred = Promise.defer();
+    let guid = "";
+
+    this._getGUIDStatement.params.id = aItemId;
+    this._getGUIDStatement.executeAsync({
+      handleResult: function (aResultSet) {
+        let row = aResultSet.getNextRow();
+        if (row) {
+          guid = row.getResultByIndex(1);
+        }
+      },
+      handleCompletion: function (aReason) {
+        if (aReason == REASON_FINISHED && guid) {
+          deferred.resolve(guid);
+
+          this.ensureObservingRemovedItems();
+          this.GUIDsForIds.set(aItemId, guid);
+        }
+        else if (!guid) {
+          deferred.reject("no item found for the given itemId");
+        }
+        else {
+          deferred.reject("SQLite Error: " + aReason);
+        }
+      }
+    });
+
+    return deferred.promise;
+  },
+
+  ensureObservingRemovedItems: function () {
+    if (!("observer" in this)) {
+      /**
+       * This observers serves two purposes:
+       * (1) Invalidate cached id<->guid paris on when items are removed.
+       * (2) Cache GUIDs given us free of charge by onItemAdded/onItemRemoved.
+      *      So, for exmaple, when the NewBookmark needs the new GUID, we already
+      *      have it cached.
+      */
+      this.observer = {
+        onItemAdded: (aItemId, aParentId, aIndex, aItemType, aURI, aTitle,
+                      aDateAdded, aGUID, aParentGUID) => {
+          this.GUIDsForIds.set(aItemId, aGUID);
+          this.GUIDsForIds.set(aParentId, aParentGUID);
+        },
+        onItemRemoved:
+        (aItemId, aParentId, aIndex, aItemTyep, aURI, aGUID, aParentGUID) => {
+          this.GUIDsForIds.delete(aItemId);
+          this.idsForGUIDs.delete(aGUID);
+          this.GUIDsForIds.set(aParentId, aParentGUID);
+        },
+
+        QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
+        __noSuchMethod__: () => {}, // Catch all all onItem* methods.
+      };
+      PlacesUtils.bookmarks.addObserver(this.observer, false);
+      PlacesUtils.registerShutdownFunction(() => {
+        PlacesUtils.bookmarks.removeObserver(this.observer);
+      });
+    }
+  }
+};
+XPCOMUtils.defineLazyGetter(GUIDHelper, "_getIDStatement", () => {
+  let s = PlacesUtils.history.DBConnection.createAsyncStatement(
+    "SELECT b.id, b.guid from moz_bookmarks b WHERE b.guid = :guid");
+  PlacesUtils.registerShutdownFunction( () => s.finalize() );
+  return s;
+});
+XPCOMUtils.defineLazyGetter(GUIDHelper, "_getGUIDStatement", () => {
+  let s = PlacesUtils.history.DBConnection.createAsyncStatement(
+    "SELECT b.id, b.guid from moz_bookmarks b WHERE b.id = :id");
+  PlacesUtils.registerShutdownFunction( () => s.finalize() );
+  return s;
+});
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Transactions handlers.
 
 /**
  * Updates commands in the undo group of the active window commands.
  * Inactive windows commands will be updated on focus.
  */
 function updateCommandsOnActiveWindow()
@@ -2034,39 +2190,33 @@ PlacesCreateLivemarkTransaction.prototyp
   doTransaction: function CLTXN_doTransaction()
   {
     PlacesUtils.livemarks.addLivemark(
       { title: this.item.title
       , feedURI: this.item.feedURI
       , parentId: this.item.parentId
       , index: this.item.index
       , siteURI: this.item.siteURI
-      },
-      (function(aStatus, aLivemark) {
-        if (Components.isSuccessCode(aStatus)) {
-          this.item.id = aLivemark.id;
-          if (this.item.annotations && this.item.annotations.length > 0) {
-            PlacesUtils.setAnnotationsForItem(this.item.id,
-                                              this.item.annotations);
-          }
+      }).then(aLivemark => {
+        this.item.id = aLivemark.id;
+        if (this.item.annotations && this.item.annotations.length > 0) {
+          PlacesUtils.setAnnotationsForItem(this.item.id,
+                                            this.item.annotations);
         }
-      }).bind(this)
-    );
+      }, Cu.reportError);
   },
 
   undoTransaction: function CLTXN_undoTransaction()
   {
     // The getLivemark callback is expected to receive a failure status but it
     // is used just to serialize, so doesn't matter.
-    PlacesUtils.livemarks.getLivemark(
-      { id: this.item.id },
-      (function (aStatus, aLivemark) {
+    PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+      .then(null, () => {
         PlacesUtils.bookmarks.removeItem(this.item.id);
-      }).bind(this)
-    );
+      });
   }
 };
 
 
 /**
  * Transaction for removing a livemark item.
  *
  * @param aLivemarkId
@@ -2094,55 +2244,45 @@ function PlacesRemoveLivemarkTransaction
     PlacesUtils.bookmarks.getItemLastModified(this.item.id);
 }
 
 PlacesRemoveLivemarkTransaction.prototype = {
   __proto__: BaseTransaction.prototype,
 
   doTransaction: function RLTXN_doTransaction()
   {
-    PlacesUtils.livemarks.getLivemark(
-      { id: this.item.id },
-      (function (aStatus, aLivemark) {
-        if (Components.isSuccessCode(aStatus)) {
-          this.item.feedURI = aLivemark.feedURI;
-          this.item.siteURI = aLivemark.siteURI;
-
-          PlacesUtils.bookmarks.removeItem(this.item.id);
-        }
-      }).bind(this)
-    );
+    PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+      .then(aLivemark => {
+        this.item.feedURI = aLivemark.feedURI;
+        this.item.siteURI = aLivemark.siteURI;
+        PlacesUtils.bookmarks.removeItem(this.item.id);
+      }, Cu.reportError);
   },
 
   undoTransaction: function RLTXN_undoTransaction()
   {
     // Undo work must be serialized, otherwise won't be able to know the
     // feedURI and siteURI of the livemark.
     // The getLivemark callback is expected to receive a failure status but it
     // is used just to serialize, so doesn't matter.
-    PlacesUtils.livemarks.getLivemark(
-      { id: this.item.id },
-      (function () {
-        let addLivemarkCallback = (function(aStatus, aLivemark) {
-          if (Components.isSuccessCode(aStatus)) {
-            let itemId = aLivemark.id;
-            PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
-            PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
-          }
-        }).bind(this);
+    PlacesUtils.livemarks.getLivemark({ id: this.item.id })
+      .then(null, () => {
         PlacesUtils.livemarks.addLivemark({ parentId: this.item.parentId
                                           , title: this.item.title
                                           , siteURI: this.item.siteURI
                                           , feedURI: this.item.feedURI
                                           , index: this.item.index
                                           , lastModified: this.item.lastModified
-                                          },
-                                          addLivemarkCallback);
-      }).bind(this)
-    );
+                                          }).then(
+          aLivemark => {
+            let itemId = aLivemark.id;
+            PlacesUtils.bookmarks.setItemDateAdded(itemId, this.item.dateAdded);
+            PlacesUtils.setAnnotationsForItem(itemId, this.item.annotations);
+          }, Cu.reportError);
+      });
   }
 };
 
 
 /**
  * Transaction for moving an Item.
  *
  * @param aItemId
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -62,16 +62,17 @@ if CONFIG['MOZ_PLACES']:
     EXTRA_JS_MODULES = [
         'BookmarkHTMLUtils.jsm',
         'BookmarkJSONUtils.jsm',
         'ClusterLib.js',
         'ColorAnalyzer_worker.js',
         'ColorConversion.js',
         'PlacesBackups.jsm',
         'PlacesDBUtils.jsm',
+        'PlacesTransactions.jsm',
     ]
 
     EXTRA_PP_JS_MODULES += [
         'PlacesUtils.jsm',
     ]
 
     EXTRA_COMPONENTS += [
         'ColorAnalyzer.js',
--- a/toolkit/components/places/mozIAsyncLivemarks.idl
+++ b/toolkit/components/places/mozIAsyncLivemarks.idl
@@ -22,32 +22,38 @@ interface mozIAsyncLivemarks : nsISuppor
    *        mozILivemarkInfo object containing at least title, parentId,
    *        index and feedURI of the livemark to create.
    * @param [optional] aCallback
    *        Invoked when the creation process is done.  In case of failure will
    *        receive an error code.
    * @return {Promise}
    * @throws NS_ERROR_INVALID_ARG if the supplied information is insufficient
    *         for the creation.
+   * @deprecated passing a callback is deprecated. Moreover, for backwards
+   *             compatibility reasons, when a callback is provided this method
+   *             won't return a promise.
    */
   jsval addLivemark(in jsval aLivemarkInfo,
                     [optional] in mozILivemarkCallback aCallback);
 
   /**
    * Removes an existing livemark.
    *
    * @param aLivemarkInfo
    *        mozILivemarkInfo object containing either an id or a guid of the
    *        livemark to remove.
    * @param [optional] aCallback
    *        Invoked when the removal process is done.  In case of failure will
    *        receive an error code.
    *
    * @return {Promise}
    * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid.
+   * @deprecated passing a callback is deprecated. Moreover, for backwards
+   *             compatibility reasons, when a callback is provided this method
+   *             won't return a promise.
    */
   jsval removeLivemark(in jsval aLivemarkInfo,
                        [optional] in mozILivemarkCallback aCallback);
 
   /**
    * Gets an existing livemark.
    *
    * @param aLivemarkInfo
@@ -55,16 +61,19 @@ interface mozIAsyncLivemarks : nsISuppor
    *        livemark to retrieve.
    * @param [optional] aCallback
    *        Invoked when the fetching process is done.  In case of failure will
    *        receive an error code.
    *
    * @return {Promise}
    * @throws NS_ERROR_INVALID_ARG if the id/guid is invalid or an invalid
    *         callback is provided.
+   * @deprecated passing a callback is deprecated. Moreover, for backwards
+   *             compatibility reasons, when a callback is provided this method
+   *             won't return a promise.
    */
   jsval getLivemark(in jsval aLivemarkInfo,
                     [optional] in mozILivemarkCallback aCallback);
 
   /**
    * Reloads all livemarks if they are expired or if forced to do so.
    *
    * @param [optional]aForceUpdate
--- a/toolkit/components/places/nsLivemarkService.js
+++ b/toolkit/components/places/nsLivemarkService.js
@@ -13,16 +13,18 @@ const Cu = Components.utils;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
+                                  "resource://gre/modules/Deprecated.jsm");
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Services
 
 XPCOMUtils.defineLazyServiceGetter(this, "secMan",
                                    "@mozilla.org/scriptsecuritymanager;1",
                                    "nsIScriptSecurityManager");
 XPCOMUtils.defineLazyGetter(this, "asyncHistory", function () {
@@ -121,19 +123,19 @@ LivemarkService.prototype = {
       },
       handleCompletion: function LS_handleCompletion() {
         livemarkSvc._pendingStmt = null;
       }
     });
     stmt.finalize();
   },
 
-  _onCacheReady: function LS__onCacheReady(aCallback, aWaitForAsyncWrites)
+  _onCacheReady: function LS__onCacheReady(aCallback)
   {
-    if (this._pendingStmt || aWaitForAsyncWrites) {
+    if (this._pendingStmt) {
       // The cache is still being populated, so enqueue the job to the Storage
       // async thread.  Ideally this should just dispatch a runnable to it,
       // that would call back on the main thread, but bug 608142 made that
       // impossible.  Thus just enqueue the cheapest query possible.
       let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
                                   .DBConnection;
       let stmt = db.createAsyncStatement("PRAGMA encoding");
       stmt.executeAsync({
@@ -207,16 +209,22 @@ LivemarkService.prototype = {
         ("parentId" in aLivemarkInfo && aLivemarkInfo.parentId < 1) ||
         !("index" in aLivemarkInfo) || aLivemarkInfo.index < Ci.nsINavBookmarksService.DEFAULT_INDEX ||
         !(aLivemarkInfo.feedURI instanceof Ci.nsIURI) ||
         (aLivemarkInfo.siteURI && !(aLivemarkInfo.siteURI instanceof Ci.nsIURI)) ||
         (aLivemarkInfo.guid && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid))) {
       throw Cr.NS_ERROR_INVALID_ARG;
     }
 
+    if (aLivemarkCallback) {
+      Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
+                         "Please use the returned promise instead.",
+                         "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
+    }
+
     // The addition is done synchronously due to the fact importExport service
     // and JSON backups require that.  The notification is async though.
     // Once bookmarks are async, this may be properly fixed.
     let deferred = Promise.defer();
     let addLivemarkEx = null;
     let livemark = null;
     try {
       // Disallow adding a livemark inside another livemark.
@@ -230,73 +238,79 @@ LivemarkService.prototype = {
                               , index:        aLivemarkInfo.index
                               , feedURI:      aLivemarkInfo.feedURI
                               , siteURI:      aLivemarkInfo.siteURI
                               , guid:         aLivemarkInfo.guid
                               , lastModified: aLivemarkInfo.lastModified
                               });
       if (this._itemAdded && this._itemAdded.id == livemark.id) {
         livemark.index = this._itemAdded.index;
-        if (!aLivemarkInfo.guid) {
-          livemark.guid = this._itemAdded.guid;
-        }
+        livemark.guid = this._itemAdded.guid;
         if (!aLivemarkInfo.lastModified) {
           livemark.lastModified = this._itemAdded.lastModified;
         }
       }
 
       // Updating the cache even if it has not yet been populated doesn't
       // matter since it will just be overwritten.
       this._livemarks[livemark.id] = livemark;
-      this._guids[aLivemarkInfo.guid] = livemark.id;
+      this._guids[livemark.guid] = livemark.id;
     }
     catch (ex) {
       addLivemarkEx = ex;
       livemark = null;
     }
     finally {
       this._onCacheReady( () => {
         if (addLivemarkEx) {
           if (aLivemarkCallback) {
             try {
               aLivemarkCallback.onCompletion(addLivemarkEx.result, livemark);
             }
             catch(ex2) { }
+          } else {
+            deferred.reject(addLivemarkEx);
           }
-          deferred.reject(addLivemarkEx);
         }
         else {
           if (aLivemarkCallback) {
             try {
               aLivemarkCallback.onCompletion(Cr.NS_OK, livemark);
             }
             catch(ex2) { }
+          } else {
+            deferred.resolve(livemark);
           }
-          deferred.resolve(livemark);
         }
-      }, true);
+      });
     }
 
-    return deferred.promise;
+    return aLivemarkCallback ? null : deferred.promise;
   },
 
   removeLivemark: function LS_removeLivemark(aLivemarkInfo, aLivemarkCallback)
   {
     if (!aLivemarkInfo) {
       throw Cr.NS_ERROR_INVALID_ARG;
     }
 
     // Accept either a guid or an id.
     let id = aLivemarkInfo.guid || aLivemarkInfo.id;
     if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
         ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
         !id) {
       throw Cr.NS_ERROR_INVALID_ARG;
     }
 
+    if (aLivemarkCallback) {
+      Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
+                         "Please use the returned promise instead.",
+                         "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
+    }
+
     // Convert the guid to an id.
     if (id in this._guids) {
       id = this._guids[id];
     }
 
     let deferred = Promise.defer();
     let removeLivemarkEx = null;
     try {
@@ -311,32 +325,34 @@ LivemarkService.prototype = {
     finally {
       this._onCacheReady( () => {
         if (removeLivemarkEx) {
           if (aLivemarkCallback) {
             try {
               aLivemarkCallback.onCompletion(removeLivemarkEx.result, null);
             }
             catch(ex2) { }
+          } else {
+            deferred.reject(removeLivemarkEx);
           }
-          deferred.reject(removeLivemarkEx);
         }
         else {
           if (aLivemarkCallback) {
             try {
               aLivemarkCallback.onCompletion(Cr.NS_OK, null);
             }
             catch(ex2) { }
+          } else {
+            deferred.resolve();
           }
-          deferred.resolve();
         }
       });
     }
 
-    return deferred.promise;
+    return aLivemarkCallback ? null : deferred.promise;
   },
 
   _reloaded: [],
   _reloadNextLivemark: function LS__reloadNextLivemark()
   {
     this._reloading = false;
     // Find first livemark to be reloaded.
     for (let id in this._livemarks) {
@@ -377,41 +393,49 @@ LivemarkService.prototype = {
     // Accept either a guid or an id.
     let id = aLivemarkInfo.guid || aLivemarkInfo.id;
     if (("guid" in aLivemarkInfo && !/^[a-zA-Z0-9\-_]{12}$/.test(aLivemarkInfo.guid)) ||
         ("id" in aLivemarkInfo && aLivemarkInfo.id < 1) ||
         !id) {
       throw Cr.NS_ERROR_INVALID_ARG;
     }
 
+    if (aLivemarkCallback) {
+      Deprecated.warning("Passing a callback to Livermarks methods is deprecated. " +
+                         "Please use the returned promise instead.",
+                         "https://developer.mozilla.org/docs/Mozilla/JavaScript_code_modules/Promise.jsm");
+    }
+
     let deferred = Promise.defer();
     this._onCacheReady( () => {
       // Convert the guid to an id.
       if (id in this._guids) {
         id = this._guids[id];
       }
       if (id in this._livemarks) {
         if (aLivemarkCallback) {
           try {
             aLivemarkCallback.onCompletion(Cr.NS_OK, this._livemarks[id]);
           } catch (ex) {}
+        } else {
+          deferred.resolve(this._livemarks[id]);
         }
-        deferred.resolve(this._livemarks[id]);
       }
       else {
         if (aLivemarkCallback) {
           try {
             aLivemarkCallback.onCompletion(Cr.NS_ERROR_INVALID_ARG, null);
           } catch (ex) { }
+        } else {
+          deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG));
         }
-        deferred.reject(Components.Exception("", Cr.NS_ERROR_INVALID_ARG));
       }
     });
 
-    return deferred.promise;
+    return aLivemarkCallback ? null : deferred.promise;
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsINavBookmarkObserver
 
   onBeginUpdateBatch:  function () {},
   onEndUpdateBatch:    function () {},
   onItemVisited:       function () {},
@@ -553,21 +577,19 @@ function Livemark(aLivemarkInfo)
     this.feedURI = aLivemarkInfo.feedURI;
     this.siteURI = aLivemarkInfo.siteURI;
     this.lastModified = aLivemarkInfo.lastModified;
   }
   else {
     // Create a new livemark.
     this.id = PlacesUtils.bookmarks.createFolder(aLivemarkInfo.parentId,
                                                  aLivemarkInfo.title,
-                                                 aLivemarkInfo.index);
+                                                 aLivemarkInfo.index,
+                                                 aLivemarkInfo.guid);
     PlacesUtils.bookmarks.setFolderReadonly(this.id, true);
-    if (aLivemarkInfo.guid) {
-      this.writeGuid(aLivemarkInfo.guid);
-    }
     this.writeFeedURI(aLivemarkInfo.feedURI);
     if (aLivemarkInfo.siteURI) {
       this.writeSiteURI(aLivemarkInfo.siteURI);
     }
     // Last modified time must be the last change.
     if (aLivemarkInfo.lastModified) {
       this.lastModified = aLivemarkInfo.lastModified;
       PlacesUtils.bookmarks.setItemLastModified(this.id, this.lastModified);
@@ -625,41 +647,16 @@ Livemark.prototype = {
     catch (ex) {
       return;
     }
 
     this._setAnno(PlacesUtils.LMANNO_SITEURI, aSiteURI.spec)
     this.siteURI = aSiteURI;
   },
 
-  writeGuid: function LM_writeGuid(aGUID)
-  {
-    // There isn't a way to create a bookmark with a given guid yet, nor to
-    // set a guid on an existing one.  So, for now, just go the dirty way.
-    let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
-                                .DBConnection;
-    let stmt = db.createAsyncStatement("UPDATE moz_bookmarks " +
-                                       "SET guid = :guid " +
-                                       "WHERE id = :item_id");
-    stmt.params.guid = aGUID;
-    stmt.params.item_id = this.id;
-    let livemark = this;
-    stmt.executeAsync({
-      handleError: function () {},
-      handleResult: function () {},
-      handleCompletion: function ETAT_handleCompletion(aReason)
-      {
-        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
-          livemark._guid = aGUID;
-        }
-      }
-    });
-    stmt.finalize();
-  },
-
   set guid(aGUID) {
     this._guid = aGUID;
     return aGUID;
   },
   get guid() this._guid,
 
   set lastModified(aLastModified) {
     this._lastModified = aLastModified;
--- a/toolkit/components/places/tests/chrome/test_303567.xul
+++ b/toolkit/components/places/tests/chrome/test_303567.xul
@@ -50,20 +50,18 @@ function runTest()
 {
   function testLivemark(aLivemarkData) {
     PlacesUtils.livemarks.addLivemark(
       { title: "foo"
       , parentId: PlacesUtils.toolbarFolderId
       , index: PlacesUtils.bookmarks.DEFAULT_INDEX
       , feedURI: aLivemarkData.feedURI
       , siteURI: aLivemarkData.siteURI
-      },
-      function (aStatus, aLivemark) {
-        ok(Components.isSuccessCode(aStatus), "Get livemark");
-
+      })
+      .then(function (aLivemark) {
         is (aLivemark.feedURI.spec, aLivemarkData.feedURI.spec,
             "Get correct feedURI");
         if (aLivemarkData.siteURI) {
           is (aLivemark.siteURI.spec, aLivemarkData.siteURI.spec,
               "Get correct siteURI");
         }
         else {
           is (aLivemark.siteURI, null, "Get correct siteURI");
@@ -79,18 +77,20 @@ function runTest()
           });
 
           PlacesUtils.bookmarks.removeItem(aLivemark.id);
 
           if (aLivemark.feedURI.equals(LIVEMARKS[LIVEMARKS.length - 1].feedURI)) {
             SimpleTest.finish();
           }          
         });
+      }, function () {
+        is(true, false, "Should not fail adding a livemark");
       }
-    )
+    );
   }
 
   LIVEMARKS.forEach(testLivemark);
 }
 
 function waitForLivemarkLoad(aLivemark, aCallback) {
   // Don't need a real node here.
   let node = {};
--- a/toolkit/components/places/tests/chrome/test_341972a.xul
+++ b/toolkit/components/places/tests/chrome/test_341972a.xul
@@ -32,29 +32,30 @@ function runTest() {
   const FEEDSITESPEC = "http://example.org/"; 
 
   PlacesUtils.livemarks.addLivemark(
     { title: "foo"
     , parentId: PlacesUtils.toolbarFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: NetUtil.newURI(FEEDSPEC)
     , siteURI: NetUtil.newURI(INITIALSITESPEC)
-    },
-    function (aStatus, aLivemark) {
-      ok(Components.isSuccessCode(aStatus), "Get livemark");
+    })
+    .then(function (aLivemark) {
       is(aLivemark.siteURI.spec, INITIALSITESPEC,
          "Has correct initial livemark site URI");
 
       waitForLivemarkLoad(aLivemark, function (aLivemark) {
         is(aLivemark.siteURI.spec, FEEDSITESPEC,
            "livemark site URI set to value in feed");
 
         PlacesUtils.bookmarks.removeItem(aLivemark.id);
         SimpleTest.finish();
       });
+    }, function () {
+      is(true, false, "Should not fail adding a livemark");
     }
   );
 }
 
 function waitForLivemarkLoad(aLivemark, aCallback) {
   // Don't need a real node here.
   let node = {};
   let resultObserver = {
--- a/toolkit/components/places/tests/chrome/test_341972b.xul
+++ b/toolkit/components/places/tests/chrome/test_341972b.xul
@@ -30,28 +30,29 @@ function runTest() {
   const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/sample_feed.atom";
   const FEEDSITESPEC = "http://example.org/"; 
 
   PlacesUtils.livemarks.addLivemark(
     { title: "foo"
     , parentId: PlacesUtils.toolbarFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: NetUtil.newURI(FEEDSPEC)
-    },
-    function (aStatus, aLivemark) {
-      ok(Components.isSuccessCode(aStatus), "Get livemark");
+    })
+    .then(function (aLivemark) {
       is(aLivemark.siteURI, null, "Has null livemark site URI");
 
       waitForLivemarkLoad(aLivemark, function (aLivemark) {
         is(aLivemark.siteURI.spec, FEEDSITESPEC,
            "livemark site URI set to value in feed");
 
         PlacesUtils.bookmarks.removeItem(aLivemark.id);
         SimpleTest.finish();
       });
+    }, function () {
+      is(true, false, "Should not fail adding a livemark");
     }
   );
 }
 
 function waitForLivemarkLoad(aLivemark, aCallback) {
   // Don't need a real node here.
   let node = {};
   let resultObserver = {
--- a/toolkit/components/places/tests/chrome/test_342484.xul
+++ b/toolkit/components/places/tests/chrome/test_342484.xul
@@ -31,32 +31,32 @@ function runTest() {
   const GOOD_URLS = ["http://example.org/first", "http://example.org/last"];
 
   PlacesUtils.livemarks.addLivemark(
     { title: "foo"
     , parentId: PlacesUtils.toolbarFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: NetUtil.newURI(FEEDSPEC)
     , siteURI: NetUtil.newURI("http:/mochi.test/")
-    },
-    function (aStatus, aLivemark) {
-      ok(Components.isSuccessCode(aStatus), "Get livemark");
-
+    })
+    .then(function (aLivemark) {
       waitForLivemarkLoad(aLivemark, function (aLivemark) {
         let nodes = aLivemark.getNodesForContainer({});
 
         is(nodes.length, 2, "Created the two good livemark items");
         for (let i = 0; i < nodes.length; ++i) {
           let node = nodes[i];
           ok(GOOD_URLS.indexOf(node.uri) != -1, "livemark item created with bad uri " + node.uri);
         }
 
         PlacesUtils.bookmarks.removeItem(aLivemark.id);
         SimpleTest.finish();
       });
+    }, function () {
+      is(true, false, "Should not fail adding a livemark");
     }
   );
 }
 
 function waitForLivemarkLoad(aLivemark, aCallback) {
   // Don't need a real node here.
   let node = {};
   let resultObserver = {
--- a/toolkit/components/places/tests/chrome/test_381357.xul
+++ b/toolkit/components/places/tests/chrome/test_381357.xul
@@ -30,30 +30,30 @@ function runTest() {
   const FEEDSPEC = "http://mochi.test:8888/tests/toolkit/components/places/tests/chrome/rss_as_html.rss";
 
   PlacesUtils.livemarks.addLivemark(
     { title: "foo"
     , parentId: PlacesUtils.toolbarFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: NetUtil.newURI(FEEDSPEC)
     , siteURI: NetUtil.newURI("http:/mochi.test/")
-    },
-    function (aStatus, aLivemark) {
-      ok(Components.isSuccessCode(aStatus), "Get livemark");
-
+    })
+    .then(function (aLivemark) {
       waitForLivemarkLoad(aLivemark, function (aLivemark) {
         let nodes = aLivemark.getNodesForContainer({});
-        ok(Components.isSuccessCode(aStatus), "Get livemark entries");
 
         is(nodes[0].title, "The First Title",
            "livemark site URI set to value in feed");
 
         PlacesUtils.bookmarks.removeItem(aLivemark.id);
         SimpleTest.finish();
       });
+    }, function () {
+      is(true, false, "Should not fail adding a livemark");
+      SimpleTest.finish();
     }
   );
 }
 
 function waitForLivemarkLoad(aLivemark, aCallback) {
   // Don't need a real node here.
   let node = {};
   let resultObserver = {
--- a/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul
+++ b/toolkit/components/places/tests/chrome/test_reloadLivemarks.xul
@@ -52,58 +52,68 @@ function runTest()
     PlacesUtils.livemarks.reloadLivemarks();
   });
 }
 
 function addLivemarks(aCallback) {
   info("Adding livemarks");
   let count = gLivemarks.length;
   gLivemarks.forEach(function(aLivemarkData) {
-    PlacesUtils.livemarks.addLivemark(aLivemarkData,
-      function (aStatus, aLivemark) {
-        ok(Components.isSuccessCode(aStatus), "Add livemark should succeed");
+    PlacesUtils.livemarks.addLivemark(aLivemarkData)
+      .then(function (aLivemark) {
+        ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark added");
         aLivemarkData.id = aLivemark.id;
         if (--count == 0) {
           aCallback();
         }
-      }
-    );
+      },
+      function () {
+        is(true, false, "Should not fail adding a livemark.");
+        aCallback();
+      });
   });
 }
 
 function reloadLivemarks(aForceUpdate, aCallback) {
   info("Reloading livemarks with forceUpdate: " + aForceUpdate);
   let count = gLivemarks.length;
   gLivemarks.forEach(function(aLivemarkData) {
-    PlacesUtils.livemarks.getLivemark(aLivemarkData,
-      function (aStatus, aLivemark) {
-        ok(Components.isSuccessCode(aStatus), "Get livemark should succeed");
+    PlacesUtils.livemarks.getLivemark(aLivemarkData)
+      .then(aLivemark => {
+        ok(aLivemark.feedURI.equals(aLivemarkData.feedURI), "Livemark found");
         aLivemarkData._observer = new resultObserver(aLivemark, function() {
           if (++count == gLivemarks.length) {
             aCallback();
           }
         });
         if (--count == 0) {
           PlacesUtils.livemarks.reloadLivemarks(aForceUpdate);
         }
+      },
+      function() {
+        is(true, false, "Should not fail getting a livemark.");
+        aCallback();
       }
     );
   });
 }
 
 function removeLivemarks(aCallback) {
   info("Removing livemarks");
   let count = gLivemarks.length;
   gLivemarks.forEach(function(aLivemarkData) {
-    PlacesUtils.livemarks.removeLivemark(aLivemarkData,
-      function (aStatus, aLivemark) {
-        ok(Components.isSuccessCode(aStatus), "Remove livemark should succeed");
+    PlacesUtils.livemarks.removeLivemark(aLivemarkData).then(
+      function (aLivemark) {
         if (--count == 0) {
           aCallback();
         }
+      },
+      function() {
+        is(true, false, "Should not fail adding a livemark.");
+        aCallback();
       }
     );
   });
 }
 
 function resultObserver(aLivemark, aCallback) {
   this._node = {};
   this._livemark = aLivemark;
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -31,16 +31,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
                                   "resource://gre/modules/BookmarkJSONUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
+                                  "resource://gre/modules/PlacesTransactions.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 
 // This imports various other objects in addition to PlacesUtils.
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
   return NetUtil.newURI(
--- a/toolkit/components/places/tests/queries/head_queries.js
+++ b/toolkit/components/places/tests/queries/head_queries.js
@@ -157,17 +157,17 @@ function task_populateDB(aArray)
           }
 
           if (qdata.isLivemark) {
             PlacesUtils.livemarks.addLivemark({ title: qdata.title
                                               , parentId: qdata.parentFolder
                                               , index: qdata.index
                                               , feedURI: uri(qdata.feedURI)
                                               , siteURI: uri(qdata.uri)
-                                              });
+                                              }).then(null, do_throw);
           }
 
           if (qdata.isBookmark) {
             let itemId = PlacesUtils.bookmarks.insertBookmark(qdata.parentFolder,
                                                               uri(qdata.uri),
                                                               qdata.index,
                                                               qdata.title);
             if (qdata.keyword)
--- a/toolkit/components/places/tests/unit/test_384370.js
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -108,17 +108,17 @@ function populate() {
   for each(let {uri: u, title: t} in bookmarkData) {
     PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.toolbarFolder,
                                          u, PlacesUtils.bookmarks.DEFAULT_INDEX, t);
   }
 }
 
 function validate() {
   yield testCanonicalBookmarks();
-  testToolbarFolder();
+  yield testToolbarFolder();
   testUnfiledBookmarks();
   testTags();
 }
 
 // Tests a bookmarks datastore that has a set of bookmarks, etc
 // that flex each supported field and feature.
 function testCanonicalBookmarks() {
   // query to see if the deleted folder and items have been imported
@@ -212,26 +212,21 @@ function testToolbarFolder() {
   // child count (add 2 for pre-existing items)
   do_check_eq(toolbar.childCount, bookmarkData.length + 2);
   
   // livemark
   var livemark = toolbar.getChild(1);
   // title
   do_check_eq("Latest Headlines", livemark.title);
 
-  PlacesUtils.livemarks.getLivemark(
-    { id: livemark.itemId },
-    function (aStatus, aLivemark) {
-      do_check_true(Components.isSuccessCode(aStatus));
-      do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
-                  aLivemark.siteURI.spec);
-      do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
-                  aLivemark.feedURI.spec);
-    }
-  );
+  let foundLivemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.itemId });
+  do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+              foundLivemark.siteURI.spec);
+  do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+              foundLivemark.feedURI.spec);
 
   // test added bookmark data
   var child = toolbar.getChild(2);
   do_check_eq(child.uri, bookmarkData[0].uri.spec);
   do_check_eq(child.title, bookmarkData[0].title);
   child = toolbar.getChild(3);
   do_check_eq(child.uri, bookmarkData[1].uri.spec);
   do_check_eq(child.title, bookmarkData[1].title);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -0,0 +1,1053 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+const bmsvc   = PlacesUtils.bookmarks;
+const tagssvc = PlacesUtils.tagging;
+const annosvc = PlacesUtils.annotations;
+const PT      = PlacesTransactions;
+
+// Create and add bookmarks observer.
+let observer = {
+  __proto__: NavBookmarkObserver.prototype,
+
+  tagRelatedGUIDs: new Set(),
+
+  reset: function () {
+    this.itemsAdded = new Map();
+    this.itemsRemoved = new Map();
+    this.itemsChanged = new Map();
+    this.itemsMoved = new Map();
+    this.beginUpdateBatch = false;
+    this.endUpdateBatch = false;
+  },
+
+  onBeginUpdateBatch: function () {
+    this.beginUpdateBatch = true;
+  },
+
+  onEndUpdateBatch: function () {
+    this.endUpdateBatch = true;
+  },
+
+  onItemAdded:
+  function (aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+            aGUID, aParentGUID) {
+    // Ignore tag items.
+    if (aParentId == PlacesUtils.tagsFolderId ||
+        (aParentId != PlacesUtils.placesRootId &&
+         bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) {
+      this.tagRelatedGUIDs.add(aGUID);
+      return;
+    }
+
+    this.itemsAdded.set(aGUID, { itemId:         aItemId
+                               , parentGUID:     aParentGUID
+                               , index:          aIndex
+                               , itemType:       aItemType
+                               , title:          aTitle
+                               , uri:            aURI });
+  },
+
+  onItemRemoved:
+  function (aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) {
+    if (this.tagRelatedGUIDs.has(aGUID))
+      return;
+
+    this.itemsRemoved.set(aGUID, { parentGUID: aParentGUID
+                                 , index:      aIndex
+                                 , itemType:   aItemType });
+  },
+
+  onItemChanged:
+  function (aItemId, aProperty, aIsAnnoProperty, aNewValue, aLastModified,
+            aItemType, aParentId, aGUID, aParentGUID) {
+    if (this.tagRelatedGUIDs.has(aGUID))
+      return;
+
+    let changesForGUID = this.itemsChanged.get(aGUID);
+    if (changesForGUID === undefined) {
+      changesForGUID = new Map();
+      this.itemsChanged.set(aGUID, changesForGUID);
+    }
+
+    let newValue = aNewValue;
+    if (aIsAnnoProperty) {
+      if (annosvc.itemHasAnnotation(aItemId, aProperty))
+        newValue = annosvc.getItemAnnotation(aItemId, aProperty);
+      else
+        newValue = null;
+    }
+    let change = { isAnnoProperty: aIsAnnoProperty
+                 , newValue: newValue
+                 , lastModified: aLastModified
+                 , itemType: aItemType };
+    changesForGUID.set(aProperty, change);
+  },
+
+  onItemVisited: () => {},
+
+  onItemMoved:
+  function (aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType,
+            aGUID, aOldParentGUID, aNewParentGUID) {
+    this.itemsMoved.set(aGUID, { oldParentGUID: aOldParentGUID
+                               , oldIndex:      aOldIndex
+                               , newParentGUID: aNewParentGUID
+                               , newIndex:      aNewIndex
+                               , itemType:      aItemType });
+  }
+};
+observer.reset();
+
+// index at which items should begin
+let bmStartIndex = 0;
+
+// get bookmarks root id
+let root = PlacesUtils.bookmarksMenuFolderId;
+
+function run_test() {
+  bmsvc.addObserver(observer, false);
+  do_register_cleanup(function () {
+    bmsvc.removeObserver(observer);
+  });
+
+  run_next_test();
+}
+
+function ensureUndoState(aEntries = [], aUndoPosition = 0) {
+  do_check_eq(PT.length, aEntries.length);
+  do_check_eq(PT.undoPosition, aUndoPosition);
+
+  for (let i = 0; i < aEntries.length; i++) {
+    let testEntry = aEntries[i];
+    let undoEntry = PT.item(i);
+    do_check_eq(testEntry.length, undoEntry.length);
+    for (let j = 0; j < testEntry.length; j++) {
+      do_check_eq(testEntry[j], undoEntry[j]);
+    }
+  }
+}
+
+function ensureItemsAdded(...items) {
+  do_check_eq(observer.itemsAdded.size, items.length);
+  for (let item of items) {
+    do_check_true(observer.itemsAdded.has(item.GUID));
+    let info = observer.itemsAdded.get(item.GUID);
+    do_check_eq(info.parentGUID, item.parentGUID);
+    if ("title" in item)
+      do_check_eq(info.title, item.title);
+    if ("index" in item)
+      do_check_eq(info.index, item.index);
+    if ("itemType" in item)
+      do_check_eq(info.itemType, item.itemType);
+  }
+}
+
+function ensureItemsRemoved(...items) {
+  do_check_eq(observer.itemsRemoved.size, items.length);
+  for (let item of items) {
+    do_check_true(observer.itemsRemoved.has(item.GUID));
+    let info = observer.itemsRemoved.get(item.GUID);
+    do_check_eq(info.parentGUID, item.parentGUID);
+    if ("index" in item)
+      do_check_eq(info.index, item.index);
+  }
+}
+
+function ensureItemsChanged(...items) {
+  for (let item of items) {
+    do_check_true(observer.itemsChanged.has(item.GUID));
+    let changes = observer.itemsChanged.get(item.GUID);
+    do_check_true(changes.has(item.property));
+    let info = changes.get(item.property);
+    do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty));
+    do_check_eq(info.newValue, item.newValue);
+    if ("uri" in item)
+      do_check_true(item.uri.equals(info.uri));
+  }
+}
+
+function ensureAnnotationsSet(aGUID, aAnnos) {
+  do_check_true(observer.itemsChanged.has(aGUID));
+  let changes = observer.itemsChanged.get(aGUID);
+  for (let anno of aAnnos) {
+    do_check_true(changes.has(anno.name));
+    let changeInfo = changes.get(anno.name);
+    do_check_true(changeInfo.isAnnoProperty);
+    do_check_eq(changeInfo.newValue, anno.value);
+  }
+}
+
+function ensureItemsMoved(...items) {
+  do_check_true(observer.itemsMoved.size, items.length);
+  for (let item of items) {
+    do_check_true(observer.itemsMoved.has(item.GUID));
+    let info = observer.itemsMoved.get(item.GUID);
+    do_check_eq(info.oldParentGUID, item.oldParentGUID);
+    do_check_eq(info.oldIndex, item.oldIndex);
+    do_check_eq(info.newParentGUID, item.newParentGUID);
+    do_check_eq(info.newIndex, item.newIndex);
+  }
+}
+
+function ensureTimestampsUpdated(aGUID, aCheckDateAdded = false) {
+  do_check_true(observer.itemsChanged.has(aGUID));
+  let changes = observer.itemsChanged.get(aGUID);
+  if (aCheckDateAdded)
+    do_check_true(changes.has("dateAdded"))
+  do_check_true(changes.has("lastModified"));
+}
+
+function ensureTagsForURI(aURI, aTags) {
+  let tagsSet = tagssvc.getTagsForURI(aURI);
+  do_check_eq(tagsSet.length, aTags.length);
+  do_check_true(aTags.every( t => tagsSet.indexOf(t) != -1 ));
+}
+
+function* createTestFolderInfo(aTitle = "Test Folder") {
+  return { parentGUID: yield PlacesUtils.promiseItemGUID(root)
+         , title: "Test Folder" };
+}
+
+add_task(function* test_new_folder_with_annotation() {
+  const ANNO = { name: "TestAnno", value: "TestValue" };
+  let folder_info = yield createTestFolderInfo();
+  folder_info.index = bmStartIndex;
+  folder_info.annotations = [ANNO];
+  ensureUndoState();
+  let txn = PT.NewFolder(folder_info);
+  folder_info.GUID = yield PT.transact(txn);
+  let ensureDo = function* (aRedo = false) {
+    ensureUndoState([[txn]], 0);
+    yield ensureItemsAdded(folder_info);
+    ensureAnnotationsSet(folder_info.GUID, [ANNO]);
+    if (aRedo)
+      ensureTimestampsUpdated(folder_info.GUID, true);
+    observer.reset();
+  };
+
+  let ensureUndo = () => {
+    ensureUndoState([[txn]], 1);
+    ensureItemsRemoved({ GUID:       folder_info.GUID
+                       , parentGUID: folder_info.parentGUID
+                       , index:      bmStartIndex });
+    observer.reset();
+  };
+
+  yield ensureDo();
+  yield PT.undo();
+  yield ensureUndo();
+  yield PT.redo();
+  yield ensureDo(true);
+  yield PT.undo();
+  ensureUndo();
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_new_bookmark() {
+  let bm_info = { parentGUID: yield PlacesUtils.promiseItemGUID(root)
+                , uri:        NetUtil.newURI("http://test_create_item.com")
+                , index:      bmStartIndex
+                , title:      "Test creating an item" };
+
+  ensureUndoState();
+  let txn = PT.NewBookmark(bm_info);
+  bm_info.GUID = yield PT.transact(txn);
+
+  let ensureDo = function* (aRedo = false) {
+    ensureUndoState([[txn]], 0);
+    yield ensureItemsAdded(bm_info);
+    if (aRedo)
+      ensureTimestampsUpdated(bm_info.GUID, true);
+    observer.reset();
+  };
+  let ensureUndo = () => {
+    ensureUndoState([[txn]], 1);
+    ensureItemsRemoved({ GUID:       bm_info.GUID
+                       , parentGUID: bm_info.parentGUID
+                       , index:      bmStartIndex });
+    observer.reset();
+  };
+
+  yield ensureDo();
+  yield PT.undo();
+  ensureUndo();
+  yield PT.redo(true);
+  yield ensureDo();
+  yield PT.undo();
+  ensureUndo();
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_merge_create_folder_and_item() {
+  let folder_info = yield createTestFolderInfo();
+  let bm_info = { uri: NetUtil.newURI("http://test_create_item_to_folder.com")
+                , title: "Test Bookmark"
+                , index: bmStartIndex };
+
+  let { folderTxn, bkmTxn } =  yield PT.transact( function* () {
+    let folderTxn = PT.NewFolder(folder_info);
+    folder_info.GUID = bm_info.parentGUID = yield folderTxn;
+    let bkmTxn = PT.NewBookmark(bm_info);
+    bm_info.GUID = yield bkmTxn;;
+    return { folderTxn: folderTxn, bkmTxn: bkmTxn};
+  });
+
+  let ensureDo = function* () {
+    ensureUndoState([[bkmTxn, folderTxn]], 0);
+    yield ensureItemsAdded(folder_info, bm_info);
+    observer.reset();
+  };
+
+  let ensureUndo = () => {
+    ensureUndoState([[bkmTxn, folderTxn]], 1);
+    ensureItemsRemoved(folder_info, bm_info);
+    observer.reset();
+  };
+
+  yield ensureDo();
+  yield PT.undo();
+  ensureUndo();
+  yield PT.redo();
+  yield ensureDo();
+  yield PT.undo();
+  ensureUndo();
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_move_items_to_folder() {
+  let folder_a_info = yield createTestFolderInfo("Folder A");
+  let bkm_a_info = { uri: NetUtil.newURI("http://test_move_items.com")
+                   , title: "Bookmark A" };
+  let bkm_b_info = { uri: NetUtil.newURI("http://test_move_items.com")
+                   , title: "Bookmark B" };
+
+  // Test moving items within the same folder.
+  let [folder_a_txn, bkm_a_txn, bkm_b_txn] = yield PT.transact(function* () {
+    let folder_a_txn = PT.NewFolder(folder_a_info);
+
+    folder_a_info.GUID =
+      bkm_a_info.parentGUID = bkm_b_info.parentGUID = yield folder_a_txn;
+    let bkm_a_txn = PT.NewBookmark(bkm_a_info);
+    bkm_a_info.GUID = yield bkm_a_txn;
+    let bkm_b_txn = PT.NewBookmark(bkm_b_info);
+    bkm_b_info.GUID = yield bkm_b_txn;
+    return [folder_a_txn, bkm_a_txn, bkm_b_txn];
+  });
+
+  ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
+
+  let moveTxn = PT.MoveItem({ GUID:          bkm_a_info.GUID
+                            , newParentGUID: folder_a_info.GUID
+                            , newIndex:      bmsvc.DEFAULT_INDEX });
+  yield PT.transact(moveTxn);
+
+  let ensureDo = () => {
+    ensureUndoState([[moveTxn], [bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
+    ensureItemsMoved({ GUID:          bkm_a_info.GUID
+                     , oldParentGUID: folder_a_info.GUID
+                     , newParentGUID: folder_a_info.GUID
+                     , oldIndex:      0
+                     , newIndex:      1 });
+    observer.reset();
+  };
+  let ensureUndo = () => {
+    ensureUndoState([[moveTxn], [bkm_b_txn, bkm_a_txn, folder_a_txn]], 1);
+    ensureItemsMoved({ GUID:          bkm_a_info.GUID
+                     , oldParentGUID: folder_a_info.GUID
+                     , newParentGUID: folder_a_info.GUID
+                     , oldIndex:      1
+                     , newIndex:      0 });
+    observer.reset();
+  };
+
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+  yield PT.redo();
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+
+  yield PT.clearTransactionsHistory(false, true);
+  ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
+
+  // Test moving items between folders.
+  let folder_b_info = yield createTestFolderInfo("Folder B");
+  let folder_b_txn = PT.NewFolder(folder_b_info);
+  folder_b_info.GUID = yield PT.transact(folder_b_txn);
+  ensureUndoState([ [folder_b_txn]
+                  , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 0);
+
+  moveTxn = PT.MoveItem({ GUID:          bkm_a_info.GUID
+                        , newParentGUID: folder_b_info.GUID
+                        , newIndex:      bmsvc.DEFAULT_INDEX });
+  yield PT.transact(moveTxn);
+
+  ensureDo = () => {
+    ensureUndoState([ [moveTxn]
+                    , [folder_b_txn]
+                    , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 0);
+    ensureItemsMoved({ GUID:          bkm_a_info.GUID
+                     , oldParentGUID: folder_a_info.GUID
+                     , newParentGUID: folder_b_info.GUID
+                     , oldIndex:      0
+                     , newIndex:      0 });
+    observer.reset();
+  };
+  let ensureUndo = () => {
+    ensureUndoState([ [moveTxn]
+                    , [folder_b_txn]
+                    , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 1);
+    ensureItemsMoved({ GUID:          bkm_a_info.GUID
+                     , oldParentGUID: folder_b_info.GUID
+                     , newParentGUID: folder_a_info.GUID
+                     , oldIndex:      0
+                     , newIndex:      0 });
+    observer.reset();
+  };
+
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+  yield PT.redo();
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+
+  // Clean up
+  yield PT.undo();  // folder_b_txn
+  yield PT.undo();  // folder_a_txn + the bookmarks;
+  do_check_eq(observer.itemsRemoved.size, 4);
+  ensureUndoState([ [moveTxn]
+                  , [folder_b_txn]
+                  , [bkm_b_txn, bkm_a_txn, folder_a_txn] ], 3);
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_remove_folder() {
+  let folder_level_1_info = yield createTestFolderInfo("Folder Level 1");
+  let folder_level_2_info = { title: "Folder Level 2" };
+  let [folder_level_1_txn,
+       folder_level_2_txn] = yield PT.transact(function* () {
+    let folder_level_1_txn  = PT.NewFolder(folder_level_1_info);
+    folder_level_1_info.GUID = yield folder_level_1_txn;
+    folder_level_2_info.parentGUID = folder_level_1_info.GUID;
+    let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
+    folder_level_2_info.GUID = yield folder_level_2_txn;
+    return [folder_level_1_txn, folder_level_2_txn];
+  });
+
+  ensureUndoState([[folder_level_2_txn, folder_level_1_txn]]);
+  yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+  observer.reset();
+
+  let remove_folder_2_txn = PT.RemoveItem(folder_level_2_info);
+  yield PT.transact(remove_folder_2_txn);
+
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ]);
+  yield ensureItemsRemoved(folder_level_2_info);
+
+  // Undo RemoveItem "Folder Level 2"
+  yield PT.undo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 1);
+  yield ensureItemsAdded(folder_level_2_info);
+  ensureTimestampsUpdated(folder_level_2_info.GUID, true);
+  observer.reset();
+
+  // Redo RemoveItem "Folder Level 2"
+  yield PT.redo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ]);
+  yield ensureItemsRemoved(folder_level_2_info);
+  observer.reset();
+
+  // Undo it again
+  yield PT.undo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 1);
+  yield ensureItemsAdded(folder_level_2_info);
+  ensureTimestampsUpdated(folder_level_2_info.GUID, true);
+  observer.reset();
+
+  // Undo the creation of both folders
+  yield PT.undo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 2);
+  yield ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
+  observer.reset();
+
+  // Redo the creation of both folders
+  yield PT.redo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 1);
+  yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+  ensureTimestampsUpdated(folder_level_1_info.GUID, true);
+  ensureTimestampsUpdated(folder_level_2_info.GUID, true);
+  observer.reset();
+
+  // Redo RemoveItem "Folder Level 2"
+  yield PT.redo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ]);
+  yield ensureItemsRemoved(folder_level_2_info);
+  observer.reset();
+
+  // Undo everything one last time
+  yield PT.undo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 1);
+  yield ensureItemsAdded(folder_level_2_info);
+  observer.reset();
+
+  yield PT.undo();
+  ensureUndoState([ [remove_folder_2_txn]
+                  , [folder_level_2_txn, folder_level_1_txn] ], 2);
+  yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
+  observer.reset();
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
+  const testURI = NetUtil.newURI("http://add.remove.tag")
+      , TAG_1 = "TestTag1", TAG_2 = "TestTag2"
+      , KEYWORD = "test_keyword"
+      , POST_DATA = "post_data"
+      , ANNO = { name: "TestAnno", value: "TestAnnoValue" };
+
+  let folder_info = yield createTestFolderInfo();
+  folder_info.GUID = yield PT.transact(PT.NewFolder(folder_info));
+  let ensureTags = ensureTagsForURI.bind(null, testURI);
+
+  // Check that the NewBookmark transaction preserves tags.
+  observer.reset();
+  let b1_info = { parentGUID: folder_info.GUID, uri: testURI, tags: [TAG_1] };
+  b1_info.GUID = yield PT.transact(PT.NewBookmark(b1_info));
+  ensureTags([TAG_1]);
+  yield PT.undo();
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.redo();
+  ensureTimestampsUpdated(b1_info.GUID, true);
+  ensureTags([TAG_1]);
+
+  // Check if the RemoveItem transaction removes and restores tags of children
+  // correctly.
+  yield PT.transact(PT.RemoveItem(folder_info.GUID));
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.undo();
+  ensureTimestampsUpdated(b1_info.GUID, true);
+  ensureTags([TAG_1]);
+
+  yield PT.redo();
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.undo();
+  ensureTimestampsUpdated(b1_info.GUID, true);
+  ensureTags([TAG_1]);
+
+  // * Check that no-op tagging (the uri is already tagged with TAG_1) is
+  //   also a no-op on undo.
+  // * Test the "keyword" property of the NewBookmark transaction.
+  observer.reset();
+  let b2_info = { parentGUID:  folder_info.GUID
+                , uri:         testURI, tags: [TAG_1, TAG_2]
+                , keyword:     KEYWORD
+                , postData:    POST_DATA
+                , annotations: [ANNO] };
+  b2_info.GUID = yield PT.transact(PT.NewBookmark(b2_info));
+  let b2_post_creation_changes = [
+   { GUID: b2_info.GUID
+   , isAnnoProperty: true
+   , property: ANNO.name
+   , newValue: ANNO.value },
+   { GUID: b2_info.GUID
+   , property: "keyword"
+   , newValue: KEYWORD },
+   { GUID: b2_info.GUID
+   , isAnnoProperty: true
+   , property: PlacesUtils.POST_DATA_ANNO
+   , newValue: POST_DATA } ];
+  ensureItemsChanged(...b2_post_creation_changes);
+  ensureTags([TAG_1, TAG_2]);
+
+  observer.reset();
+  yield PT.undo();
+  yield ensureItemsRemoved(b2_info);
+  ensureTags([TAG_1]);
+
+  // Check if RemoveItem correctly restores keywords, tags and annotations.
+  observer.reset();
+  yield PT.redo();
+  ensureItemsChanged(...b2_post_creation_changes);
+  ensureTags([TAG_1, TAG_2]);
+
+  // Test RemoveItem for multiple items.
+  observer.reset();
+  yield PT.transact(PT.RemoveItem(b1_info.GUID));
+  yield PT.transact(PT.RemoveItem(b2_info.GUID));
+  yield PT.transact(PT.RemoveItem(folder_info.GUID));
+  yield ensureItemsRemoved(b1_info, b2_info, folder_info);
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.undo();
+  yield ensureItemsAdded(folder_info);
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.undo();
+  ensureItemsChanged(...b2_post_creation_changes);
+  ensureTags([TAG_1, TAG_2]);
+
+  observer.reset();
+  yield PT.undo();
+  yield ensureItemsAdded(b1_info);
+  ensureTags([TAG_1, TAG_2]);
+
+  // The redo calls below cleanup everything we did.
+  observer.reset();
+  yield PT.redo();
+  yield ensureItemsRemoved(b1_info);
+  ensureTags([TAG_1, TAG_2]);
+
+  observer.reset();
+  yield PT.redo();
+  yield ensureItemsRemoved(b2_info);
+  ensureTags([]);
+
+  observer.reset();
+  yield PT.redo();
+  yield ensureItemsRemoved(folder_info);
+  ensureTags([]);
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_creating_and_removing_a_separator() {
+  let folder_info = yield createTestFolderInfo();
+  let separator_info = {};
+  let undoEntries = [];
+
+  observer.reset();
+  let create_txns = yield PT.transact(function* () {
+    let folder_txn = PT.NewFolder(folder_info);
+    folder_info.GUID = separator_info.parentGUID = yield folder_txn;
+    let separator_txn = PT.NewSeparator(separator_info);
+    separator_info.GUID = yield separator_txn;
+    return [separator_txn, folder_txn];
+  });
+  undoEntries.unshift(create_txns);
+  ensureUndoState(undoEntries, 0);
+  ensureItemsAdded(folder_info, separator_info);
+
+  observer.reset();
+  yield PT.undo();
+  ensureUndoState(undoEntries, 1);
+  ensureItemsRemoved(folder_info, separator_info);
+
+  observer.reset();
+  yield PT.redo();
+  ensureUndoState(undoEntries, 0);
+  ensureItemsAdded(folder_info, separator_info);
+
+  observer.reset();
+  let remove_sep_txn = PT.RemoveItem(separator_info);
+  yield PT.transact(remove_sep_txn);
+  undoEntries.unshift([remove_sep_txn]);
+  ensureUndoState(undoEntries, 0);
+  ensureItemsRemoved(separator_info);
+
+  observer.reset();
+  yield PT.undo();
+  ensureUndoState(undoEntries, 1);
+  ensureItemsAdded(separator_info);
+
+  observer.reset();
+  yield PT.undo();
+  ensureUndoState(undoEntries, 2);
+  ensureItemsRemoved(folder_info, separator_info);
+
+  observer.reset();
+  yield PT.redo();
+  ensureUndoState(undoEntries, 1);
+  ensureItemsAdded(folder_info, separator_info);
+
+  // Clear redo entries and check that |redo| does nothing
+  observer.reset();
+  yield PT.clearTransactionsHistory(false, true);
+  undoEntries.shift();
+  ensureUndoState(undoEntries, 0);
+  yield PT.redo();
+  ensureItemsAdded();
+  ensureItemsRemoved();
+
+  // Cleanup
+  observer.reset();
+  yield PT.undo();
+  ensureUndoState(undoEntries, 1);
+  ensureItemsRemoved(folder_info, separator_info);
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_edit_title() {
+  let bm_info = { parentGUID: yield PlacesUtils.promiseItemGUID(root)
+                , uri:        NetUtil.newURI("http://test_create_item.com")
+                , title:      "Original Title" };
+
+  function ensureTitleChange(aCurrentTitle) {
+    ensureItemsChanged({ GUID: bm_info.GUID
+                       , property: "title"
+                       , newValue: aCurrentTitle});
+  }
+
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+
+  observer.reset();
+  yield PT.transact(PT.EditTitle({ GUID: bm_info.GUID, title: "New Title" }));
+  ensureTitleChange("New Title");
+
+  observer.reset();
+  yield PT.undo();
+  ensureTitleChange("Original Title");
+
+  observer.reset();
+  yield PT.redo();
+  ensureTitleChange("New Title");
+
+  // Cleanup
+  observer.reset();
+  yield PT.undo();
+  ensureTitleChange("Original Title");
+  yield PT.undo();
+  ensureItemsRemoved(bm_info);
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_edit_url() {
+  let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
+  let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
+  let bm_info = { parentGUID: yield PlacesUtils.promiseItemGUID(root)
+                , uri:        oldURI
+                , tags:       ["TestTag"]};
+
+  function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
+    ensureItemsChanged({ GUID: bm_info.GUID
+                       , property: "uri"
+                       , newValue: aPostChangeURI.spec });
+    ensureTagsForURI(aPostChangeURI, bm_info.tags);
+    ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
+  }
+
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+  ensureTagsForURI(oldURI, bm_info.tags);
+
+  // When there's a single bookmark for the same url, tags should be moved.
+  observer.reset();
+  yield PT.transact(PT.EditURI({ GUID: bm_info.GUID, uri: newURI }));
+  ensureURIAndTags(oldURI, newURI, false);
+
+  observer.reset();
+  yield PT.undo();
+  ensureURIAndTags(newURI, oldURI, false);
+
+  observer.reset();
+  yield PT.redo();
+  ensureURIAndTags(oldURI, newURI, false);
+
+  observer.reset();
+  yield PT.undo();
+  ensureURIAndTags(newURI, oldURI, false);
+
+  // When there're multiple bookmarks for the same url, tags should be copied.
+  let bm2_info = Object.create(bm_info);
+  bm2_info.GUID = yield PT.transact(PT.NewBookmark(bm2_info));
+  let bm3_info = Object.create(bm_info);
+  bm3_info.uri = newURI;
+  bm3_info.GUID = yield PT.transact(PT.NewBookmark(bm3_info));
+
+  observer.reset();
+  yield PT.transact(PT.EditURI({ GUID: bm_info.GUID, uri: newURI }));
+  ensureURIAndTags(oldURI, newURI, true);
+
+  observer.reset();
+  yield PT.undo();
+  ensureURIAndTags(newURI, oldURI, true);
+
+  observer.reset();
+  yield PT.redo();
+  ensureURIAndTags(oldURI, newURI, true);
+
+  // Cleanup
+  observer.reset();
+  yield PT.undo();
+  ensureURIAndTags(newURI, oldURI, true);
+  yield PT.undo();
+  yield PT.undo();
+  yield PT.undo();
+  ensureItemsRemoved(bm3_info, bm2_info, bm_info);
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_edit_keyword() {
+  let bm_info = { parentGUID: yield PlacesUtils.promiseItemGUID(root)
+                , uri:        NetUtil.newURI("http://test.edit.keyword") };
+  const KEYWORD = "test_keyword";
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+  function ensureKeywordChange(aCurrentKeyword = "") {
+    ensureItemsChanged({ GUID: bm_info.GUID
+                       , property: "keyword"
+                       , newValue: aCurrentKeyword });
+  }
+
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+
+  observer.reset();
+  yield PT.transact(PT.EditKeyword({ GUID: bm_info.GUID, keyword: KEYWORD }));
+  ensureKeywordChange(KEYWORD);
+
+  observer.reset();
+  yield PT.undo();
+  ensureKeywordChange();
+
+  observer.reset();
+  yield PT.redo();
+  ensureKeywordChange(KEYWORD);
+
+  // Cleanup
+  observer.reset();
+  yield PT.undo();
+  ensureKeywordChange();
+  yield PT.undo();
+  ensureItemsRemoved(bm_info);
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_tag_uri_unbookmarked_uri() {
+  let info = { uri: NetUtil.newURI("http://un.book.marked"), tags: ["MyTag"] };
+
+  function ensureDo() {
+    // A new bookmark should be created.
+    // (getMostRecentBookmarkForURI ignores tags)
+    do_check_neq(PlacesUtils.getMostRecentBookmarkForURI(info.uri), -1);
+    ensureTagsForURI(info.uri, info.tags);
+  }
+  function ensureUndo() {
+    do_check_eq(PlacesUtils.getMostRecentBookmarkForURI(info.uri), -1);
+    ensureTagsForURI(info.uri, []);
+  }
+
+  yield PT.transact(PT.TagURI(info));
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+  yield PT.redo();
+  ensureDo();
+  yield PT.undo();
+  ensureUndo();
+});
+
+add_task(function* test_tag_uri_bookmarked_uri() {
+  let bm_info = { uri: NetUtil.newURI("http://bookmarked.uri")
+                , parentGUID: yield PlacesUtils.promiseItemGUID(root) };
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+
+  let tagging_info = { uri: bm_info.uri, tags: ["MyTag"] };
+  yield PT.transact(PT.TagURI(tagging_info));
+  ensureTagsForURI(tagging_info.uri, tagging_info.tags);
+
+  yield PT.undo();
+  ensureTagsForURI(tagging_info.uri, []);
+  yield PT.redo();
+  ensureTagsForURI(tagging_info.uri, tagging_info.tags);
+
+  // Cleanup
+  yield PT.undo();
+  ensureTagsForURI(tagging_info.uri, []);
+  observer.reset();
+  yield PT.undo();
+  ensureItemsRemoved(bm_info);
+
+  yield PT.clearTransactionsHistory();
+  ensureUndoState();
+});
+
+add_task(function* test_untag_uri() {
+  let bm_info = { uri: NetUtil.newURI("http://test.untag.uri")
+                , parentGUID: yield PlacesUtils.promiseItemGUID(root)
+                , tags: ["T"]};
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+
+  yield PT.transact(PT.UntagURI(bm_info));
+  ensureTagsForURI(bm_info.uri, []);
+  yield PT.undo();
+  ensureTagsForURI(bm_info.uri, bm_info.tags);
+  yield PT.redo();
+  ensureTagsForURI(bm_info.uri, []);
+  yield PT.undo();
+  ensureTagsForURI(bm_info.uri, bm_info.tags);
+
+  // Also test just passing the uri (should remove all tags)
+  yield PT.transact(PT.UntagURI(bm_info.uri));
+  ensureTagsForURI(bm_info.uri, []);
+  yield PT.undo();
+  ensureTagsForURI(bm_info.uri, bm_info.tags);
+  yield PT.redo();
+  ensureTagsForURI(bm_info.uri, []);
+});
+
+add_task(function* test_set_item_annotation() {
+  let bm_info = { uri: NetUtil.newURI("http://test.item.annotation")
+                , parentGUID: yield PlacesUtils.promiseItemGUID(root) };
+  let anno_info = { name: "TestAnno", value: "TestValue" };
+  function ensureAnnoState(aSet) {
+    ensureAnnotationsSet(bm_info.GUID,
+                         [{ name: anno_info.name
+                          , value: aSet ? anno_info.value : null }]);
+  }
+
+  bm_info.GUID = yield PT.transact(PT.NewBookmark(bm_info));
+
+  observer.reset();
+  yield PT.transact(PT.SetItemAnnotation({ GUID: bm_info.GUID
+                                         , annotationObject: anno_info }));
+  ensureAnnoState(true);
+
+  observer.reset();
+  yield PT.undo();
+  ensureAnnoState(false);
+
+  observer.reset();
+  yield PT.redo();
+  ensureAnnoState(true);
+
+  // Test removing the annotation by not passing the |value| property.
+  observer.reset();
+  yield PT.transact(
+    PT.SetItemAnnotation({ GUID: bm_info.GUID
+                         , annotationObject: { name: anno_info.name }}));
+  ensureAnnoState(false);
+
+  observer.reset();
+  yield PT.undo();
+  ensureAnnoState(true);
+
+  observer.reset();
+  yield PT.redo();
+  ensureAnnoState(false);
+});
+
+add_task(function* test_sort_folder_by_name() {
+  let folder_info = yield createTestFolderInfo();
+
+  let uri = NetUtil.newURI("http://sort.by.name/");
+  let preSep =  [{ title: i, uri: uri } for (i of ["3","2","1"])];
+  let sep = {};
+  let postSep = [{ title: l, uri: uri } for (l of ["c","b","a"])];
+  let originalOrder = [...preSep, sep, ...postSep];
+  let sortedOrder = [...preSep.slice(0).reverse(),
+                     sep,
+                     ...postSep.slice(0).reverse()];
+  yield PT.transact(function* () {
+    folder_info.GUID = yield PT.NewFolder(folder_info);
+    for (let info of originalOrder) {
+      info.parentGUID = folder_info.GUID;
+      info.GUID = yield info == sep ?
+                  PT.NewSeparator(info) : PT.NewBookmark(info);
+    }
+  });
+
+  let folderId = yield PlacesUtils.promiseItemId(folder_info.GUID);
+  let folderContainer = PlacesUtils.getFolderContents(folderId).root;
+  function ensureOrder(aOrder) {
+    for (let i = 0; i < folderContainer.childCount; i++) {
+      do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].GUID);
+    }
+  }
+
+  ensureOrder(originalOrder);
+  yield PT.transact(PT.SortByName(folder_info.GUID));
+  ensureOrder(sortedOrder);
+  yield PT.undo();
+  ensureOrder(originalOrder);
+  yield PT.redo();
+  ensureOrder(sortedOrder);
+
+  // Cleanup
+  observer.reset();
+  yield PT.undo();
+  ensureOrder(originalOrder);
+  yield PT.undo();
+  ensureItemsRemoved(...originalOrder, folder_info);
+});
+
+add_task(function* test_livemark_txns() {
+  let livemark_info =
+    { feedURI: NetUtil.newURI("http://test.feed.uri")
+    , parentGUID: yield PlacesUtils.promiseItemGUID(root)
+    , title: "Test Livemark" };
+  function ensureLivemarkAdded() {
+    ensureItemsAdded({ GUID:       livemark_info.GUID
+                     , title:      livemark_info.title
+                     , parentGUID: livemark_info.parentGUID
+                     , itemType:   bmsvc.TYPE_FOLDER });
+    let annos = [{ name:  PlacesUtils.LMANNO_FEEDURI
+                 , value: livemark_info.feedURI.spec }];
+    if ("siteURI" in livemark_info) {
+      annos.push({ name: PlacesUtils.LMANNO_SITEURI
+                 , value: livemark_info.siteURI.spec });
+    }
+    ensureAnnotationsSet(livemark_info.GUID, annos);
+  }
+  function ensureLivemarkRemoved() {
+    ensureItemsRemoved({ GUID:       livemark_info.GUID
+                       , parentGUID: livemark_info.parentGUID });
+  }
+
+  function* _testDoUndoRedoUndo() {
+    observer.reset();
+    livemark_info.GUID = yield PT.transact(PT.NewLivemark(livemark_info));
+    ensureLivemarkAdded();
+
+    observer.reset();
+    yield PT.undo();
+    ensureLivemarkRemoved();
+
+    observer.reset();
+    yield PT.redo();
+    ensureLivemarkAdded();
+
+    yield PT.undo();
+    ensureLivemarkRemoved();
+  }
+
+  yield* _testDoUndoRedoUndo()
+  livemark_info.siteURI = NetUtil.newURI("http://feed.site.uri");
+  yield* _testDoUndoRedoUndo();
+
+  yield PT.clearTransactionsHistory();
+});
--- a/toolkit/components/places/tests/unit/test_bookmarks_html.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -358,23 +358,19 @@ function checkItem(aExpected, aNode)
                                  .getItemAnnotation(id, PlacesUtils.POST_DATA_ANNO),
                       aExpected.postData);
           break;
         case "charset":
           let testURI = NetUtil.newURI(aNode.uri);
           do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
           break;
         case "feedUrl":
-          yield PlacesUtils.livemarks.getLivemark(
-            { id: id },
-            (aStatus, aLivemark) => {
-              do_check_true(Components.isSuccessCode(aStatus));
-              do_check_eq(aLivemark.siteURI.spec, aExpected.url);
-              do_check_eq(aLivemark.feedURI.spec, aExpected.feedUrl);
-            });
+          let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+          do_check_eq(livemark.siteURI.spec, aExpected.url);
+          do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
           break;
         case "children":
           let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
           do_check_eq(folder.hasChildren, aExpected.children.length > 0);
           folder.containerOpen = true;
           do_check_eq(folder.childCount, aExpected.children.length);
 
           aExpected.children.forEach(function (item, index) checkItem(item, folder.getChild(index)));
--- a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
@@ -149,29 +149,21 @@ function database_check() {
     toolbar.containerOpen = true;
     do_check_eq(toolbar.childCount, 3);
 
     // livemark
     var livemark = toolbar.getChild(1);
     // title
     do_check_eq("Latest Headlines", livemark.title);
 
-    let deferGetLivemark = Promise.defer();
-    PlacesUtils.livemarks.getLivemark(
-      { id: livemark.itemId },
-      function (aStatus, aLivemark) {
-        do_check_true(Components.isSuccessCode(aStatus));
-        do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
-                    aLivemark.siteURI.spec);
-        do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
-                    aLivemark.feedURI.spec);
-        deferGetLivemark.resolve();
-      }
-    );
-    yield deferGetLivemark.promise;
+    let foundLivemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.itemId });
+    do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+                foundLivemark.siteURI.spec);
+    do_check_eq("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+                foundLivemark.feedURI.spec);
 
     // cleanup
     toolbar.containerOpen = false;
 
     // UNFILED BOOKMARKS
     query.setFolders([bs.unfiledBookmarksFolder], 1);
     result = hs.executeQuery(query, hs.getNewQueryOptions());
     var unfiledBookmarks = result.root;
--- a/toolkit/components/places/tests/unit/test_bookmarks_json.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -187,23 +187,19 @@ function checkItem(aExpected, aNode) {
           do_check_eq(PlacesUtils.annotations.getItemAnnotation(
                       id, PlacesUtils.POST_DATA_ANNO), aExpected.postData);
           break;
         case "charset":
           let testURI = NetUtil.newURI(aNode.uri);
           do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
           break;
         case "feedUrl":
-          yield PlacesUtils.livemarks.getLivemark(
-            { id: id },
-            (aStatus, aLivemark) => {
-              do_check_true(Components.isSuccessCode(aStatus));
-              do_check_eq(aLivemark.siteURI.spec, aExpected.url);
-              do_check_eq(aLivemark.feedURI.spec, aExpected.feedUrl);
-            });
+          let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+          do_check_eq(livemark.siteURI.spec, aExpected.url);
+          do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
           break;
         case "children":
           let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
           do_check_eq(folder.hasChildren, aExpected.children.length > 0);
           folder.containerOpen = true;
           do_check_eq(folder.childCount, aExpected.children.length);
 
           aExpected.children.forEach(function (item, index) checkItem(item, folder.getChild(index)));
--- a/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
+++ b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
@@ -8,24 +8,21 @@ function run_test()
   do_test_pending();
 
   let annoObserver = {
     onItemAnnotationSet:
     function AO_onItemAnnotationSet(aItemId, aAnnotationName)
     {
       if (aAnnotationName == PlacesUtils.LMANNO_FEEDURI) {
         PlacesUtils.annotations.removeObserver(this);
-        PlacesUtils.livemarks.getLivemark(
-          { id: aItemId },
-          function (aStatus, aLivemark) {
-            do_check_true(Components.isSuccessCode(aStatus));
+        PlacesUtils.livemarks.getLivemark({ id: aItemId })
+          .then(aLivemark => {
             PlacesUtils.bookmarks.removeItem(aItemId);
             do_test_finished();
-          }
-        );
+          }, do_throw);
       }
     },
   
     onItemAnnotationRemoved: function () {},
     onPageAnnotationSet: function() {},
     onPageAnnotationRemoved: function() {},
     QueryInterface: XPCOMUtils.generateQI([
       Ci.nsIAnnotationObserver
@@ -34,10 +31,10 @@ function run_test()
   PlacesUtils.annotations.addObserver(annoObserver, false);
   PlacesUtils.livemarks.addLivemark(
     { title: "livemark title"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , siteURI: uri("http://example.com/")
     , feedURI: uri("http://example.com/rdf")
     }
-  );
+  ).then(null, do_throw);
 }
--- a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
+++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
@@ -173,211 +173,135 @@ add_task(function test_addLivemark_noCal
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI });
   do_check_true(onItemAddedCalled);
 });
 
 
-add_task(function test_addLivemark_noSiteURI_callback_succeeds()
+add_task(function test_addLivemark_noSiteURI_succeeds()
 {
-  let checkLivemark = aLivemark => {
-    do_check_true(aLivemark.id > 0);
-    do_check_valid_places_guid(aLivemark.guid);
-    do_check_eq(aLivemark.title, "test");
-    do_check_eq(aLivemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
-    do_check_eq(aLivemark.index, PlacesUtils.bookmarks.getItemIndex(aLivemark.id));
-    do_check_eq(aLivemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(aLivemark.id));
-    do_check_true(aLivemark.feedURI.equals(FEED_URI));
-    do_check_eq(aLivemark.siteURI, null);
-  };
-
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+    });
+  do_check_true(livemark.id > 0);
+  do_check_valid_places_guid(livemark.guid);
+  do_check_eq(livemark.title, "test");
+  do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+  do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
+  do_check_eq(livemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(livemark.id));
+  do_check_true(livemark.feedURI.equals(FEED_URI));
+  do_check_eq(livemark.siteURI, null);
 });
 
-add_task(function test_addLivemark_callback_succeeds()
+add_task(function test_addLivemark_succeeds()
 {
-  let checkLivemark = aLivemark => {
-    do_check_true(aLivemark.id > 0);
-    do_check_valid_places_guid(aLivemark.guid);
-    do_check_eq(aLivemark.title, "test");
-    do_check_eq(aLivemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
-    do_check_eq(aLivemark.index, PlacesUtils.bookmarks.getItemIndex(aLivemark.id));
-    do_check_eq(aLivemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(aLivemark.id));
-    do_check_true(aLivemark.feedURI.equals(FEED_URI));
-    do_check_true(aLivemark.siteURI.equals(SITE_URI));
-    do_check_true(PlacesUtils.annotations
-                             .itemHasAnnotation(aLivemark.id,
-                                                PlacesUtils.LMANNO_FEEDURI));
-    do_check_true(PlacesUtils.annotations
-                             .itemHasAnnotation(aLivemark.id,
-                                                PlacesUtils.LMANNO_SITEURI));
-  };
-
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     , siteURI: SITE_URI
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+    });
+
+  do_check_true(livemark.id > 0);
+  do_check_valid_places_guid(livemark.guid);
+  do_check_eq(livemark.title, "test");
+  do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+  do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
+  do_check_eq(livemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(livemark.id));
+  do_check_true(livemark.feedURI.equals(FEED_URI));
+  do_check_true(livemark.siteURI.equals(SITE_URI));
+  do_check_true(PlacesUtils.annotations
+                           .itemHasAnnotation(livemark.id,
+                                              PlacesUtils.LMANNO_FEEDURI));
+  do_check_true(PlacesUtils.annotations
+                           .itemHasAnnotation(livemark.id,
+                                              PlacesUtils.LMANNO_SITEURI));
 });
 
-add_task(function test_addLivemark_bogusid_callback_succeeds()
+add_task(function test_addLivemark_bogusid_succeeds()
 {
-  let checkLivemark = aLivemark => {
-    do_check_true(aLivemark.id > 0);
-    do_check_neq(aLivemark.id, 100);
-  };
-
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { id: 100 // Should be ignored.
     , title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     , siteURI: SITE_URI
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+    });
+  do_check_true(livemark.id > 0);
+  do_check_neq(livemark.id, 100);
 });
 
-add_task(function test_addLivemark_bogusParent_callback_fails()
+add_task(function test_addLivemark_bogusParent_fails()
 {
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   try {
     yield PlacesUtils.livemarks.addLivemark(
       { title: "test"
       , parentId: 187
       , index: PlacesUtils.bookmarks.DEFAULT_INDEX
       , feedURI: FEED_URI
-      },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_false(Components.isSuccessCode(aStatus));
-        do_check_eq(aLivemark, null);
-      } );
+      });
     do_throw("Adding a livemark with a bogus parent should fail");
-  }
-  catch(ex) {
-    do_check_true(callbackCalled);
-  }
+  } catch(ex) {}
 });
 
-add_task(function test_addLivemark_intoLivemark_callback_fails()
+add_task(function test_addLivemark_intoLivemark_fails()
 {
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-    } );
-  do_check_true(callbackCalled);
+    });
   do_check_true(Boolean(livemark));
 
-  callbackCalled = false;
   try {
     yield PlacesUtils.livemarks.addLivemark(
       { title: "test"
       , parentId: livemark.id
       , index: PlacesUtils.bookmarks.DEFAULT_INDEX
       , feedURI: FEED_URI
-      },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_false(Components.isSuccessCode(aStatus));
-        do_check_eq(aLivemark, null);
-      } );
+      });
     do_throw("Adding a livemark into a livemark should fail");
-  }
-  catch(ex) {
-    do_check_true(callbackCalled);
-  }
+  } catch(ex) {}
 });
 
-add_task(function test_addLivemark_forceGuid_callback_succeeds()
+add_task(function test_addLivemark_forceGuid_succeeds()
 {
   let checkLivemark = aLivemark => {
     do_check_eq(aLivemark.guid, "1234567890AB");
     do_check_guid_for_bookmark(aLivemark.id, "1234567890AB");
   };
 
-  // The deprecated callback is called before resolving the promise.
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     , guid: "1234567890AB"
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-  do_check_true(callbackCalled);
+    });
   checkLivemark(livemark);
 });
 
-add_task(function test_addLivemark_lastModified_callback_succeeds()
+add_task(function test_addLivemark_lastModified_succeeds()
 {
   let now = Date.now() * 1000;
-  let callbackCalled = false;
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     , lastModified: now
-    },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      do_check_eq(aLivemark.lastModified, now);
-    } );
-  do_check_true(callbackCalled);
+    });
   do_check_eq(livemark.lastModified, now);
 });
 
 add_task(function test_removeLivemark_emptyObject_throws()
 {
   try {
     yield PlacesUtils.livemarks.removeLivemark({});
     do_throw("Invoking removeLivemark with empty object should throw");
@@ -393,29 +317,21 @@ add_task(function test_removeLivemark_no
     do_throw("Invoking removeLivemark with no valid id should throw");
   } catch (ex) {
     do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
   }
 });
 
 add_task(function test_removeLivemark_nonExistent_fails()
 {
-  let callbackCalled = false;
   try {
-    yield PlacesUtils.livemarks.removeLivemark(
-      { id: 1337 },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_false(Components.isSuccessCode(aStatus));
-        do_check_eq(aLivemark, null);
-      } );
+    yield PlacesUtils.livemarks.removeLivemark({ id: 1337 });
     do_throw("Removing a non-existent livemark should fail");
   }
   catch(ex) {
-    do_check_true(callbackCalled);
   }
 });
 
 add_task(function test_removeLivemark_guid_succeeds()
 {
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
@@ -464,108 +380,70 @@ add_task(function test_getLivemark_noVal
     do_throw("Invoking getLivemark with no valid id should throw");
   } catch (ex) {
     do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
   }
 });
 
 add_task(function test_getLivemark_nonExistentId_fails()
 {
-  let callbackCalled = false;
   try {
-    yield PlacesUtils.livemarks.getLivemark({ id: 1234 },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_false(Components.isSuccessCode(aStatus));
-        do_check_eq(aLivemark, null);
-      } );
+    yield PlacesUtils.livemarks.getLivemark({ id: 1234 });
     do_throw("getLivemark for a non existent id should fail");
   }
-  catch(ex) {
-    do_check_true(callbackCalled);
-  }
+  catch(ex) {}
 });
 
 add_task(function test_getLivemark_nonExistentGUID_fails()
 {
-  let callbackCalled = false;
   try {
-    yield PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_false(Components.isSuccessCode(aStatus));
-        do_check_eq(aLivemark, null);
-      } );
+    yield PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
     do_throw("getLivemark for a non-existent guid should fail");
   }
-  catch(ex) {
-    do_check_true(callbackCalled);
-  }
+  catch(ex) {}
 });
 
 add_task(function test_getLivemark_guid_succeeds()
 {
   yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     , guid: "34567890ABCD" });
 
-  let checkLivemark = aLivemark => {
-    do_check_eq(aLivemark.title, "test");
-    do_check_eq(aLivemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
-    do_check_eq(aLivemark.index, PlacesUtils.bookmarks.getItemIndex(aLivemark.id));
-    do_check_true(aLivemark.feedURI.equals(FEED_URI));
-    do_check_eq(aLivemark.siteURI, null);
-    do_check_eq(aLivemark.guid, "34567890ABCD");
-  };
-
   // invalid id to check the guid wins.
   let livemark =
-    yield PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" },
-      (aStatus, aLivemark) => {
-        callbackCalled = true;
-        do_check_true(Components.isSuccessCode(aStatus));
-        checkLivemark(aLivemark)
-      } );
+    yield PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
 
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+  do_check_eq(livemark.title, "test");
+  do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+  do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
+  do_check_true(livemark.feedURI.equals(FEED_URI));
+  do_check_eq(livemark.siteURI, null);
+  do_check_eq(livemark.guid, "34567890ABCD");
 });
 
 add_task(function test_getLivemark_id_succeeds()
 {
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
     , feedURI: FEED_URI
     });
 
-  let checkLivemark = aLivemark => {
-    do_check_eq(aLivemark.title, "test");
-    do_check_eq(aLivemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
-    do_check_eq(aLivemark.index, PlacesUtils.bookmarks.getItemIndex(aLivemark.id));
-    do_check_true(aLivemark.feedURI.equals(FEED_URI));
-    do_check_eq(aLivemark.siteURI, null);
-    do_check_guid_for_bookmark(aLivemark.id, aLivemark.guid);
-  };
+  livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.id });
 
-  let callbackCalled = false;
-  livemark = yield PlacesUtils.livemarks.getLivemark(
-    { id: livemark.id },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+  do_check_eq(livemark.title, "test");
+  do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+  do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
+  do_check_true(livemark.feedURI.equals(FEED_URI));
+  do_check_eq(livemark.siteURI, null);
+  do_check_guid_for_bookmark(livemark.id, livemark.guid);
 });
 
 add_task(function test_getLivemark_removeItem_contention()
 {
   PlacesUtils.livemarks.addLivemark({ title: "test"
                                     , parentId: PlacesUtils.unfiledBookmarksFolderId
                                     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
                                     , feedURI: FEED_URI
@@ -574,36 +452,24 @@ add_task(function test_getLivemark_remov
   PlacesUtils.livemarks.addLivemark({ title: "test"
                                     , parentId: PlacesUtils.unfiledBookmarksFolderId
                                     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
                                     , feedURI: FEED_URI
                                     });
   let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId,
                                                 PlacesUtils.bookmarks.DEFAULT_INDEX);
 
-  let checkLivemark = (aLivemark) => {
-    do_check_eq(aLivemark.title, "test");
-    do_check_eq(aLivemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
-    do_check_eq(aLivemark.index, PlacesUtils.bookmarks.getItemIndex(aLivemark.id));
-    do_check_true(aLivemark.feedURI.equals(FEED_URI));
-    do_check_eq(aLivemark.siteURI, null);
-    do_check_guid_for_bookmark(aLivemark.id, aLivemark.guid);
-  };
+  let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
 
-  let callbackCalled = false;
-  let livemark = yield PlacesUtils.livemarks.getLivemark(
-    { id: id },
-    (aStatus, aLivemark) => {
-      callbackCalled = true;
-      do_check_true(Components.isSuccessCode(aStatus));
-      checkLivemark(aLivemark);
-    } );
-
-  do_check_true(callbackCalled);
-  checkLivemark(livemark);
+  do_check_eq(livemark.title, "test");
+  do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+  do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
+  do_check_true(livemark.feedURI.equals(FEED_URI));
+  do_check_eq(livemark.siteURI, null);
+  do_check_guid_for_bookmark(livemark.id, livemark.guid);
 });
 
 add_task(function test_title_change()
 {
   let livemark = yield PlacesUtils.livemarks.addLivemark(
     { title: "test"
     , parentId: PlacesUtils.unfiledBookmarksFolderId
     , index: PlacesUtils.bookmarks.DEFAULT_INDEX
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -133,8 +133,9 @@ skip-if = os == "android"
 [test_utils_getURLsForContainerNode.js]
 [test_utils_setAnnotationsFor.js]
 [test_PlacesUtils_asyncGetBookmarkIds.js]
 [test_PlacesUtils_lazyobservers.js]
 [test_placesTxn.js]
 [test_telemetry.js]
 [test_getPlacesInfo.js]
 [test_pageGuid_bookmarkGuid.js]
+[test_async_transactions.js]