Bug 613477 - Make the primary Star UI async.
authorMarco Bonardo <mbonardo@mozilla.com>
Mon, 22 Nov 2010 20:34:57 +0100
changeset 59348 ac0b81efdb004863874b237de25a2c8a5e0a0598
parent 59347 f118db7595c60b053b78d58cc64d97968cfcd556
child 59349 6cdfd382478d0ad089d34d727cd753d5baeecb9b
push id1
push usershaver@mozilla.com
push dateTue, 04 Jan 2011 17:58:04 +0000
bugs613477
milestone2.0b8pre
Bug 613477 - Make the primary Star UI async. r=sdwilsh ui-r=limi a=blocking
browser/base/content/browser-places.js
browser/base/content/browser.xul
browser/base/content/test/browser_bug432599.js
browser/base/content/test/browser_bug581253.js
browser/components/places/tests/browser/browser_410196_paste_into_tags.js
toolkit/components/places/src/PlacesUtils.jsm
toolkit/components/places/src/nsTaggingService.js
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -18,16 +18,17 @@
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #   Ben Goodger <beng@google.com>
 #   Annie Sullivan <annie.sullivan@gmail.com>
 #   Joe Hughes <joe@retrovirus.com>
 #   Asaf Romano <mano@mozilla.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
+#   Marco Bonardo <mak77@bonardo.net>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -915,92 +916,158 @@ var PlacesMenuDNDHandler = {
                                 Ci.nsITreeView.DROP_ON);
     PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
     event.stopPropagation();
   }
 };
 
 
 var PlacesStarButton = {
-  init: function PSB_init() {
+  init: function PSB_init()
+  {
     try {
       PlacesUtils.bookmarks.addObserver(this, false);
     } catch(ex) {
       Components.utils.reportError("PlacesStarButton.init(): error adding bookmark observer: " + ex);
     }
   },
 
-  uninit: function PSB_uninit() {
-    PlacesUtils.bookmarks.removeObserver(this);
+  uninit: function PSB_uninit()
+  {
+    try {
+      PlacesUtils.bookmarks.removeObserver(this);
+    } catch(ex) {}
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsINavBookmarkObserver
+  ]),
+
+  get _starredTooltip()
+  {
+    delete this._starredTooltip;
+    return this._starredTooltip =
+      gNavigatorBundle.getString("starButtonOn.tooltip");
+  },
+  get _unstarredTooltip()
+  {
+    delete this._unstarredTooltip;
+    return this._unstarredTooltip =
+      gNavigatorBundle.getString("starButtonOff.tooltip");
   },
 
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver]),
-
-  _starred: false,
-  _batching: false,
+  updateState: function PSB_updateState()
+  {
+    this._starIcon = document.getElementById("star-button");
+    if (!this._starIcon || gBrowser.currentURI.equals(this._uri)) {
+      return;
+    }
 
-  updateState: function PSB_updateState() {
-    var starIcon = document.getElementById("star-button");
-    if (!starIcon)
-      return;
+    // Reset tracked values.
+    this._uri = gBrowser.currentURI;
+    this._itemIds = [];
+
+    // Hide the star while we update its state.
+    this._starIcon.hidden = true;
 
-    var uri = gBrowser.currentURI;
-    this._starred = uri && (PlacesUtils.getMostRecentBookmarkForURI(uri) != -1 ||
-                            PlacesUtils.getMostRecentFolderForFeedURI(uri) != -1);
-    if (this._starred) {
-      starIcon.setAttribute("starred", "true");
-      starIcon.setAttribute("tooltiptext", gNavigatorBundle.getString("starButtonOn.tooltip"));
+    PlacesUtils.asyncGetBookmarkIds(this._uri, function (aItemIds) {
+      this._itemIds = aItemIds;
+      this._updateStateInternal();
+      // Finally show the star.
+      this._starIcon.hidden = false;
+    }, this);
+  },
+
+  _updateStateInternal: function PSB__updateStateInternal()
+  {
+    if (!this._starIcon) {
+      return;
     }
-    else {
-      starIcon.removeAttribute("starred");
-      starIcon.setAttribute("tooltiptext", gNavigatorBundle.getString("starButtonOff.tooltip"));
+
+    let starred = this._starIcon.hasAttribute("starred");
+    if (this._itemIds.length > 0 && !starred) {
+      this._starIcon.setAttribute("starred", "true");
+      this._starIcon.setAttribute("tooltiptext", this._starredTooltip);
+    }
+    else if (this._itemIds.length == 0 && starred) {
+      this._starIcon.removeAttribute("starred");
+      this._starIcon.setAttribute("tooltiptext", this._unstarredTooltip);
     }
   },
 
-  onClick: function PSB_onClick(aEvent) {
-    if (aEvent.button == 0)
-      PlacesCommandHook.bookmarkCurrentPage(this._starred);
-
-    // don't bubble to the textbox so that the address won't be selected
+  onClick: function PSB_onClick(aEvent)
+  {
+    if (aEvent.button == 0) {
+      PlacesCommandHook.bookmarkCurrentPage(this._itemIds.length > 0);
+    }
+    // Don't bubble to the textbox, to avoid unwanted selection of the address.
     aEvent.stopPropagation();
   },
 
-  // nsINavBookmarkObserver  
-  onBeginUpdateBatch: function PSB_onBeginUpdateBatch() {
-    this._batching = true;
-  },
+  // nsINavBookmarkObserver
+  onItemAdded:
+  function PSB_onItemAdded(aItemId, aFolder, aIndex, aItemType, aURI)
+  {
+    if (!this._starIcon) {
+      return;
+    }
 
-  onEndUpdateBatch: function PSB_onEndUpdateBatch() {
-    this.updateState();
-    this._batching = false;
-  },
-
-  onItemAdded: function PSB_onItemAdded(aItemId, aFolder, aIndex, aItemType,
-                                        aURI) {
-    if (!this._batching && !this._starred)
-      this.updateState();
+    if (aURI.equals(this._uri)) {
+      // If a new bookmark has been added to the tracked uri, register it.
+      if (this._itemIds.indexOf(aItemId) == -1) {
+        this._itemIds.push(aItemId);
+        this._updateStateInternal();
+      }
+    }
   },
 
-  onBeforeItemRemoved: function() {},
+  onItemRemoved:
+  function PSB_onItemRemoved(aItemId, aFolder, aIndex, aItemType)
+  {
+    if (!this._starIcon) {
+      return;
+    }
 
-  onItemRemoved: function PSB_onItemRemoved(aItemId, aFolder, aIndex,
-                                            aItemType) {
-    if (!this._batching)
-      this.updateState();
+    let index = this._itemIds.indexOf(aItemId);
+    // If one of the tracked bookmarks has been removed, unregister it.
+    if (index != -1) {
+      this._itemIds.splice(index, 1);
+      this._updateStateInternal();
+    }
   },
 
-  onItemChanged: function PSB_onItemChanged(aItemId, aProperty,
-                                            aIsAnnotationProperty, aNewValue,
-                                            aLastModified, aItemType) {
-    if (!this._batching && aProperty == "uri")
-      this.updateState();
+  onItemChanged:
+  function PSB_onItemChanged(aItemId, aProperty, aIsAnnotationProperty,
+                             aNewValue, aLastModified, aItemType)
+  {
+    if (!this._starIcon) {
+      return;
+    }
+
+    if (aProperty == "uri") {
+      let index = this._itemIds.indexOf(aItemId);
+      // If the changed bookmark was tracked, check if it is now pointing to
+      // a different uri and unregister it.
+      if (index != -1 && aNewValue != this._uri.spec) {
+        this._itemIds.splice(index, 1);
+        this._updateStateInternal();
+      }
+      // If another bookmark is now pointing to the tracked uri, register it.
+      else if (index == -1 && aNewValue == this._uri.spec) {
+        this._itemIds.push(aItemId);
+        this._updateStateInternal();
+      }
+    }
   },
 
-  onItemVisited: function() {},
-  onItemMoved: function() {}
+  onBeginUpdateBatch: function () {},
+  onEndUpdateBatch: function () {},
+  onBeforeItemRemoved: function () {},
+  onItemVisited: function () {},
+  onItemMoved: function () {}
 };
 
 
 // This object handles the initialization and uninitialization of the bookmarks
 // toolbar.  updateState is called when the browser window is opened and
 // after closing the toolbar customization dialog.
 let PlacesToolbarHelper = {
   _place: "place:folder=TOOLBAR",
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -567,16 +567,17 @@
                 <label id="identity-icon-country-label" class="plain"/>
               </hbox>
             </hbox>
           </box>
           <label id="urlbar-display" value="&urlbar.switchToTab.label;"/>
           <hbox id="urlbar-icons">
             <image id="star-button"
                    class="urlbar-icon"
+                   hidden="true"
                    onclick="PlacesStarButton.onClick(event);"/>
             <image id="go-button"
                    class="urlbar-icon"
                    tooltiptext="&goEndCap.tooltip;"
                    onclick="gURLBar.handleCommand(event);"/>
           </hbox>
           <toolbarbutton id="urlbar-go-button"
                          class="chromeclass-toolbar-additional"
--- a/browser/base/content/test/browser_bug432599.js
+++ b/browser/base/content/test/browser_bug432599.js
@@ -26,67 +26,87 @@ function invokeUsingStarButton(phase) {
   case 3:
     EventUtils.synthesizeMouse(document.getElementById("star-button"),
                                1, 1, { clickCount: 2 });
     break;
   }
 }
 
 var testURL = "data:text/plain,Content";
+var bookmarkId;
+
+function add_bookmark(aURI, aTitle) {
+  return PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                              aURI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                              aTitle);
+}
 
 // test bug 432599
 function test() {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", initTest, true);
+  gBrowser.selectedBrowser.addEventListener("load", function () {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    waitForStarChange(false, initTest);
+  }, true);
 
   content.location = testURL;
 }
 
+function initTest() {
+  // First, bookmark the page.
+  bookmarkId = add_bookmark(makeURI(testURL), "Bug 432599 Test");
+
+  checkBookmarksPanel(invokers[currentInvoker], 1);
+}
+
+function waitForStarChange(aValue, aCallback) {
+  let starButton = document.getElementById("star-button");
+  if (starButton.hidden || starButton.hasAttribute("starred") != aValue) {
+    info("Waiting for star button change.");
+    setTimeout(arguments.callee, 50, aValue, aCallback);
+    return;
+  }
+  aCallback();
+}
+
 let invokers = [invokeUsingStarButton, invokeUsingCtrlD];
 let currentInvoker = 0;
 
-function initTest() {
-  gBrowser.selectedBrowser.removeEventListener("load", initTest, true);
-  // first, bookmark the page
-  Application.bookmarks.toolbar.addBookmark("Bug 432599 Test", makeURI(testURL));
-
-  checkBookmarksPanel(invokers[currentInvoker], 1);
-}
-
 let initialValue;
 let initialRemoveHidden;
 
 let popupElement = document.getElementById("editBookmarkPanel");
 let titleElement = document.getElementById("editBookmarkPanelTitle");
 let removeElement = document.getElementById("editBookmarkPanelRemoveButton");
 
 function checkBookmarksPanel(invoker, phase)
 {
   let onPopupShown = function(aEvent) {
     if (aEvent.originalTarget == popupElement) {
+      popupElement.removeEventListener("popupshown", arguments.callee, false);
       checkBookmarksPanel(invoker, phase + 1);
-      popupElement.removeEventListener("popupshown", onPopupShown, false);
     }
   };
   let onPopupHidden = function(aEvent) {
     if (aEvent.originalTarget == popupElement) {
+      popupElement.removeEventListener("popuphidden", arguments.callee, false);
       if (phase < 4) {
         checkBookmarksPanel(invoker, phase + 1);
       } else {
         ++currentInvoker;
         if (currentInvoker < invokers.length) {
           checkBookmarksPanel(invokers[currentInvoker], 1);
         } else {
           gBrowser.removeCurrentTab();
-          finish();
+          PlacesUtils.bookmarks.removeItem(bookmarkId);
+          executeSoon(finish);
         }
       }
-      popupElement.removeEventListener("popuphidden", onPopupHidden, false);
     }
   };
 
   switch (phase) {
   case 1:
   case 3:
     popupElement.addEventListener("popupshown", onPopupShown, false);
     break;
--- a/browser/base/content/test/browser_bug581253.js
+++ b/browser/base/content/test/browser_bug581253.js
@@ -16,29 +16,43 @@ function test() {
     let uri = makeURI(testURL);
     let bmTxn =
       new PlacesCreateBookmarkTransaction(uri,
                                           PlacesUtils.unfiledBookmarksFolderId,
                                           -1, "", null, []);
     PlacesUtils.transactionManager.doTransaction(bmTxn);
 
     ok(PlacesUtils.bookmarks.isBookmarked(uri), "the test url is bookmarked");
-    ok(starButton.getAttribute("starred") == "true",
-       "star button indicates that the page is bookmarked");
-    
-    let tagTxn = new PlacesTagURITransaction(uri, [testTag]);
-    PlacesUtils.transactionManager.doTransaction(tagTxn);
-    
-    StarUI.panel.addEventListener("popupshown", onPanelShown, false);
-    starButton.click();
+    waitForStarChange(true, onStarred);
   }), true);
 
   content.location = testURL;
 }
 
+function waitForStarChange(aValue, aCallback) {
+  let starButton = document.getElementById("star-button");
+  if (starButton.hidden || starButton.hasAttribute("starred") != aValue) {
+    info("Waiting for star button change.");
+    setTimeout(arguments.callee, 50, aValue, aCallback);
+    return;
+  }
+  aCallback();
+}
+
+function onStarred() {
+  ok(starButton.getAttribute("starred") == "true",
+     "star button indicates that the page is bookmarked");
+
+  let uri = makeURI(testURL);
+  let tagTxn = new PlacesTagURITransaction(uri, [testTag]);
+  PlacesUtils.transactionManager.doTransaction(tagTxn);
+
+  StarUI.panel.addEventListener("popupshown", onPanelShown, false);
+  starButton.click();
+}
 
 function onPanelShown(aEvent) {
   if (aEvent.target == StarUI.panel) {
     StarUI.panel.removeEventListener("popupshown", arguments.callee, false);
     let tagsField = document.getElementById("editBMPanel_tagsField");
     ok(tagsField.value == testTag, "tags field value was set");
     tagsField.focus();
 
--- a/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
+++ b/browser/components/places/tests/browser/browser_410196_paste_into_tags.js
@@ -1,226 +1,186 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Places test code.
- *
- * The Initial Developer of the Original Code is Mozilla Corp.
- * Portions created by the Initial Developer are Copyright (C) 2009
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  David Dahl <ddahl@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-// Get history services
-var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
-         getService(Ci.nsINavHistoryService);
-var gh = hs.QueryInterface(Ci.nsIGlobalHistory2);
-var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
-var ts = Cc["@mozilla.org/browser/tagging-service;1"].
-         getService(Components.interfaces.nsITaggingService);
-var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
-         getService(Ci.nsINavBookmarksService);
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
 
 function add_visit(aURI, aReferrer) {
-  var visitId = hs.addVisit(aURI,
-                            Date.now() * 1000,
-                            aReferrer,
-                            hs.TRANSITION_TYPED, // user typed in URL bar
-                            false, // not redirect
-                            0);
-  return visitId;
+  return PlacesUtils.history.addVisit(aURI, Date.now() * 1000, aReferrer,
+                                      PlacesUtils.history.TRANSITION_TYPED,
+                                      false, 0);
 }
 
 function add_bookmark(aURI) {
-  var bId = bs.insertBookmark(bs.unfiledBookmarksFolder, aURI,
-                              bs.DEFAULT_INDEX, "bookmark/" + aURI.spec);
-  return bId;
+  return PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                              aURI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                              "bookmark/" + aURI.spec);
 }
 
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
 const TEST_URL = "http://example.com/";
 const MOZURISPEC = "http://mozilla.com/";
 
+let gLibrary;
+let PlacesOrganizer;
+
 function test() {
   waitForExplicitFinish();
-  var win = window.openDialog("chrome://browser/content/places/places.xul",
-                              "",
-                              "chrome,toolbar=yes,dialog=no,resizable");
+  gLibrary = window.openDialog("chrome://browser/content/places/places.xul",
+                               "", "chrome,toolbar=yes,dialog=no,resizable");
+  waitForFocus(onLibraryReady, gLibrary);
+}
 
-  win.addEventListener("load", function onload() {
-    win.removeEventListener("load", onload, false);
-    executeSoon(function () {
-      var PU = win.PlacesUtils;
-      var PO = win.PlacesOrganizer;
-      var PUIU = win.PlacesUIUtils;
+function onLibraryReady() {
+  ok(PlacesUtils, "PlacesUtils in scope");
+  ok(PlacesUIUtils, "PlacesUIUtils in scope");
 
-      // individual tests for each step of tagging a history item
-      var tests = {
+  PlacesOrganizer = gLibrary.PlacesOrganizer;
+  ok(PlacesOrganizer, "Places organizer in scope");
 
-        sanity: function(){
-          // sanity check
-          ok(PU, "PlacesUtils in scope");
-          ok(PUIU, "PlacesUIUtils in scope");
-          ok(PO, "Places organizer in scope");
-        },
+  tests.makeHistVisit();
+  tests.makeTag();
+  tests.focusTag();
+  tests.copyHistNode();
+  tests.waitForClipboard();
+}
+
+function onClipboardReady() {
+  tests.pasteToTag();
+  tests.historyNode();
+  tests.checkForBookmarkInUI();
 
-        makeHistVisit: function() {
-          // need to add a history object
-          var testURI1 = PU._uri(MOZURISPEC);
-          isnot(testURI1, null, "testURI is not null");
-          var visitId = add_visit(testURI1);
-          ok(visitId > 0, "A visit was added to the history");
-          ok(gh.isVisited(testURI1), MOZURISPEC + " is a visited url.");
-        },
+  gLibrary.close();
+
+  // Remove new Places data we created.
+  PlacesUtils.tagging.untagURI(NetUtil.newURI(MOZURISPEC), ["foo"]);
+  PlacesUtils.tagging.untagURI(NetUtil.newURI(TEST_URL), ["foo"]);
+  let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(TEST_URL));
+  is(tags.length, 0, "tags are gone");
+  PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+  
+  waitForClearHistory(finish);
+}
 
-        makeTag: function() {
-          // create an initial tag to work with
-          var bmId = add_bookmark(PlacesUtils._uri(TEST_URL));
-          ok(bmId > 0, "A bookmark was added");
-          ts.tagURI(PlacesUtils._uri(TEST_URL), ["foo"]);
-          var tags = ts.getTagsForURI(PU._uri(TEST_URL));
-          is(tags[0], 'foo', "tag is foo");
-        },
+let tests = {
+
+  makeHistVisit: function() {
+    // need to add a history object
+    let testURI1 = NetUtil.newURI(MOZURISPEC);
+    isnot(testURI1, null, "testURI is not null");
+    let visitId = add_visit(testURI1);
+    ok(visitId > 0, "A visit was added to the history");
+    ok(PlacesUtils.ghistory2.isVisited(testURI1), MOZURISPEC + " is a visited url.");
+  },
 
-        focusTag: function (paste){
-          // focus the new tag
-          PO.selectLeftPaneQuery("Tags");
-          var tags = PO._places.selectedNode;
-          tags.containerOpen = true;
-          var fooTag = tags.getChild(0);
-          this.tagNode = fooTag;
-          PO._places.selectNode(fooTag);
-          is(this.tagNode.title, 'foo', "tagNode title is foo");
-          var ip = PO._places.insertionPoint;
-          ok(ip.isTag, "IP is a tag");
-          if (paste) {
-            ok(true, "About to paste");
-            PO._places.controller.paste();
-          }
-        },
+  makeTag: function() {
+    // create an initial tag to work with
+    let bmId = add_bookmark(NetUtil.newURI(TEST_URL));
+    ok(bmId > 0, "A bookmark was added");
+    PlacesUtils.tagging.tagURI(NetUtil.newURI(TEST_URL), ["foo"]);
+    let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(TEST_URL));
+    is(tags[0], 'foo', "tag is foo");
+  },
 
-        histNode: null,
+  focusTag: function (paste){
+    // focus the new tag
+    PlacesOrganizer.selectLeftPaneQuery("Tags");
+    let tags = PlacesOrganizer._places.selectedNode;
+    tags.containerOpen = true;
+    let fooTag = tags.getChild(0);
+    this.tagNode = fooTag;
+    PlacesOrganizer._places.selectNode(fooTag);
+    is(this.tagNode.title, 'foo', "tagNode title is foo");
+    let ip = PlacesOrganizer._places.insertionPoint;
+    ok(ip.isTag, "IP is a tag");
+    if (paste) {
+      ok(true, "About to paste");
+      PlacesOrganizer._places.controller.paste();
+    }
+  },
 
-        copyHistNode: function (){
-          // focus the history object
-          PO.selectLeftPaneQuery("History");
-          var histContainer = PO._places.selectedNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
-          histContainer.containerOpen = true;
-          PO._places.selectNode(histContainer.getChild(0));
-          this.histNode = PO._content.view.nodeForTreeIndex(0);
-          PO._content.selectNode(this.histNode);
-          is(this.histNode.uri, MOZURISPEC,
-             "historyNode exists: " + this.histNode.uri);
-          // copy the history node
-          PO._content.controller.copy();
-        },
+  histNode: null,
 
-        waitForPaste: function (){
-          try {
-            var xferable = Cc["@mozilla.org/widget/transferable;1"].
-                           createInstance(Ci.nsITransferable);
-            xferable.addDataFlavor(PU.TYPE_X_MOZ_PLACE);
-            var clipboard = Cc["@mozilla.org/widget/clipboard;1"].
-                            getService(Ci.nsIClipboard);
-            clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
-            var data = { }, type = { };
-            xferable.getAnyTransferData(type, data, { });
-            // Data is in the clipboard
-            continue_test();
-          } catch (ex) {
-            // check again after 100ms.
-            setTimeout(tests.waitForPaste, 100);
-          }
-        },
+  copyHistNode: function (){
+    // focus the history object
+    PlacesOrganizer.selectLeftPaneQuery("History");
+    let histContainer = PlacesOrganizer._places.selectedNode;
+    PlacesUtils.asContainer(histContainer);
+    histContainer.containerOpen = true;
+    PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+    this.histNode = PlacesOrganizer._content.view.nodeForTreeIndex(0);
+    PlacesOrganizer._content.selectNode(this.histNode);
+    is(this.histNode.uri, MOZURISPEC,
+       "historyNode exists: " + this.histNode.uri);
+    // copy the history node
+    PlacesOrganizer._content.controller.copy();
+  },
 
-        pasteToTag: function (){
-          // paste history node into tag
-          this.focusTag(true);
-        },
+  waitForClipboard: function (){
+    try {
+      let xferable = Cc["@mozilla.org/widget/transferable;1"].
+                     createInstance(Ci.nsITransferable);
+      xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
+      let clipboard = Cc["@mozilla.org/widget/clipboard;1"].
+                      getService(Ci.nsIClipboard);
+      clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+      let data = { }, type = { };
+      xferable.getAnyTransferData(type, data, { });
+      // Data is in the clipboard
+      onClipboardReady();
+    } catch (ex) {
+      // check again after 100ms.
+      setTimeout(arguments.callee, 100);
+    }
+  },
 
-        historyNode: function (){
-          // re-focus the history again
-          PO.selectLeftPaneQuery("History");
-          var histContainer = PO._places.selectedNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
-          histContainer.containerOpen = true;
-          PO._places.selectNode(histContainer.getChild(0));
-          var histNode = PO._content.view.nodeForTreeIndex(0);
-          ok(histNode, "histNode exists: " + histNode.title);
-          // check to see if the history node is tagged!
-          var tags = PU.tagging.getTagsForURI(PU._uri(MOZURISPEC));
-          ok(tags.length == 1, "history node is tagged: " + tags.length);
-          // check if a bookmark was created
-          var isBookmarked = PU.bookmarks.isBookmarked(PU._uri(MOZURISPEC));
-          is(isBookmarked, true, MOZURISPEC + " is bookmarked");
-          var bookmarkIds = PU.bookmarks.getBookmarkIdsForURI(
-                              PU._uri(histNode.uri));
-          ok(bookmarkIds.length > 0, "bookmark exists for the tagged history item: " + bookmarkIds);
-        },
+  pasteToTag: function (){
+    // paste history node into tag
+    this.focusTag(true);
+  },
 
-        checkForBookmarkInUI: function(){
-          // is the bookmark visible in the UI?
-          // get the Unsorted Bookmarks node
-          PO.selectLeftPaneQuery("UnfiledBookmarks");
-          // now we can see what is in the _content tree
-          var unsortedNode = PO._content.view.nodeForTreeIndex(1);
-          ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
-          is(unsortedNode.uri, MOZURISPEC, "node uri's are the same");
-        },
-
-        tagNode: null,
-
-        cleanUp: function(){
-          ts.untagURI(PU._uri(MOZURISPEC), ["foo"]);
-          ts.untagURI(PU._uri(TEST_URL), ["foo"]);
-          hs.removeAllPages();
-          var tags = ts.getTagsForURI(PU._uri(TEST_URL));
-          is(tags.length, 0, "tags are gone");
-          bs.removeFolderChildren(bs.unfiledBookmarksFolder);
-        }
-      };
+  historyNode: function (){
+    // re-focus the history again
+    PlacesOrganizer.selectLeftPaneQuery("History");
+    let histContainer = PlacesOrganizer._places.selectedNode;
+    PlacesUtils.asContainer(histContainer);
+    histContainer.containerOpen = true;
+    PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+    let histNode = PlacesOrganizer._content.view.nodeForTreeIndex(0);
+    ok(histNode, "histNode exists: " + histNode.title);
+    // check to see if the history node is tagged!
+    let tags = PlacesUtils.tagging.getTagsForURI(NetUtil.newURI(MOZURISPEC));
+    ok(tags.length == 1, "history node is tagged: " + tags.length);
+    // check if a bookmark was created
+    let isBookmarked = PlacesUtils.bookmarks.isBookmarked(NetUtil.newURI(MOZURISPEC));
+    is(isBookmarked, true, MOZURISPEC + " is bookmarked");
+    let bookmarkIds = PlacesUtils.bookmarks.getBookmarkIdsForURI(
+                        NetUtil.newURI(histNode.uri));
+    ok(bookmarkIds.length > 0, "bookmark exists for the tagged history item: " + bookmarkIds);
+  },
 
-      tests.sanity();
-      tests.makeHistVisit();
-      tests.makeTag();
-      tests.focusTag();
-      tests.copyHistNode();
-      tests.waitForPaste();
-      
-      function continue_test() {
-        tests.pasteToTag();
-        tests.historyNode();
-        tests.checkForBookmarkInUI();
+  checkForBookmarkInUI: function(){
+    // is the bookmark visible in the UI?
+    // get the Unsorted Bookmarks node
+    PlacesOrganizer.selectLeftPaneQuery("UnfiledBookmarks");
+    // now we can see what is in the _content tree
+    let unsortedNode = PlacesOrganizer._content.view.nodeForTreeIndex(1);
+    ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
+    is(unsortedNode.uri, MOZURISPEC, "node uri's are the same");
+  },
+
+  tagNode: null,
+};
 
-        // remove new places data we created
-        tests.cleanUp();
-
-        win.close();
-        finish();
-      }
-
-    });
-  },false);
+/**
+ * Clears history invoking callback when done.
+ */
+function waitForClearHistory(aCallback) {
+  const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
+  let observer = {
+    observe: function(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(this, TOPIC_EXPIRATION_FINISHED);
+      aCallback();
+    }
+  };
+  Services.obs.addObserver(observer, TOPIC_EXPIRATION_FINISHED, false);
+  PlacesUtils.bhistory.removeAllPages();
 }
--- a/toolkit/components/places/src/PlacesUtils.jsm
+++ b/toolkit/components/places/src/PlacesUtils.jsm
@@ -77,20 +77,16 @@ XPCOMUtils.defineLazyGetter(this, "Servi
   return Services;
 });
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
-// Global observers flags.
-let gHasAnnotationsObserver = false;
-let gHasShutdownObserver = false;
-
 // The minimum amount of transactions before starting a batch. Usually we do
 // do incremental updates, a batch will cause views to completely
 // refresh instead.
 const MIN_TRANSACTIONS_FOR_BATCH = 5;
 
 // The RESTORE_*_NSIOBSERVER_TOPIC constants should match the #defines of the
 // same names in browser/components/places/src/nsPlacesImportExportService.cpp
 const RESTORE_BEGIN_NSIOBSERVER_TOPIC = "bookmarks-restore-begin";
@@ -270,55 +266,52 @@ var PlacesUtils = {
    * When the annotation observer detects annotations added or
    * removed that are the RO annotation name, it adds/removes
    * the ids from the cache.
    *
    * At shutdown, the annotation and shutdown observers are removed.
    */
   get _readOnly() {
     // Add annotations observer.
-    if (!gHasAnnotationsObserver) {
-      this.annotations.addObserver(this, false);
-      gHasAnnotationsObserver = true;
-    }
-    // Observe shutdown, so we can remove the anno observer.
-    if (!gHasShutdownObserver) {
-      Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
-      gHasShutdownObserver = true;
-    }
+    this.annotations.addObserver(this, false);
+    this.registerShutdownFunction(function () {
+      this.annotations.removeObserver(this);
+    });
+
     var readOnly = this.annotations.getItemsWithAnnotation(this.READ_ONLY_ANNO);
     this.__defineGetter__("_readOnly", function() readOnly);
     return this._readOnly;
   },
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIAnnotationObserver
   , Ci.nsIObserver
   , Ci.nsITransactionListener
   ]),
 
-  // nsIObserver
-  observe: function PU_observe(aSubject, aTopic, aData) {
+  _shutdownFunctions: [],
+  registerShutdownFunction: function PU_registerShutdownFunction(aFunc)
+  {
+    // If this is the first registered function, add the shutdown observer.
+    if (this._shutdownFunctions.length == 0) {
+      Services.obs.addObserver(this, this.TOPIC_SHUTDOWN, false);
+    }
+    this._shutdownFunctions.push(aFunc);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIObserver
+  observe: function PU_observe(aSubject, aTopic, aData)
+  {
     if (aTopic == this.TOPIC_SHUTDOWN) {
-      if (gHasAnnotationsObserver)
-        this.annotations.removeObserver(this);
-
-      if (Object.getOwnPropertyDescriptor(this, "transactionManager").value !== undefined) {
-        // Clear all references to local transactions in the transaction manager,
-        // this prevents from leaking it.
-        this.transactionManager.RemoveListener(this);
-        this.transactionManager.clear();
-      }
-
       Services.obs.removeObserver(this, this.TOPIC_SHUTDOWN);
-      gHasShutdownObserver = false;
+      this._shutdownFunctions.forEach(function (aFunc) aFunc.apply(this), this);
     }
   },
 
-
   //////////////////////////////////////////////////////////////////////////////
   //// nsIAnnotationObserver
 
   onItemAnnotationSet: function PU_onItemAnnotationSet(aItemId, aAnnotationName)
   {
     if (aAnnotationName == this.READ_ONLY_ANNO &&
         this._readOnly.indexOf(aItemId) == -1)
       this._readOnly.push(aItemId);
@@ -2043,16 +2036,73 @@ var PlacesUtils = {
 
       if (newBackupFile.exists())
         return;
 
       this.saveBookmarksToJSONFile(newBackupFile);
     }
 
   },
+
+  /**
+   * Given a uri returns list of itemIds associated to it.
+   *
+   * @param aURI
+   *        nsIURI or spec of the page.
+   * @param aCallback
+   *        Function to be called when done.
+   *        The function will receive an array of itemIds associated to aURI.
+   * @param aScope
+   *        Scope for the callback.
+   *
+   * @note Children of live bookmarks folders are excluded.
+   */
+  asyncGetBookmarkIds: function PU_asyncGetBookmarkIds(aURI, aCallback, aScope)
+  {
+    if (!this._asyncGetBookmarksStmt) {
+      let db = this.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+      this._asyncGetBookmarksStmt = db.createAsyncStatement(
+        "SELECT b.id "
+      + "FROM moz_bookmarks b "
+      + "JOIN moz_places h on h.id = b.fk "
+      + "WHERE h.url = :url "
+      +   "AND NOT EXISTS( "
+      +     "SELECT 1 FROM moz_items_annos a "
+      +     "JOIN moz_anno_attributes n ON a.anno_attribute_id = n.id "
+      +     "WHERE a.item_id = b.parent AND n.name = :name "
+      +   ") "
+      );
+      this.registerShutdownFunction(function () {
+        this._asyncGetBookmarksStmt.finalize();
+      });
+    }
+
+    let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+    this._asyncGetBookmarksStmt.params.url = url;
+    this._asyncGetBookmarksStmt.params.name = this.LMANNO_FEEDURI;
+    this._asyncGetBookmarksStmt.executeAsync({
+      _itemIds: [],
+      handleResult: function(aResultSet) {
+        let row, haveMatches = false;
+        for (let row; (row = aResultSet.getNextRow());) {
+          this._itemIds.push(row.getResultByIndex(0));
+        }
+      },
+      handleError: function(aError) {
+        Cu.reportError("Async statement execution returned (" + aError.result +
+                       "): " + aError.message);
+      },
+      handleCompletion: function(aReason)
+      {
+        if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) {
+          aCallback.apply(aScope, [this._itemIds]);
+        }
+      }
+    });
+  }
 };
 
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "history",
                                    "@mozilla.org/browser/nav-history-service;1",
                                    "nsINavHistoryService");
 
 XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
   return PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
@@ -2086,28 +2136,25 @@ XPCOMUtils.defineLazyServiceGetter(Place
                                    "@mozilla.org/browser/livemark-service;2",
                                    "nsILivemarkService");
 
 XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "microsummaries",
                                    "@mozilla.org/microsummary/service;1",
                                    "nsIMicrosummaryService");
 
 XPCOMUtils.defineLazyGetter(PlacesUtils, "transactionManager", function() {
-  Services.obs.addObserver(PlacesUtils,
-                           PlacesUtils.TOPIC_SHUTDOWN,
-                           false);
-  // Observe shutdown, so we can remove the anno observer.
-  if (!gHasShutdownObserver) {
-    Services.obs.addObserver(PlacesUtils, PlacesUtils.TOPIC_SHUTDOWN, false);
-    gHasShutdownObserver = true;
-  }
-
   let tm = Cc["@mozilla.org/transactionmanager;1"].
            getService(Ci.nsITransactionManager);
   tm.AddListener(PlacesUtils);
+  this.registerShutdownFunction(function () {
+    // Clear all references to local transactions in the transaction manager,
+    // this prevents from leaking it.
+    this.transactionManager.RemoveListener(this);
+    this.transactionManager.clear();
+  });
   return tm;
 });
 
 XPCOMUtils.defineLazyGetter(this, "bundle", function() {
   const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties";
   return Cc["@mozilla.org/intl/stringbundle;1"].
          getService(Ci.nsIStringBundleService).
          createBundle(PLACES_STRING_BUNDLE_URI);
--- a/toolkit/components/places/src/nsTaggingService.js
+++ b/toolkit/components/places/src/nsTaggingService.js
@@ -188,17 +188,17 @@ TaggingService.prototype = {
     let taggingService = this;
     PlacesUtils.bookmarks.runInBatchMode({
       runBatched: function (aUserData)
       {
         tags.forEach(function (tag)
         {
           if (tag.id == -1) {
             // Tag does not exist yet, create it.
-            tag.id = this._createTag(tag.name);
+            this._createTag(tag.name);
           }
 
           if (this._getItemIdForTaggedURI(aURI, tag.name) == -1) {
             // The provided URI is not yet tagged, add a tag for it.
             // Note that bookmarks under tag containers must have null titles.
             PlacesUtils.bookmarks.insertBookmark(
               tag.id, aURI, PlacesUtils.bookmarks.DEFAULT_INDEX, null
             );