Merge m-c to b2g-inbound a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 04 Feb 2015 17:48:54 -0800
changeset 227537 2037891bcdc84fdacd7e9b93f1b9cedc5f73b804
parent 227536 4f1b69be79ddda8398d640e6bbd30f05cd5a43c7 (current diff)
parent 227534 34a66aaaca81826f0b838016aa15a077b50fa2fc (diff)
child 227538 a2b89c1b4c47b4c8518ed0f16c8c578a21070bdc
push id28233
push usercbook@mozilla.com
push dateThu, 05 Feb 2015 13:22:30 +0000
treeherdermozilla-central@d2df44094059 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b2g-inbound a=merge
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -290,35 +290,36 @@ let gFxAccounts = {
     }
     let note = null;
     switch (this._migrationInfo.state) {
       case this.fxaMigrator.STATE_USER_FXA: {
         // There are 2 cases here - no email address means it is an offer on
         // the first device (so the user is prompted to create an account).
         // If there is an email address it is the "join the party" flow, so the
         // user is prompted to sign in with the address they previously used.
-        let msg, upgradeLabel, upgradeAccessKey;
+        let msg, upgradeLabel, upgradeAccessKey, learnMoreLink;
         if (this._migrationInfo.email) {
           msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
                                                   [this._migrationInfo.email],
                                                   1);
           upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label");
           upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey");
         } else {
           msg = this.strings.GetStringFromName("needUserLong");
           upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label");
           upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey");
+          learnMoreLink = this.fxaMigrator.learnMoreLink;
         }
         note = new Weave.Notification(
           undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [
             new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => {
               this._expectingNotifyClose = true;
               this.fxaMigrator.createFxAccount(window);
             }),
-          ]
+          ], learnMoreLink
         );
         break;
       }
       case this.fxaMigrator.STATE_USER_FXA_VERIFIED: {
         let msg =
           this.strings.formatStringFromName("needVerifiedUserLong",
                                             [this._migrationInfo.email], 1);
         let resendLabel =
--- a/browser/base/content/sync/notification.xml
+++ b/browser/base/content/sync/notification.xml
@@ -68,16 +68,28 @@
         <parameter name="notification"/>
         <body><![CDATA[
           var node = this.appendNotification(notification.description,
                                              notification.title,
                                              notification.iconURL,
                                              notification.priority,
                                              notification.buttons);
           node.notification = notification;
+
+          if (notification.link) {
+            let XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+            let link = node.ownerDocument.createElementNS(XULNS, "label");
+            link.className = "text-link";
+            link.setAttribute("value", notification.link.text);
+            link.href = notification.link.href;
+            let desc = node.ownerDocument.getAnonymousElementByAttribute(
+              node, "anonid", "messageText"
+            );
+            desc.appendChild(link);
+          }
         ]]></body>
       </method>
 
     </implementation>
   </binding>
 
   <binding id="notification" extends="chrome://global/content/bindings/notification.xml#notification">
     <content>
--- a/browser/components/places/tests/unit/head_bookmarks.js
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -7,66 +7,102 @@ const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/LoadContextInfo.jsm");
 
 // Import common head.
-let (commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false)) {
+let commonFile = do_get_file("../../../../../toolkit/components/places/tests/head_common.js", false);
+if (commonFile) {
   let uri = Services.io.newFileURI(commonFile);
   Services.scriptloader.loadSubScript(uri.spec, this);
 }
 
 // Put any other stuff relative to this test folder below.
 
-
 XPCOMUtils.defineLazyGetter(this, "PlacesUIUtils", function() {
   Cu.import("resource:///modules/PlacesUIUtils.jsm");
   return PlacesUIUtils;
 });
 
-
 const ORGANIZER_FOLDER_ANNO = "PlacesOrganizer/OrganizerFolder";
 const ORGANIZER_QUERY_ANNO = "PlacesOrganizer/OrganizerQuery";
 
-
 // Needed by some test that relies on having an app  registered.
-let (XULAppInfo = {
+let XULAppInfo = {
   vendor: "Mozilla",
   name: "PlacesTest",
   ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
   version: "1",
   appBuildID: "2007010101",
   platformVersion: "",
   platformBuildID: "2007010101",
   inSafeMode: false,
   logConsoleErrors: true,
   OS: "XPCShell",
   XPCOMABI: "noarch-spidermonkey",
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIXULAppInfo,
     Ci.nsIXULRuntime,
   ])
-}) {
-  let XULAppInfoFactory = {
-    createInstance: function (outer, iid) {
-      if (outer != null)
-        throw Cr.NS_ERROR_NO_AGGREGATION;
-      return XULAppInfo.QueryInterface(iid);
-    }
-  };
-  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-  registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
-                            "XULAppInfo", "@mozilla.org/xre/app-info;1",
-                            XULAppInfoFactory);
-}
+};
+
+let XULAppInfoFactory = {
+  createInstance: function (outer, iid) {
+    if (outer != null)
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    return XULAppInfo.QueryInterface(iid);
+  }
+};
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
+                          "XULAppInfo", "@mozilla.org/xre/app-info;1",
+                          XULAppInfoFactory);
 
 // Smart bookmarks constants.
 const SMART_BOOKMARKS_VERSION = 7;
 const SMART_BOOKMARKS_ON_TOOLBAR = 1;
 const SMART_BOOKMARKS_ON_MENU =  3; // Takes into account the additional separator.
 
 // Default bookmarks constants.
 const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1;
 const DEFAULT_BOOKMARKS_ON_MENU = 1;
+
+const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
+
+function checkItemHasAnnotation(guid, name) {
+  return PlacesUtils.promiseItemId(guid).then(id => {
+    let hasAnnotation = PlacesUtils.annotations.itemHasAnnotation(id, name);
+    Assert.ok(hasAnnotation, `Expected annotation ${name}`);
+  });
+}
+
+function waitForImportAndSmartBookmarks() {
+  return Promise.all([
+    promiseTopicObserved("bookmarks-restore-success"),
+    PlacesTestUtils.promiseAsyncUpdates()
+  ]);
+}
+
+function promiseEndUpdateBatch() {
+  return new Promise(resolve => {
+    PlacesUtils.bookmarks.addObserver({
+      __proto__: NavBookmarkObserver.prototype,
+      onEndUpdateBatch: resolve
+    }, false);
+  });
+}
+
+let createCorruptDB = Task.async(function* () {
+  let dbPath = OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+  yield OS.File.remove(dbPath);
+
+  // Create a corrupt database.
+  let dir = yield OS.File.getCurrentDirectory();
+  let src = OS.Path.join(dir, "corruptDB.sqlite");
+  yield OS.File.copy(src, dbPath);
+
+  // Check there's a DB now.
+  Assert.ok((yield OS.File.exists(dbPath)), "should have a DB now");
+});
--- a/browser/components/places/tests/unit/test_421483.js
+++ b/browser/components/places/tests/unit/test_421483.js
@@ -1,81 +1,104 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 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 SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
 
 let gluesvc = Cc["@mozilla.org/browser/browserglue;1"].
                 getService(Ci.nsIBrowserGlue).
                 QueryInterface(Ci.nsIObserver);
 // Avoid default bookmarks import.
 gluesvc.observe(null, "initial-migration-will-import-default-bookmarks", "");
 
 function run_test() {
   run_next_test();
 }
 
-add_task(function smart_bookmarks_disabled() {
+add_task(function* smart_bookmarks_disabled() {
   Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
+
   let smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_eq(smartBookmarkItemIds.length, 0);
+  Assert.equal(smartBookmarkItemIds.length, 0);
+
   do_print("check that pref has not been bumped up");
-  do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
+  Assert.equal(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
 });
 
-add_task(function create_smart_bookmarks() {
+add_task(function* create_smart_bookmarks() {
   Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
+
   let smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_neq(smartBookmarkItemIds.length, 0);
+  Assert.notEqual(smartBookmarkItemIds.length, 0);
+
   do_print("check that pref has been bumped up");
-  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+  Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
 });
 
-add_task(function remove_smart_bookmark_and_restore() {
+add_task(function* remove_smart_bookmark_and_restore() {
   let smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
   let smartBookmarksCount = smartBookmarkItemIds.length;
   do_print("remove one smart bookmark and restore");
-  PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]);
+
+  let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+  yield PlacesUtils.bookmarks.remove(guid);
   Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+
   gluesvc.ensurePlacesDefaultQueriesInitialized();
   smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
+  Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+
   do_print("check that pref has been bumped up");
-  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+  Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
 });
 
-add_task(function move_smart_bookmark_rename_and_restore() {
+add_task(function* move_smart_bookmark_rename_and_restore() {
   let smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
   let smartBookmarksCount = smartBookmarkItemIds.length;
   do_print("smart bookmark should be restored in place");
-  let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]);
-  let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]);
+
+  let guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+  let bm = yield PlacesUtils.bookmarks.fetch(guid);
+  let oldTitle = bm.title;
+
   // create a subfolder and move inside it
-  let newParent =
-    PlacesUtils.bookmarks.createFolder(parent, "test",
-                                       PlacesUtils.bookmarks.DEFAULT_INDEX);
-  PlacesUtils.bookmarks.moveItem(smartBookmarkItemIds[0], newParent,
-                                 PlacesUtils.bookmarks.DEFAULT_INDEX);
-  // change title
-  PlacesUtils.bookmarks.setItemTitle(smartBookmarkItemIds[0], "new title");
+  let subfolder = yield PlacesUtils.bookmarks.insert({
+    parentGuid: bm.parentGuid,
+    title: "test",
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+    type: PlacesUtils.bookmarks.TYPE_FOLDER
+  });
+
+  // change title and move into new subfolder
+  yield PlacesUtils.bookmarks.update({
+    guid: guid,
+    parentGuid: subfolder.guid,
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+    title: "new title"
+  });
+
   // restore
   Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
   gluesvc.ensurePlacesDefaultQueriesInitialized();
+
   smartBookmarkItemIds =
     PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
-  do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
-  do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
+  Assert.equal(smartBookmarkItemIds.length, smartBookmarksCount);
+
+  guid = yield PlacesUtils.promiseItemGuid(smartBookmarkItemIds[0]);
+  bm = yield PlacesUtils.bookmarks.fetch(guid);
+  Assert.equal(bm.parentGuid, subfolder.guid);
+  Assert.equal(bm.title, oldTitle);
+
   do_print("check that pref has been bumped up");
-  do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
+  Assert.ok(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
 });
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt.js
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
@@ -4,83 +4,56 @@
  * 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/. */
 
 /**
  * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
  * database is corrupt and one backup is available.
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "bs",
-                                   "@mozilla.org/browser/nav-bookmarks-service;1",
-                                   "nsINavBookmarksService");
-XPCOMUtils.defineLazyServiceGetter(this, "anno",
-                                   "@mozilla.org/browser/annotation-service;1",
-                                   "nsIAnnotationService");
-
-let bookmarksObserver = {
-  onBeginUpdateBatch: function() {},
-  onEndUpdateBatch: function() {
-    let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
-    do_check_neq(itemId, -1);
-    if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
-      continue_test();
-  },
-  onItemAdded: function() {},
-  onItemRemoved: function(id, folder, index, itemType) {},
-  onItemChanged: function() {},
-  onItemVisited: function(id, visitID, time) {},
-  onItemMoved: function() {},
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
-};
-
 function run_test() {
-  do_test_pending();
-
-  // Create our bookmarks.html copying bookmarks.glue.html to the profile
-  // folder.  It should be ignored.
+  // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
 
-  // Create our JSON backup copying bookmarks.glue.json to the profile folder.
   remove_all_JSON_backups();
+
+  // Create our JSON backup from bookmarks.glue.json.
   create_JSON_backup("bookmarks.glue.json");
 
-  // Remove current database file.
-  let db = gProfD.clone();
-  db.append("places.sqlite");
-  if (db.exists()) {
-    db.remove(false);
-    do_check_false(db.exists());
-  }
+  run_next_test();
+}
+
+do_register_cleanup(function () {
+  remove_bookmarks_html();
+  remove_all_JSON_backups();
+  return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_main() {
   // Create a corrupt database.
-  let corruptDB = gTestDir.clone();
-  corruptDB.append("corruptDB.sqlite");
-  corruptDB.copyTo(gProfD, "places.sqlite");
-  do_check_true(db.exists());
+  yield createCorruptDB();
 
   // Initialize nsBrowserGlue before Places.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue);
 
-  // Initialize Places through the History Service.
-  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
-           getService(Ci.nsINavHistoryService);
   // Check the database was corrupt.
   // nsBrowserGlue uses databaseStatus to manage initialization.
-  do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CORRUPT);
 
   // The test will continue once restore has finished and smart bookmarks
   // have been created.
-  bs.addObserver(bookmarksObserver, false);
-}
+  yield promiseEndUpdateBatch();
 
-function continue_test() {
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
   // Check that JSON backup has been restored.
   // Notice restore from JSON notification is fired before smart bookmarks creation.
-  let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
-  do_check_eq(bs.getItemTitle(itemId), "examplejson");
-
-  remove_bookmarks_html();
-  remove_all_JSON_backups();
-
-  do_test_finished();
-}
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(bm.title, "examplejson");
+});
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
@@ -4,78 +4,49 @@
  * 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/. */
 
 /**
  * Tests that nsBrowserGlue correctly imports from bookmarks.html if database
  * is corrupt but a JSON backup is not available.
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "bs",
-                                   "@mozilla.org/browser/nav-bookmarks-service;1",
-                                   "nsINavBookmarksService");
-XPCOMUtils.defineLazyServiceGetter(this, "anno",
-                                   "@mozilla.org/browser/annotation-service;1",
-                                   "nsIAnnotationService");
+function run_test() {
+  // Create our bookmarks.html from bookmarks.glue.html.
+  create_bookmarks_html("bookmarks.glue.html");
 
-let bookmarksObserver = {
-  onBeginUpdateBatch: function() {},
-  onEndUpdateBatch: function() {
-    let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
-    do_check_neq(itemId, -1);
-    if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
-      continue_test();
-  },
-  onItemAdded: function() {},
-  onItemRemoved: function(id, folder, index, itemType) {},
-  onItemChanged: function() {},
-  onItemVisited: function(id, visitID, time) {},
-  onItemMoved: function() {},
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
-};
-
-function run_test() {
-  do_test_pending();
-
-  // Create bookmarks.html in the profile.
-  create_bookmarks_html("bookmarks.glue.html");
   // Remove JSON backup from profile.
   remove_all_JSON_backups();
 
-  // Remove current database file.
-  let db = gProfD.clone();
-  db.append("places.sqlite");
-  if (db.exists()) {
-    db.remove(false);
-    do_check_false(db.exists());
-  }
+  run_next_test();
+}
+
+do_register_cleanup(remove_bookmarks_html);
+
+add_task(function* () {
   // Create a corrupt database.
-  let corruptDB = gTestDir.clone();
-  corruptDB.append("corruptDB.sqlite");
-  corruptDB.copyTo(gProfD, "places.sqlite");
-  do_check_true(db.exists());
+  yield createCorruptDB();
 
   // Initialize nsBrowserGlue before Places.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue);
 
-  // Initialize Places through the History Service.
-  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
-           getService(Ci.nsINavHistoryService);
   // Check the database was corrupt.
   // nsBrowserGlue uses databaseStatus to manage initialization.
-  do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CORRUPT);
 
   // The test will continue once import has finished and smart bookmarks
   // have been created.
-  bs.addObserver(bookmarksObserver, false);
-}
+  yield promiseEndUpdateBatch();
 
-function continue_test() {
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
   // Check that bookmarks html has been restored.
-  let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
-  do_check_eq(bs.getItemTitle(itemId), "example");
-
-  remove_bookmarks_html();
-
-  do_test_finished();
-}
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(bm.title, "example");
+});
--- a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
@@ -4,77 +4,47 @@
  * 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/. */
 
 /**
  * Tests that nsBrowserGlue correctly restores default bookmarks if database is
  * corrupt, nor a JSON backup nor bookmarks.html are available.
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "bs",
-                                   "@mozilla.org/browser/nav-bookmarks-service;1",
-                                   "nsINavBookmarksService");
-XPCOMUtils.defineLazyServiceGetter(this, "anno",
-                                   "@mozilla.org/browser/annotation-service;1",
-                                   "nsIAnnotationService");
-
-let bookmarksObserver = {
-  onBeginUpdateBatch: function() {},
-  onEndUpdateBatch: function() {
-    let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
-    do_check_neq(itemId, -1);
-    if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
-      continue_test();
-  },
-  onItemAdded: function() {},
-  onItemRemoved: function(id, folder, index, itemType) {},
-  onItemChanged: function() {},
-  onItemVisited: function(id, visitID, time) {},
-  onItemMoved: function() {},
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
-};
-
 function run_test() {
-  do_test_pending();
-
   // Remove bookmarks.html from profile.
   remove_bookmarks_html();
+
   // Remove JSON backup from profile.
   remove_all_JSON_backups();
 
-  // Remove current database file.
-  let db = gProfD.clone();
-  db.append("places.sqlite");
-  if (db.exists()) {
-    db.remove(false);
-    do_check_false(db.exists());
-  }
+  run_next_test();
+}
+
+add_task(function* () {
   // Create a corrupt database.
-  let corruptDB = gTestDir.clone();
-  corruptDB.append("corruptDB.sqlite");
-  corruptDB.copyTo(gProfD, "places.sqlite");
-  do_check_true(db.exists());
+  yield createCorruptDB();
 
   // Initialize nsBrowserGlue before Places.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue);
 
-  // Initialize Places through the History Service.
-  let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
-           getService(Ci.nsINavHistoryService);
   // Check the database was corrupt.
   // nsBrowserGlue uses databaseStatus to manage initialization.
-  do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CORRUPT);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CORRUPT);
 
   // The test will continue once import has finished and smart bookmarks
   // have been created.
-  bs.addObserver(bookmarksObserver, false);
-}
+  yield promiseEndUpdateBatch();
 
-function continue_test() {
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
   // Check that default bookmarks have been restored.
-  let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
-  do_check_true(itemId > 0);
-  do_check_eq(bs.getItemTitle(itemId), "Getting Started");
-
-  do_test_finished();
-}
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  do_check_eq(bm.title, "Getting Started");
+});
--- a/browser/components/places/tests/unit/test_browserGlue_distribution.js
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -8,98 +8,90 @@
 const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
 const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed";
 const PREF_DISTRIBUTION_ID = "distribution.id";
 
 const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
 const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete";
 const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
 
-function run_test()
-{
-  do_test_pending();
-
+function run_test() {
   // Set special pref to load distribution.ini from the profile folder.
   Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
+
   // Copy distribution.ini file to the profile dir.
   let distroDir = gProfD.clone();
   distroDir.leafName = "distribution";
   let iniFile = distroDir.clone();
   iniFile.append("distribution.ini");
   if (iniFile.exists()) {
     iniFile.remove(false);
     print("distribution.ini already exists, did some test forget to cleanup?");
   }
+
   let testDistributionFile = gTestDir.clone();
   testDistributionFile.append("distribution.ini");
   testDistributionFile.copyTo(distroDir, "distribution.ini");
-  do_check_true(testDistributionFile.exists());
+  Assert.ok(testDistributionFile.exists());
+
+  run_next_test();
+}
 
+do_register_cleanup(function () {
+  // Remove the distribution file, even if the test failed, otherwise all
+  // next tests will import it.
+  let iniFile = gProfD.clone();
+  iniFile.leafName = "distribution";
+  iniFile.append("distribution.ini");
+  if (iniFile.exists()) {
+    iniFile.remove(false);
+  }
+  Assert.ok(!iniFile.exists());
+});
+
+add_task(function* () {
   // Disable Smart Bookmarks creation.
   Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
 
   // Initialize Places through the History Service and check that a new
   // database has been created.
-  do_check_eq(PlacesUtils.history.databaseStatus,
-              PlacesUtils.history.DATABASE_STATUS_CREATE);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
 
   // Force distribution.
-  Cc["@mozilla.org/browser/browserglue;1"].
-  getService(Ci.nsIObserver).observe(null,
-                                     TOPIC_BROWSERGLUE_TEST,
-                                     TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
+  let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver)
+  glue.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_DISTRIBUTION_CUSTOMIZATION);
 
   // Test will continue on customization complete notification.
-  Services.obs.addObserver(function(aSubject, aTopic, aData) {
-    Services.obs.removeObserver(arguments.callee,
-                                TOPIC_CUSTOMIZATION_COMPLETE,
-                                false);
-    do_execute_soon(onCustomizationComplete);
-  }, TOPIC_CUSTOMIZATION_COMPLETE, false);
-}
+  yield promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE);
 
-function onCustomizationComplete()
-{
   // Check the custom bookmarks exist on menu.
-  let menuItemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
-  do_check_neq(menuItemId, -1);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(menuItemId),
-              "Menu Link Before");
-  menuItemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
-                                         1 + DEFAULT_BOOKMARKS_ON_MENU);
-  do_check_neq(menuItemId, -1);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(menuItemId),
-              "Menu Link After");
+  let menuItem = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  });
+  Assert.equal(menuItem.title, "Menu Link Before");
+
+  menuItem = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 1 + DEFAULT_BOOKMARKS_ON_MENU
+  });
+  Assert.equal(menuItem.title, "Menu Link After");
 
   // Check the custom bookmarks exist on toolbar.
-  let toolbarItemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-  do_check_neq(toolbarItemId, -1);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(toolbarItemId),
-              "Toolbar Link Before");
-  toolbarItemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                         1 + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-  do_check_neq(toolbarItemId, -1);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(toolbarItemId),
-              "Toolbar Link After");
+  let toolbarItem = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  Assert.equal(toolbarItem.title, "Toolbar Link Before");
+
+  toolbarItem = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(toolbarItem.title, "Toolbar Link After");
 
   // Check the bmprocessed pref has been created.
-  do_check_true(Services.prefs.getBoolPref(PREF_BMPROCESSED));
+  Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED));
 
   // Check distribution prefs have been created.
-  do_check_eq(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
-
-  do_test_finished();
-}
-
-do_register_cleanup(function() {
-  // Remove the distribution file, even if the test failed, otherwise all
-  // next tests will import it.
-  let iniFile = gProfD.clone();
-  iniFile.leafName = "distribution";
-  iniFile.append("distribution.ini");
-  if (iniFile.exists())
-    iniFile.remove(false);
-  do_check_false(iniFile.exists());
+  Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
 });
--- a/browser/components/places/tests/unit/test_browserGlue_migrate.js
+++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js
@@ -5,84 +5,66 @@
  * Tests that nsBrowserGlue does not overwrite bookmarks imported from the
  * migrators.  They usually run before nsBrowserGlue, so if we find any
  * bookmark on init, we should not try to import.
  */
 
 const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
 
 function run_test() {
-  do_test_pending();
-
-  // Create our bookmarks.html copying bookmarks.glue.html to the profile
-  // folder.  It should be ignored.
+  // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
 
   // Remove current database file.
-  let db = gProfD.clone();
-  db.append("places.sqlite");
-  if (db.exists()) {
-    db.remove(false);
-    do_check_false(db.exists());
-  }
+  clearDB();
 
+  run_next_test();
+}
+
+do_register_cleanup(remove_bookmarks_html);
+
+add_task(function* test_migrate_bookmarks() {
   // Initialize Places through the History Service and check that a new
   // database has been created.
-  do_check_eq(PlacesUtils.history.databaseStatus,
-              PlacesUtils.history.DATABASE_STATUS_CREATE);
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
 
   // A migrator would run before nsBrowserGlue Places initialization, so mimic
   // that behavior adding a bookmark and notifying the migration.
-  let bg = Cc["@mozilla.org/browser/browserglue;1"].
-           getService(Ci.nsIObserver);
-
+  let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
   bg.observe(null, "initial-migration-will-import-default-bookmarks", null);
 
-  PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarks.bookmarksMenuFolder, uri("http://mozilla.org/"),
-                    PlacesUtils.bookmarks.DEFAULT_INDEX, "migrated");
+  yield PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+    type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+    url: "http://mozilla.org/",
+    title: "migrated"
+  });
+
+  let promise = promiseEndUpdateBatch();
+  bg.observe(null, "initial-migration-did-import-default-bookmarks", null);
+  yield promise;
 
-  let bookmarksObserver = {
-    onBeginUpdateBatch: function() {},
-    onEndUpdateBatch: function() {
-      // Check if the currently finished batch created the smart bookmarks.
-      let itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-      do_check_neq(itemId, -1);
-      if (PlacesUtils.annotations
-                     .itemHasAnnotation(itemId, "Places/SmartBookmark")) {
-        do_execute_soon(onSmartBookmarksCreation);
-      }
-    },
-    onItemAdded: function() {},
-    onItemRemoved: function(id, folder, index, itemType) {},
-    onItemChanged: function() {},
-    onItemVisited: function(id, visitID, time) {},
-    onItemMoved: function() {},
-    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
-  };
-  // The test will continue once import has finished and smart bookmarks
-  // have been created.
-  PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
 
-  bg.observe(null, "initial-migration-did-import-default-bookmarks", null);
-}
-
-function onSmartBookmarksCreation() {
-  // Check the created bookmarks still exist.
-  let itemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
-                                         SMART_BOOKMARKS_ON_MENU);
-  do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "migrated");
+  // Check the created bookmark still exists.
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: SMART_BOOKMARKS_ON_MENU
+  });
+  Assert.equal(bm.title, "migrated");
 
   // Check that we have not imported any new bookmark.
-  itemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
-                                         SMART_BOOKMARKS_ON_MENU + 1)
-  do_check_eq(itemId, -1);
-  itemId =
-    PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                         SMART_BOOKMARKS_ON_MENU)
-  do_check_eq(itemId, -1);
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: SMART_BOOKMARKS_ON_MENU + 1
+  })));
 
-  remove_bookmarks_html();
-
-  do_test_finished();
-}
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_MENU
+  })));
+});
--- a/browser/components/places/tests/unit/test_browserGlue_prefs.js
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -10,274 +10,233 @@ const PREF_IMPORT_BOOKMARKS_HTML = "brow
 const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
 const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
 const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
 
 const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
 const TOPICDATA_FORCE_PLACES_INIT = "force-places-init";
 
 let bg = Cc["@mozilla.org/browser/browserglue;1"].
-         getService(Ci.nsIBrowserGlue);
-
-function waitForImportAndSmartBookmarks(aCallback) {
-  Services.obs.addObserver(function waitImport() {
-    Services.obs.removeObserver(waitImport, "bookmarks-restore-success");
-    // Delay to test eventual smart bookmarks creation.
-    do_execute_soon(function () {
-      PlacesTestUtils.promiseAsyncUpdates().then(aCallback);
-    });
-  }, "bookmarks-restore-success", false);
-}
-
-[
-
-  // This test must be the first one.
-  function test_checkPreferences() {
-    // Initialize Places through the History Service and check that a new
-    // database has been created.
-    do_check_eq(PlacesUtils.history.databaseStatus,
-                PlacesUtils.history.DATABASE_STATUS_CREATE);
-
-    // Wait for Places init notification.
-    Services.obs.addObserver(function(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(arguments.callee,
-                                  "places-browser-init-complete");
-      do_execute_soon(function () {
-        // Ensure preferences status.
-        do_check_false(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
-
-        try {
-          do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-          do_throw("importBookmarksHTML pref should not exist");
-        }
-        catch(ex) {}
-
-        try {
-          do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
-          do_throw("importBookmarksHTML pref should not exist");
-        }
-        catch(ex) {}
-
-        run_next_test();
-      });
-    }, "places-browser-init-complete", false);
-  },
-
-  function test_import()
-  {
-    do_print("Import from bookmarks.html if importBookmarksHTML is true.");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
-
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been imported, and a smart bookmark has been
-      // created.
-      itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                                    SMART_BOOKMARKS_ON_TOOLBAR);
-      do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example");
-      // Check preferences have been reverted.
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces().
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-  },
-
-  function test_import_noSmartBookmarks()
-  {
-    do_print("import from bookmarks.html, but don't create smart bookmarks \
-              if they are disabled");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
-    Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
-
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been imported, but smart bookmarks have not
-      // been created.
-      itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-      do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example");
-      // Check preferences have been reverted.
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces().
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-  },
-
-  function test_import_autoExport_updatedSmartBookmarks()
-  {
-    do_print("Import from bookmarks.html, but don't create smart bookmarks \
-              if autoExportHTML is true and they are at latest version");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999);
-    Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
-    Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+         getService(Ci.nsIObserver);
 
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been imported, but smart bookmarks have not
-      // been created.
-      itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-      do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example");
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-      // Check preferences have been reverted.
-      Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces()
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-  },
-
-  function test_import_autoExport_oldSmartBookmarks()
-  {
-    do_print("Import from bookmarks.html, and create smart bookmarks if \
-              autoExportHTML is true and they are not at latest version.");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
-    Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
-    Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
-
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been imported, but smart bookmarks have not
-      // been created.
-      itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                             SMART_BOOKMARKS_ON_TOOLBAR);
-      do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "example");
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-      // Check preferences have been reverted.
-      Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces()
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-  },
-
-  function test_restore()
-  {
-    do_print("restore from default bookmarks.html if \
-              restore_default_bookmarks is true.");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
-
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been restored.
-      itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                             SMART_BOOKMARKS_ON_TOOLBAR);
-      do_check_true(itemId > 0);
-      // Check preferences have been reverted.
-      do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces()
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-
-  },
-
-  function test_restore_import()
-  {
-    do_print("setting both importBookmarksHTML and \
-              restore_default_bookmarks should restore defaults.");
-
-    remove_all_bookmarks();
-    // Sanity check: we should not have any bookmark on the toolbar.
-    let itemId =
-      PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_eq(itemId, -1);
-
-    // Set preferences.
-    Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
-    Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
-
-    waitForImportAndSmartBookmarks(function () {
-      // Check bookmarks.html has been restored.
-      itemId =
-        PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId,
-                                             SMART_BOOKMARKS_ON_TOOLBAR);
-      do_check_true(itemId > 0);
-      // Check preferences have been reverted.
-      do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-
-      run_next_test();
-    });
-    // Force nsBrowserGlue::_initPlaces()
-    do_print("Simulate Places init");
-    bg.QueryInterface(Ci.nsIObserver).observe(null,
-                                              TOPIC_BROWSERGLUE_TEST,
-                                              TOPICDATA_FORCE_PLACES_INIT);
-  }
-
-].forEach(add_test);
-
-do_register_cleanup(function () {
-  remove_all_bookmarks();
-  remove_bookmarks_html();
-  remove_all_JSON_backups();
-});
-
-function run_test()
-{
+function run_test() {
   // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
+
   remove_all_JSON_backups();
+
   // Create our JSON backup from bookmarks.glue.json.
   create_JSON_backup("bookmarks.glue.json");
 
   run_next_test();
 }
+
+do_register_cleanup(function () {
+  remove_bookmarks_html();
+  remove_all_JSON_backups();
+
+  return PlacesUtils.bookmarks.eraseEverything();
+});
+
+function simulatePlacesInit() {
+  do_print("Simulate Places init");
+  let promise = waitForImportAndSmartBookmarks();
+
+  // Force nsBrowserGlue::_initPlaces().
+  bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT);
+  return promise;
+}
+
+add_task(function* test_checkPreferences() {
+  // Initialize Places through the History Service and check that a new
+  // database has been created.
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_CREATE);
+
+  // Wait for Places init notification.
+  yield promiseTopicObserved("places-browser-init-complete");
+
+  // Ensure preferences status.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+
+  Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+  Assert.throws(() => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+});
+
+add_task(function* test_import() {
+  do_print("Import from bookmarks.html if importBookmarksHTML is true.");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been imported, and a smart bookmark has been
+  // created.
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(bm.title, "example");
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(function* test_import_noSmartBookmarks() {
+  do_print("import from bookmarks.html, but don't create smart bookmarks " +
+              "if they are disabled");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, -1);
+  Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been imported, but smart bookmarks have not
+  // been created.
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  Assert.equal(bm.title, "example");
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(function* test_import_autoExport_updatedSmartBookmarks() {
+  do_print("Import from bookmarks.html, but don't create smart bookmarks " +
+              "if autoExportHTML is true and they are at latest version");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 999);
+  Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+  Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been imported, but smart bookmarks have not
+  // been created.
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  Assert.equal(bm.title, "example");
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+  Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+});
+
+add_task(function* test_import_autoExport_oldSmartBookmarks() {
+  do_print("Import from bookmarks.html, and create smart bookmarks if " +
+              "autoExportHTML is true and they are not at latest version.");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+  Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, true);
+  Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been imported, but smart bookmarks have not
+  // been created.
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(bm.title, "example");
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+  Services.prefs.setBoolPref(PREF_AUTO_EXPORT_HTML, false);
+});
+
+add_task(function* test_restore() {
+  do_print("restore from default bookmarks.html if " +
+              "restore_default_bookmarks is true.");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been restored.
+  Assert.ok(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  }));
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+});
+
+add_task(function* test_restore_import() {
+  do_print("setting both importBookmarksHTML and " +
+              "restore_default_bookmarks should restore defaults.");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+
+  // Sanity check: we should not have any bookmark on the toolbar.
+  Assert.ok(!(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  })));
+
+  // Set preferences.
+  Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+  Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+  yield simulatePlacesInit();
+
+  // Check bookmarks.html has been restored.
+  Assert.ok(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  }));
+
+  // Check preferences have been reverted.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+  Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
--- a/browser/components/places/tests/unit/test_browserGlue_restore.js
+++ b/browser/components/places/tests/unit/test_browserGlue_restore.js
@@ -4,79 +4,59 @@
  * 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/. */
 
 /**
  * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
  * database has been created and one backup is available.
  */
 
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "bs",
-                                   "@mozilla.org/browser/nav-bookmarks-service;1",
-                                   "nsINavBookmarksService");
-XPCOMUtils.defineLazyServiceGetter(this, "anno",
-                                   "@mozilla.org/browser/annotation-service;1",
-                                   "nsIAnnotationService");
-
-let bookmarksObserver = {
-  onBeginUpdateBatch: function() {},
-  onEndUpdateBatch: function() {
-    let itemId = bs.getIdForItemAt(bs.toolbarFolder, 0);
-    do_check_neq(itemId, -1);
-    if (anno.itemHasAnnotation(itemId, "Places/SmartBookmark"))
-      continue_test();
-  },
-  onItemAdded: function() {},
-  onItemRemoved: function(id, folder, index, itemType) {},
-  onItemChanged: function() {},
-  onItemVisited: function(id, visitID, time) {},
-  onItemMoved: function() {},
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
-};
-
 function run_test() {
-  do_test_pending();
-
-  // Create our bookmarks.html copying bookmarks.glue.html to the profile
-  // folder.  It will be ignored.
+  // Create our bookmarks.html from bookmarks.glue.html.
   create_bookmarks_html("bookmarks.glue.html");
 
-  // Create our JSON backup copying bookmarks.glue.json to the profile
-  // folder.  It will be ignored.
   remove_all_JSON_backups();
+
+  // Create our JSON backup from bookmarks.glue.json.
   create_JSON_backup("bookmarks.glue.json");
 
   // Remove current database file.
-  let db = gProfD.clone();
-  db.append("places.sqlite");
-  if (db.exists()) {
-    db.remove(false);
-    do_check_false(db.exists());
-  }
+  clearDB();
+
+  run_next_test();
+}
 
+do_register_cleanup(function () {
+  remove_bookmarks_html();
+  remove_all_JSON_backups();
+  return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_main() {
   // Initialize nsBrowserGlue before Places.
   Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue);
 
   // Initialize Places through the History Service.
   let hs = Cc["@mozilla.org/browser/nav-history-service;1"].
            getService(Ci.nsINavHistoryService);
+
   // Check a new database has been created.
   // nsBrowserGlue uses databaseStatus to manage initialization.
-  do_check_eq(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+  Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
 
   // The test will continue once restore has finished and smart bookmarks
   // have been created.
-  bs.addObserver(bookmarksObserver, false);
-}
+  yield promiseEndUpdateBatch();
 
-function continue_test() {
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
   // Check that JSON backup has been restored.
   // Notice restore from JSON notification is fired before smart bookmarks creation.
-  let itemId = bs.getIdForItemAt(bs.toolbarFolder, SMART_BOOKMARKS_ON_TOOLBAR);
-  do_check_eq(bs.getItemTitle(itemId), "examplejson");
-
-  remove_bookmarks_html();
-  remove_all_JSON_backups();
-
-  do_test_finished();
-}
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: SMART_BOOKMARKS_ON_TOOLBAR
+  });
+  Assert.equal(bm.title, "examplejson");
+});
--- a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
+++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -9,343 +9,329 @@
  * by the user or by other components.
  */
 
 const PREF_SMART_BOOKMARKS_VERSION = "browser.places.smartBookmarksVersion";
 const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
 const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
 const PREF_RESTORE_DEFAULT_BOOKMARKS = "browser.bookmarks.restore_default_bookmarks";
 
-const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
-
-/**
- * Rebuilds smart bookmarks listening to console output to report any message or
- * exception generated when calling ensurePlacesDefaultQueriesInitialized().
- */
-function rebuildSmartBookmarks() {
-  let consoleListener = {
-    observe: function(aMsg) {
-      print("Got console message: " + aMsg.message);
-    },
-
-    QueryInterface: XPCOMUtils.generateQI([
-      Ci.nsIConsoleListener
-    ]),
-  };
-  Services.console.reset();
-  Services.console.registerListener(consoleListener);
-  Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue)
-                                          .ensurePlacesDefaultQueriesInitialized();
-  Services.console.unregisterListener(consoleListener);
+function run_test() {
+  remove_bookmarks_html();
+  remove_all_JSON_backups();
+  run_next_test();
 }
 
-
-let tests = [];
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "All smart bookmarks are created if smart bookmarks version is 0.",
-  exec: function() {
-    // Sanity check: we should have default bookmark.
-    do_check_neq(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0), -1);
-    do_check_neq(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0), -1);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "An existing smart bookmark is replaced when version changes.",
-  exec: function() {
-    // Sanity check: we have a smart bookmark on the toolbar.
-    let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_neq(itemId, -1);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-    // Change its title.
-    PlacesUtils.bookmarks.setItemTitle(itemId, "new title");
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), "new title");
-
-    // Sanity check items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check smart bookmark has been replaced, itemId has changed.
-    itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0);
-    do_check_neq(itemId, -1);
-    do_check_neq(PlacesUtils.bookmarks.getItemTitle(itemId), "new title");
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "bookmarks position is retained when version changes.",
-  exec: function() {
-    // Sanity check items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-    let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
-
-    itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-    let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check smart bookmarks are still in correct position.
-    itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), firstItemTitle);
-
-    itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId, SMART_BOOKMARKS_ANNO));
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId), secondItemTitle);
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "moved bookmarks position is retained when version changes.",
-  exec: function() {
-    // Sanity check items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    let itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO));
-    let firstItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId1);
-
-    let itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 1);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO));
-    let secondItemTitle = PlacesUtils.bookmarks.getItemTitle(itemId2);
-
-    // Move the first smart bookmark to the end of the menu.
-    PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId,
-                                   PlacesUtils.bookmarks.DEFAULT_INDEX);
-
-    do_check_eq(itemId1, PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
-                                                   PlacesUtils.bookmarks.DEFAULT_INDEX));
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check smart bookmarks are still in correct position.
-    itemId2 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId, 0);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId2, SMART_BOOKMARKS_ANNO));
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId2), secondItemTitle);
-
-    itemId1 = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.bookmarksMenuFolderId,
-                                                   PlacesUtils.bookmarks.DEFAULT_INDEX);
-    do_check_true(PlacesUtils.annotations.itemHasAnnotation(itemId1, SMART_BOOKMARKS_ANNO));
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(itemId1), firstItemTitle);
-
-    // Move back the smart bookmark to the original position.
-    PlacesUtils.bookmarks.moveItem(itemId1, PlacesUtils.bookmarksMenuFolderId, 1);
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "An explicitly removed smart bookmark should not be recreated.",
-  exec: function() {   
-    // Remove toolbar's smart bookmarks
-    PlacesUtils.bookmarks.removeItem(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.toolbarFolderId, 0));
-
-    // Sanity check items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    // We should not have recreated the smart bookmark on toolbar.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-
-//------------------------------------------------------------------------------
-
-tests.push({
-  description: "Even if a smart bookmark has been removed recreate it if version is 0.",
-  exec: function() {
-    // Sanity check items.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Set preferences.
-    Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
-
-    rebuildSmartBookmarks();
-
-    // Count items.
-    // We should not have recreated the smart bookmark on toolbar.
-    do_check_eq(countFolderChildren(PlacesUtils.toolbarFolderId),
-                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
-    do_check_eq(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
-                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
-
-    // Check version has been updated.
-    do_check_eq(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
-                SMART_BOOKMARKS_VERSION);
-
-    next_test();
-  }
-});
-//------------------------------------------------------------------------------
+do_register_cleanup(() => PlacesUtils.bookmarks.eraseEverything());
 
 function countFolderChildren(aFolderItemId) {
   let rootNode = PlacesUtils.getFolderContents(aFolderItemId).root;
   let cc = rootNode.childCount;
   // Dump contents.
   for (let i = 0; i < cc ; i++) {
     let node = rootNode.getChild(i);
     let title = PlacesUtils.nodeIsSeparator(node) ? "---" : node.title;
     print("Found child(" + i + "): " + title);
   }
   rootNode.containerOpen = false;
   return cc;
 }
 
-function next_test() {
-  if (tests.length) {
-    // Execute next test.
-    let test = tests.shift();
-    print("\nTEST: " + test.description);
-    test.exec();
-  }
-  else {
-    // Clean up database from all bookmarks.
-    remove_all_bookmarks();
-    do_test_finished();
-  }
+/**
+ * Rebuilds smart bookmarks listening to console output to report any message or
+ * exception generated when calling ensurePlacesDefaultQueriesInitialized().
+ */
+function rebuildSmartBookmarks() {
+  let consoleListener = {
+    observe: function(aMsg) {
+      do_throw("Got console message: " + aMsg.message);
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([
+      Ci.nsIConsoleListener
+    ]),
+  };
+  Services.console.reset();
+  Services.console.registerListener(consoleListener);
+  Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIBrowserGlue)
+                                          .ensurePlacesDefaultQueriesInitialized();
+  Services.console.unregisterListener(consoleListener);
 }
 
-function run_test() {
-  do_test_pending();
-
-  remove_bookmarks_html();
-  remove_all_JSON_backups();
-
+add_task(function* setup() {
   // Initialize browserGlue, but remove it's listener to places-init-complete.
   let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+
   // Initialize Places.
   PlacesUtils.history;
-  // Observes Places initialisation complete.
-  Services.obs.addObserver(function waitPlaceInitComplete() {
-    Services.obs.removeObserver(waitPlaceInitComplete, "places-browser-init-complete");
+
+  // Wait for Places init notification.
+  yield promiseTopicObserved("places-browser-init-complete");
+
+  // Ensure preferences status.
+  Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+  Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+  Assert.throws(() => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+
+  yield waitForImportAndSmartBookmarks();
+});
+
+add_task(function* test_version_0() {
+  do_print("All smart bookmarks are created if smart bookmarks version is 0.");
+
+  // Sanity check: we should have default bookmark.
+  Assert.ok(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  }));
+
+  Assert.ok(yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  }));
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+  rebuildSmartBookmarks();
+
+  // Count items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+               SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change() {
+  do_print("An existing smart bookmark is replaced when version changes.");
+
+  // Sanity check: we have a smart bookmark on the toolbar.
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+
+  // Change its title.
+  yield PlacesUtils.bookmarks.update({guid: bm.guid, title: "new title"});
+  bm = yield PlacesUtils.bookmarks.fetch({guid: bm.guid});
+  Assert.equal(bm.title, "new title");
+
+  // Sanity check items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+  rebuildSmartBookmarks();
 
-    // Ensure preferences status.
-    do_check_false(Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
-    do_check_false(Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
-    try {
-      do_check_false(Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
-      do_throw("importBookmarksHTML pref should not exist");
-    }
-    catch(ex) {}
+  // Count items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check smart bookmark has been replaced, itemId has changed.
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+  Assert.notEqual(bm.title, "new title");
+
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+               SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change_pos() {
+  do_print("bookmarks position is retained when version changes.");
+
+  // Sanity check items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+  let firstItemTitle = bm.title;
+
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 1
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+  let secondItemTitle = bm.title;
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+  rebuildSmartBookmarks();
+
+  // Count items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check smart bookmarks are still in correct position.
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+  Assert.equal(bm.title, firstItemTitle);
+
+  bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 1
+  });
+  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
+  Assert.equal(bm.title, secondItemTitle);
 
-    waitForImportAndSmartBookmarks(next_test);
-  }, "places-browser-init-complete", false);
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+               SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_version_change_pos_moved() {
+  do_print("moved bookmarks position is retained when version changes.");
+
+  // Sanity check items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  let bm1 = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
+  let firstItemTitle = bm1.title;
+
+  let bm2 = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 1
+  });
+  yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
+  let secondItemTitle = bm2.title;
 
-  // Usually places init would async notify to glue, but we want to avoid
-  // randomness here, thus we fire the notification synchronously.
-  bg.observe(null, "places-init-complete", null);
-}
+  // Move the first smart bookmark to the end of the menu.
+  yield PlacesUtils.bookmarks.update({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    guid: bm1.guid,
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX
+  });
+
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX
+  });
+  Assert.equal(bm.guid, bm1.guid);
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+  rebuildSmartBookmarks();
+
+  // Count items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check smart bookmarks are still in correct position.
+  bm2 = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: 0
+  });
+  yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
+  Assert.equal(bm2.title, secondItemTitle);
+
+  bm1 = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX
+  });
+  yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
+  Assert.equal(bm1.title, firstItemTitle);
 
-function waitForImportAndSmartBookmarks(aCallback) {
-  Services.obs.addObserver(function waitImport() {
-    Services.obs.removeObserver(waitImport, "bookmarks-restore-success");
-    // Delay to test eventual smart bookmarks creation.
-    do_execute_soon(function () {
-      PlacesTestUtils.promiseAsyncUpdates().then(aCallback);
-    });
-  }, "bookmarks-restore-success", false);
-}
+  // Move back the smart bookmark to the original position.
+  yield PlacesUtils.bookmarks.update({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    guid: bm1.guid,
+    index: 1
+  });
+
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+              SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_recreation() {
+  do_print("An explicitly removed smart bookmark should not be recreated.");
+
+  // Remove toolbar's smart bookmarks
+  let bm = yield PlacesUtils.bookmarks.fetch({
+    parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+    index: 0
+  });
+  yield PlacesUtils.bookmarks.remove(bm.guid);
+
+  // Sanity check items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
+
+  rebuildSmartBookmarks();
+
+  // Count items.
+  // We should not have recreated the smart bookmark on toolbar.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+               SMART_BOOKMARKS_VERSION);
+});
+
+add_task(function* test_recreation_version_0() {
+  do_print("Even if a smart bookmark has been removed recreate it if version is 0.");
+
+  // Sanity check items.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Set preferences.
+  Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 0);
+
+  rebuildSmartBookmarks();
+
+  // Count items.
+  // We should not have recreated the smart bookmark on toolbar.
+  Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
+               SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
+  Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
+               SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
+
+  // Check version has been updated.
+  Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
+               SMART_BOOKMARKS_VERSION);
+});
--- a/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
+++ b/browser/components/places/tests/unit/test_leftpane_corruption_handling.js
@@ -3,144 +3,154 @@
 /* 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/. */
 
 /**
  * Tests that we build a working leftpane in various corruption situations.
  */
 
+function run_test() {
+  // We want empty roots.
+  remove_all_bookmarks();
+
+  // Sanity check.
+  Assert.ok(!!PlacesUIUtils);
+
+  // Check getters.
+  gLeftPaneFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "leftPaneFolderId");
+  Assert.equal(typeof(gLeftPaneFolderIdGetter.get), "function");
+  gAllBookmarksFolderIdGetter = Object.getOwnPropertyDescriptor(PlacesUIUtils, "allBookmarksFolderId");
+  Assert.equal(typeof(gAllBookmarksFolderIdGetter.get), "function");
+
+  run_next_test();
+}
+
+do_register_cleanup(remove_all_bookmarks);
+
 // Used to store the original leftPaneFolderId getter.
 let gLeftPaneFolderIdGetter;
 let gAllBookmarksFolderIdGetter;
 // Used to store the original left Pane status as a JSON string.
 let gReferenceHierarchy;
 let gLeftPaneFolderId;
-// Third party annotated folder.
-let gFolderId;
-
-// Corruption cases.
-let gTests = [
-
-  function test1() {
-    print("1. Do nothing, checks test calibration.");
-  },
-
-  function test2() {
-    print("2. Delete the left pane folder.");
-    PlacesUtils.bookmarks.removeItem(gLeftPaneFolderId);
-  },
-
-  function test3() {
-    print("3. Delete a child of the left pane folder.");
-    let id = PlacesUtils.bookmarks.getIdForItemAt(gLeftPaneFolderId, 0);
-    PlacesUtils.bookmarks.removeItem(id);
-  },
-
-  function test4() {
-    print("4. Delete AllBookmarks.");
-    PlacesUtils.bookmarks.removeItem(PlacesUIUtils.allBookmarksFolderId);
-  },
-
-  function test5() {
-    print("5. Create a duplicated left pane folder.");
-    let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
-                                                "PlacesRoot",
-                                                PlacesUtils.bookmarks.DEFAULT_INDEX);
-    PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_FOLDER_ANNO,
-                                              "PlacesRoot", 0,
-                                              PlacesUtils.annotations.EXPIRE_NEVER);
-  },
 
-  function test6() {
-    print("6. Create a duplicated left pane query.");
-    let id = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
-                                                "AllBookmarks",
-                                                PlacesUtils.bookmarks.DEFAULT_INDEX);
-    PlacesUtils.annotations.setItemAnnotation(id, ORGANIZER_QUERY_ANNO,
-                                              "AllBookmarks", 0,
-                                              PlacesUtils.annotations.EXPIRE_NEVER);
-  },
-
-  function test7() {
-    print("7. Remove the left pane folder annotation.");
-    PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId,
-                                                 ORGANIZER_FOLDER_ANNO);
-  },
-
-  function test8() {
-    print("8. Remove a left pane query annotation.");
-    PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId,
-                                                 ORGANIZER_QUERY_ANNO);
-  },
+add_task(function* () {
+  // Add a third party bogus annotated item.  Should not be removed.
+  let folder = yield PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    title: "test",
+    index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+    type: PlacesUtils.bookmarks.TYPE_FOLDER
+  });
 
-  function test9() {
-    print("9. Remove a child of AllBookmarks.");
-    let id = PlacesUtils.bookmarks.getIdForItemAt(PlacesUIUtils.allBookmarksFolderId, 0);
-    PlacesUtils.bookmarks.removeItem(id);
-  },
-
-];
-
-function run_test() {
-  // We want empty roots.
-  remove_all_bookmarks();
-
-  // Sanity check.
-  do_check_true(!!PlacesUIUtils);
-
-  // Check getters.
-  gLeftPaneFolderIdGetter = PlacesUIUtils.__lookupGetter__("leftPaneFolderId");
-  do_check_eq(typeof(gLeftPaneFolderIdGetter), "function");
-  gAllBookmarksFolderIdGetter = PlacesUIUtils.__lookupGetter__("allBookmarksFolderId");
-  do_check_eq(typeof(gAllBookmarksFolderIdGetter), "function");
-
-  // Add a third party bogus annotated item.  Should not be removed.
-  gFolderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
-                                                 "test",
-                                                 PlacesUtils.bookmarks.DEFAULT_INDEX);
-  PlacesUtils.annotations.setItemAnnotation(gFolderId, ORGANIZER_QUERY_ANNO,
+  let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+  PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO,
                                             "test", 0,
                                             PlacesUtils.annotations.EXPIRE_NEVER);
 
   // Create the left pane, and store its current status, it will be used
   // as reference value.
   gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
   gReferenceHierarchy = folderIdToHierarchy(gLeftPaneFolderId);
-  do_test_pending();
-  run_next_test();
-}
+
+  while (gTests.length) {
+    // Run current test.
+    yield Task.spawn(gTests.shift());
 
-function run_next_test() {
-  if (gTests.length) {
-    // Create corruption.
-    let test = gTests.shift();
-    test();
     // Regenerate getters.
-    PlacesUIUtils.__defineGetter__("leftPaneFolderId", gLeftPaneFolderIdGetter);
+    Object.defineProperty(PlacesUIUtils, "leftPaneFolderId", gLeftPaneFolderIdGetter);
     gLeftPaneFolderId = PlacesUIUtils.leftPaneFolderId;
-    PlacesUIUtils.__defineGetter__("allBookmarksFolderId", gAllBookmarksFolderIdGetter);
+    Object.defineProperty(PlacesUIUtils, "allBookmarksFolderId", gAllBookmarksFolderIdGetter);
+
     // Check the new left pane folder.
-    Task.spawn(function() {
-      let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId)
-      if (gReferenceHierarchy != leftPaneHierarchy) {
-        do_throw("hierarchies differ!\n" + gReferenceHierarchy +
-                                    "\n" + leftPaneHierarchy);
-      }
-      do_check_eq(PlacesUtils.bookmarks.getItemTitle(gFolderId), "test");
-      // Go to next test.
-      run_next_test();
+    let leftPaneHierarchy = folderIdToHierarchy(gLeftPaneFolderId)
+    Assert.equal(gReferenceHierarchy, leftPaneHierarchy);
+
+    folder = yield PlacesUtils.bookmarks.fetch({guid: folder.guid});
+    Assert.equal(folder.title, "test");
+  }
+});
+
+// Corruption cases.
+let gTests = [
+
+  function* test1() {
+    print("1. Do nothing, checks test calibration.");
+  },
+
+  function* test2() {
+    print("2. Delete the left pane folder.");
+    let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId);
+    yield PlacesUtils.bookmarks.remove(guid);
+  },
+
+  function* test3() {
+    print("3. Delete a child of the left pane folder.");
+    let guid = yield PlacesUtils.promiseItemGuid(gLeftPaneFolderId);
+    let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0});
+    yield PlacesUtils.bookmarks.remove(bm.guid);
+  },
+
+  function* test4() {
+    print("4. Delete AllBookmarks.");
+    let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId);
+    yield PlacesUtils.bookmarks.remove(guid);
+  },
+
+  function* test5() {
+    print("5. Create a duplicated left pane folder.");
+    let folder = yield PlacesUtils.bookmarks.insert({
+      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+      title: "PlacesRoot",
+      index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+      type: PlacesUtils.bookmarks.TYPE_FOLDER
     });
+
+    let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+    PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_FOLDER_ANNO,
+                                              "PlacesRoot", 0,
+                                              PlacesUtils.annotations.EXPIRE_NEVER);
+  },
+
+  function* test6() {
+    print("6. Create a duplicated left pane query.");
+    let folder = yield PlacesUtils.bookmarks.insert({
+      parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+      title: "AllBookmarks",
+      index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+      type: PlacesUtils.bookmarks.TYPE_FOLDER
+    });
+
+    let folderId = yield PlacesUtils.promiseItemId(folder.guid);
+    PlacesUtils.annotations.setItemAnnotation(folderId, ORGANIZER_QUERY_ANNO,
+                                              "AllBookmarks", 0,
+                                              PlacesUtils.annotations.EXPIRE_NEVER);
+  },
+
+  function* test7() {
+    print("7. Remove the left pane folder annotation.");
+    PlacesUtils.annotations.removeItemAnnotation(gLeftPaneFolderId,
+                                                 ORGANIZER_FOLDER_ANNO);
+  },
+
+  function* test8() {
+    print("8. Remove a left pane query annotation.");
+    PlacesUtils.annotations.removeItemAnnotation(PlacesUIUtils.allBookmarksFolderId,
+                                                 ORGANIZER_QUERY_ANNO);
+  },
+
+  function* test9() {
+    print("9. Remove a child of AllBookmarks.");
+    let guid = yield PlacesUtils.promiseItemGuid(PlacesUIUtils.allBookmarksFolderId);
+    let bm = yield PlacesUtils.bookmarks.fetch({parentGuid: guid, index: 0});
+    yield PlacesUtils.bookmarks.remove(bm.guid);
   }
-  else {
-    // All tests finished.
-    remove_all_bookmarks();
-    do_test_finished();
-  }
-}
+
+];
 
 /**
  * Convert a folder item id to a JSON representation of it and its contents.
  */
 function folderIdToHierarchy(aFolderId) {
   let root = PlacesUtils.getFolderContents(aFolderId).root;
   let hier = JSON.stringify(hierarchyToObj(root));
   root.containerOpen = false;
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -308,16 +308,26 @@ let gSyncPane = {
         // user is prompted to sign in with the address they previously used.
         let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
         let elt = document.getElementById("sync-migrate-upgrade-description");
         elt.textContent = email ?
                           sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
                                                   [email], 1) :
                           sb.GetStringFromName("needUserLong");
 
+        // The "Learn more" link.
+        if (!email) {
+          let learnMoreLink = document.createElement("label");
+          learnMoreLink.className = "text-link";
+          let { text, href } = fxaMigrator.learnMoreLink;
+          learnMoreLink.setAttribute("value", text);
+          learnMoreLink.href = href;
+          elt.appendChild(learnMoreLink);
+        }
+
         // The "upgrade" button.
         let button = document.getElementById("sync-migrate-upgrade");
         button.setAttribute("label",
                             sb.GetStringFromName(email
                                                  ? "signInAfterUpgradeOnOtherDevice.label"
                                                  : "upgradeToFxA.label"));
         button.setAttribute("accesskey",
                             sb.GetStringFromName(email
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -208,16 +208,26 @@ let gSyncPane = {
         // user is prompted to sign in with the address they previously used.
         let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
         let elt = document.getElementById("sync-migrate-upgrade-description");
         elt.textContent = email ?
                           sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
                                                   [email], 1) :
                           sb.GetStringFromName("needUserLong");
 
+        // The "Learn more" link.
+        if (!email) {
+          let learnMoreLink = document.createElement("label");
+          learnMoreLink.className = "text-link";
+          let { text, href } = fxaMigrator.learnMoreLink;
+          learnMoreLink.setAttribute("value", text);
+          learnMoreLink.href = href;
+          elt.appendChild(learnMoreLink);
+        }
+
         // The "upgrade" button.
         let button = document.getElementById("sync-migrate-upgrade");
         button.setAttribute("label",
                             sb.GetStringFromName(email
                                                  ? "signInAfterUpgradeOnOtherDevice.label"
                                                  : "upgradeToFxA.label"));
         button.setAttribute("accesskey",
                             sb.GetStringFromName(email
--- a/browser/devtools/animationinspector/animation-panel.js
+++ b/browser/devtools/animationinspector/animation-panel.js
@@ -133,16 +133,18 @@ EventEmitter.decorate(AnimationsPanel);
 function PlayerWidget(player, containerEl) {
   EventEmitter.decorate(this);
 
   this.player = player;
   this.containerEl = containerEl;
 
   this.onStateChanged = this.onStateChanged.bind(this);
   this.onPlayPauseBtnClick = this.onPlayPauseBtnClick.bind(this);
+
+  this.metaDataComponent = new PlayerMetaDataHeader();
 }
 
 PlayerWidget.prototype = {
   initialize: Task.async(function*() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
@@ -154,16 +156,17 @@ PlayerWidget.prototype = {
   destroy: Task.async(function*() {
     if (this.destroyed) {
       return;
     }
     this.destroyed = true;
 
     this.stopTimelineAnimation();
     this.stopListeners();
+    this.metaDataComponent.destroy();
 
     this.el.remove();
     this.playPauseBtnEl = this.currentTimeEl = this.timeDisplayEl = null;
     this.containerEl = this.el = this.player = null;
   }),
 
   startListeners: function() {
     this.player.on(this.player.AUTO_REFRESH_EVENT, this.onStateChanged);
@@ -179,55 +182,18 @@ PlayerWidget.prototype = {
     let state = this.player.state;
 
     this.el = createNode({
       attributes: {
         "class": "player-widget " + state.playState
       }
     });
 
-    // Animation header
-    let titleEl = createNode({
-      parent: this.el,
-      attributes: {
-        "class": "animation-title"
-      }
-    });
-    let titleHTML = "";
-
-    // Name.
-    if (state.name) {
-      // Css animations have names.
-      titleHTML += L10N.getStr("player.animationNameLabel");
-      titleHTML += "<strong>" + state.name + "</strong>";
-    } else {
-      // Css transitions don't.
-      titleHTML += L10N.getStr("player.transitionNameLabel");
-    }
-
-    // Duration, delay and iteration count.
-    titleHTML += "<span class='meta-data'>";
-    titleHTML += L10N.getStr("player.animationDurationLabel");
-    titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
-      this.getFormattedTime(state.duration)) + "</strong>";
-
-    if (state.delay) {
-      titleHTML += L10N.getStr("player.animationDelayLabel");
-      titleHTML += "<strong>" + L10N.getFormatStr("player.timeLabel",
-        this.getFormattedTime(state.delay)) + "</strong>";
-    }
-
-    if (state.iterationCount !== 1) {
-      titleHTML += L10N.getStr("player.animationIterationCountLabel");
-      let count = state.iterationCount || L10N.getStr("player.infiniteIterationCount");
-      titleHTML += "<strong>" + count + "</strong>";
-    }
-
-    titleHTML += "</span>";
-    titleEl.innerHTML = titleHTML;
+    this.metaDataComponent.createMarkup(this.el);
+    this.metaDataComponent.render(state);
 
     // Timeline widget.
     let timelineEl = createNode({
       parent: this.el,
       attributes: {
         "class": "timeline"
       }
     });
@@ -292,28 +258,16 @@ PlayerWidget.prototype = {
 
     this.containerEl.appendChild(this.el);
 
     // Show the initial time.
     this.displayTime(state.currentTime);
   },
 
   /**
-   * Format time as a string.
-   * @param {Number} time Defaults to the player's currentTime.
-   * @return {String} The formatted time, e.g. "10.55"
-   */
-  getFormattedTime: function(time) {
-    return (time/1000).toLocaleString(undefined, {
-      minimumFractionDigits: 2,
-      maximumFractionDigits: 2
-    });
-  },
-
-  /**
    * Executed when the playPause button is clicked.
    * Note that tests may want to call this callback directly rather than
    * simulating a click on the button since it returns the promise returned by
    * play and paused.
    * @return {Promise}
    */
   onPlayPauseBtnClick: function() {
     if (this.player.state.playState === "running") {
@@ -323,17 +277,18 @@ PlayerWidget.prototype = {
     }
   },
 
   /**
    * Whenever a player state update is received.
    */
   onStateChanged: function() {
     let state = this.player.state;
-    this.updateWidgetState(state.playState);
+    this.updateWidgetState(state);
+    this.metaDataComponent.render(state);
 
     switch (state.playState) {
       case "finished":
         this.stopTimelineAnimation();
         this.displayTime(this.player.state.duration);
         this.stopListeners();
         break;
       case "running":
@@ -349,36 +304,36 @@ PlayerWidget.prototype = {
   /**
    * Pause the animation player via this widget.
    * @return {Promise} Resolves when the player is paused, the button is
    * switched to the right state, and the timeline animation is stopped.
    */
   pause: function() {
     // Switch to the right className on the element right away to avoid waiting
     // for the next state update to change the playPause icon.
-    this.updateWidgetState("paused");
+    this.updateWidgetState({playState: "paused"});
     return this.player.pause().then(() => {
       this.stopTimelineAnimation();
     });
   },
 
   /**
    * Play the animation player via this widget.
    * @return {Promise} Resolves when the player is playing, the button is
    * switched to the right state, and the timeline animation is started.
    */
   play: function() {
     // Switch to the right className on the element right away to avoid waiting
     // for the next state update to change the playPause icon.
-    this.updateWidgetState("running");
+    this.updateWidgetState({playState: "running"});
     this.startTimelineAnimation();
     return this.player.play();
   },
 
-  updateWidgetState: function(playState) {
+  updateWidgetState: function({playState}) {
     this.el.className = "player-widget " + playState;
   },
 
   /**
    * Make the timeline progress smoothly, even though the currentTime is only
    * updated at some intervals. This uses a local animation loop.
    */
   startTimelineAnimation: function() {
@@ -412,17 +367,17 @@ PlayerWidget.prototype = {
     // the animation total duration (this may happen due to the local
     // requestAnimationFrame loop).
     if (state.iterationCount) {
       time = Math.min(time, state.iterationCount * state.duration);
     }
 
     // Set the time label value.
     this.timeDisplayEl.textContent = L10N.getFormatStr("player.timeLabel",
-      this.getFormattedTime(time));
+      L10N.numberWithDecimals(time / 1000, 2));
 
     // Set the timeline slider value.
     if (!state.iterationCount && time !== state.duration) {
       time = time % state.duration;
     }
     this.currentTimeEl.value = time;
   },
 
@@ -433,16 +388,172 @@ PlayerWidget.prototype = {
     if (this.rafID) {
       cancelAnimationFrame(this.rafID);
       this.rafID = null;
     }
   }
 };
 
 /**
+ * UI component responsible for displaying and updating the player meta-data:
+ * name, duration, iterations, delay.
+ * The parent UI component for this should drive its updates by calling
+ * render(state) whenever it wants the component to update.
+ */
+function PlayerMetaDataHeader() {
+  // Store the various state pieces we need to only refresh the UI when things
+  // change.
+  this.state = {};
+}
+
+PlayerMetaDataHeader.prototype = {
+  createMarkup: function(containerEl) {
+    // The main title element.
+    this.el = createNode({
+      parent: containerEl,
+      attributes: {
+        "class": "animation-title"
+      }
+    });
+
+    // Animation name (value hidden by default since transitions don't have names).
+    this.nameLabel = createNode({
+      parent: this.el,
+      nodeType: "span"
+    });
+
+    this.nameValue = createNode({
+      parent: this.el,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+
+    // Animation duration, delay and iteration container.
+    let metaData = createNode({
+      parent: this.el,
+      nodeType: "span",
+      attributes: {
+        "class": "meta-data"
+      }
+    });
+
+    // Animation duration.
+    this.durationLabel = createNode({
+      parent: metaData,
+      nodeType: "span"
+    });
+    this.durationLabel.textContent = L10N.getStr("player.animationDurationLabel");
+
+    this.durationValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation delay (hidden by default since there may not be a delay).
+    this.delayLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.delayLabel.textContent = L10N.getStr("player.animationDelayLabel");
+
+    this.delayValue = createNode({
+      parent: metaData,
+      nodeType: "strong"
+    });
+
+    // Animation iteration count (also hidden by default since we don't display
+    // single iterations).
+    this.iterationLabel = createNode({
+      parent: metaData,
+      nodeType: "span",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+    this.iterationLabel.textContent = L10N.getStr("player.animationIterationCountLabel");
+
+    this.iterationValue = createNode({
+      parent: metaData,
+      nodeType: "strong",
+      attributes: {
+        "style": "display:none;"
+      }
+    });
+  },
+
+  destroy: function() {
+    this.state = null;
+    this.el.remove();
+    this.el = null;
+    this.nameLabel = this.nameValue = null;
+    this.durationLabel = this.durationValue = null;
+    this.delayLabel = this.delayValue = null;
+    this.iterationLabel = this.iterationValue = null;
+  },
+
+  render: function(state) {
+    // Update the name if needed.
+    if (state.name !== this.state.name) {
+      if (state.name) {
+        // Css animations have names.
+        this.nameLabel.textContent = L10N.getStr("player.animationNameLabel");
+        this.nameValue.style.display = "inline";
+        this.nameValue.textContent = state.name;
+      } else {
+        // Css transitions don't.
+        this.nameLabel.textContent = L10N.getStr("player.transitionNameLabel");
+        this.nameValue.style.display = "none";
+      }
+    }
+
+    // update the duration value if needed.
+    if (state.duration !== this.state.duration) {
+      this.durationValue.textContent = L10N.getFormatStr("player.timeLabel",
+        L10N.numberWithDecimals(state.duration / 1000, 2));
+    }
+
+    // Update the delay if needed.
+    if (state.delay !== this.state.delay) {
+      if (state.delay) {
+        this.delayLabel.style.display = "inline";
+        this.delayValue.style.display = "inline";
+        this.delayValue.textContent = L10N.getFormatStr("player.timeLabel",
+          L10N.numberWithDecimals(state.delay / 1000, 2));
+      } else {
+        // Hide the delay elements if there is no delay defined.
+        this.delayLabel.style.display = "none";
+        this.delayValue.style.display = "none";
+      }
+    }
+
+    // Update the iterationCount if needed.
+    if (state.iterationCount !== this.state.iterationCount) {
+      if (state.iterationCount !== 1) {
+        this.iterationLabel.style.display = "inline";
+        this.iterationValue.style.display = "inline";
+        let count = state.iterationCount ||
+                    L10N.getStr("player.infiniteIterationCount");
+        this.iterationValue.innerHTML = count;
+      } else {
+        // Hide the iteration elements if iteration is 1.
+        this.iterationLabel.style.display = "none";
+        this.iterationValue.style.display = "none";
+      }
+    }
+
+    this.state = state;
+  }
+};
+
+/**
  * DOM node creation helper function.
  * @param {Object} Options to customize the node to be created.
  * @return {DOMNode} The newly created node.
  */
 function createNode(options) {
   let type = options.nodeType || "div";
   let node = document.createElement(type);
 
--- a/browser/devtools/animationinspector/test/browser.ini
+++ b/browser/devtools/animationinspector/test/browser.ini
@@ -17,8 +17,9 @@ support-files =
 [browser_animation_playerWidgets_meta_data.js]
 [browser_animation_playerWidgets_state_after_pause.js]
 [browser_animation_refresh_when_active.js]
 [browser_animation_same_nb_of_playerWidgets_and_playerFronts.js]
 [browser_animation_shows_player_on_valid_node.js]
 [browser_animation_timeline_animates.js]
 [browser_animation_timeline_waits_for_delay.js]
 [browser_animation_ui_updates_when_animation_changes.js]
+[browser_animation_ui_updates_when_animation_data_changes.js]
--- a/browser/devtools/animationinspector/test/browser_animation_iterationCount_hidden_by_default.js
+++ b/browser/devtools/animationinspector/test/browser_animation_iterationCount_hidden_by_default.js
@@ -8,17 +8,27 @@
 
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting a node with an animation that doesn't repeat");
   yield selectNode(".long", inspector);
   let widget = panel.playerWidgets[0];
-  let metaDataLabels = widget.el.querySelectorAll(".animation-title .meta-data strong");
-  is(metaDataLabels.length, 1, "Only the duration is shown");
+
+  ok(isNodeVisible(widget.metaDataComponent.durationValue),
+    "The duration value is shown");
+  ok(!isNodeVisible(widget.metaDataComponent.delayValue),
+    "The delay value is hidden");
+  ok(!isNodeVisible(widget.metaDataComponent.iterationValue),
+    "The iteration count is hidden");
 
   info("Selecting a node with an animation that repeats several times");
   yield selectNode(".delayed", inspector);
   widget = panel.playerWidgets[0];
-  let iterationLabel = widget.el.querySelectorAll(".animation-title .meta-data strong")[2];
-  is(iterationLabel.textContent, "10", "The iteration is shown");
+
+  ok(isNodeVisible(widget.metaDataComponent.durationValue),
+    "The duration value is shown");
+  ok(isNodeVisible(widget.metaDataComponent.delayValue),
+    "The delay value is shown");
+  ok(isNodeVisible(widget.metaDataComponent.iterationValue),
+    "The iteration count is shown");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_dont_show_time_after_duration.js
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_dont_show_time_after_duration.js
@@ -3,16 +3,18 @@
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that after the animation has ended, the current time label and timeline
 // slider don't show values bigger than the animation duration (which would
 // happen if the local requestAnimationFrame loop didn't stop correctly).
 
+let L10N = new ViewHelpers.L10N();
+
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Start an animation on the test node");
   getNode(".still").classList.add("short");
 
   info("Select the node");
@@ -30,11 +32,11 @@ add_task(function*() {
     }
   };
   front.on(front.AUTO_REFRESH_EVENT, onStateChanged);
   yield def.promise;
 
   is(widget.currentTimeEl.value, front.state.duration,
     "The timeline slider has the right value");
   is(widget.timeDisplayEl.textContent,
-    widget.getFormattedTime(front.state.duration) + "s",
+    L10N.numberWithDecimals(front.state.duration / 1000, 2) + "s",
     "The timeline slider has the right value");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_meta_data.js
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_meta_data.js
@@ -21,29 +21,34 @@ add_task(function*() {
   let nameEl = titleEl.querySelector("strong");
   ok(nameEl, "The first <strong> tag was retrieved, it should contain the name");
   is(nameEl.textContent, "simple-animation", "The animation name is correct");
 
   let metaDataEl = titleEl.querySelector(".meta-data");
   ok(metaDataEl, "The meta-data element exists");
 
   let metaDataEls = metaDataEl.querySelectorAll("strong");
-  is(metaDataEls.length, 2, "2 meta-data elements were found");
-  is(metaDataEls[0].textContent, "2.00s",
+  is(metaDataEls.length, 3, "3 meta-data elements were found");
+  is(metaDataEls[0].textContent, "2s",
     "The first meta-data is the duration, and is correct");
+  ok(!isNodeVisible(metaDataEls[1]),
+    "The second meta-data is hidden, since there's no delay on the animation");
 
   info("Select the node with the delayed animation");
   yield selectNode(".delayed", inspector);
 
   titleEl = panel.playerWidgets[0].el.querySelector(".animation-title");
   nameEl = titleEl.querySelector("strong");
   is(nameEl.textContent, "simple-animation", "The animation name is correct");
 
   metaDataEls = titleEl.querySelectorAll(".meta-data strong");
   is(metaDataEls.length, 3,
     "3 meta-data elements were found for the delayed animation");
-  is(metaDataEls[0].textContent, "3.00s",
+  is(metaDataEls[0].textContent, "3s",
     "The first meta-data is the duration, and is correct");
-  is(metaDataEls[1].textContent, "60.00s",
+  ok(isNodeVisible(metaDataEls[0]), "The duration is shown");
+  is(metaDataEls[1].textContent, "60s",
     "The second meta-data is the delay, and is correct");
+  ok(isNodeVisible(metaDataEls[1]), "The delay is shown");
   is(metaDataEls[2].textContent, "10",
     "The third meta-data is the iteration count, and is correct");
+  ok(isNodeVisible(metaDataEls[2]), "The iteration count is shown");
 });
--- a/browser/devtools/animationinspector/test/browser_animation_playerWidgets_state_after_pause.js
+++ b/browser/devtools/animationinspector/test/browser_animation_playerWidgets_state_after_pause.js
@@ -2,16 +2,18 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that once an animation is paused and its widget is refreshed, the right
 // initial time is displayed.
 
+let L10N = new ViewHelpers.L10N();
+
 add_task(function*() {
   yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
   let {inspector, panel} = yield openAnimationInspector();
 
   info("Selecting the test node");
   yield selectNode(".animated", inspector);
 
   info("Pausing the animation by using the widget");
@@ -20,11 +22,12 @@ add_task(function*() {
 
   info("Selecting another node and then the same node again to refresh the widget");
   yield selectNode(".still", inspector);
   yield selectNode(".animated", inspector);
 
   widget = panel.playerWidgets[0];
   ok(widget.el.classList.contains("paused"), "The widget is still in paused mode");
   is(widget.timeDisplayEl.textContent,
-    widget.getFormattedTime(widget.player.state.currentTime) + "s",
+    L10N.numberWithDecimals(widget.player.state.currentTime / 1000, 2) + "s",
     "The initial time has been set to the player's");
 });
+
--- a/browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js
+++ b/browser/devtools/animationinspector/test/browser_animation_timeline_waits_for_delay.js
@@ -15,10 +15,10 @@ add_task(function*() {
   yield selectNode(".delayed", inspector);
 
   let widget = panel.playerWidgets[0];
 
   let timeline = widget.currentTimeEl;
   is(timeline.value, 0, "The timeline is at 0 since the animation hasn't started");
 
   let timeLabel = widget.timeDisplayEl;
-  is(timeLabel.textContent, "0.00s", "The current time is 0");
+  is(timeLabel.textContent, "0s", "The current time is 0");
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/animationinspector/test/browser_animation_ui_updates_when_animation_data_changes.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify that if the animation's duration, iterations or delay change in
+// content, then the widget reflects the changes.
+
+add_task(function*() {
+  yield addTab(TEST_URL_ROOT + "doc_simple_animation.html");
+  let {panel, inspector} = yield openAnimationInspector();
+
+  info("Select the test node");
+  yield selectNode(".animated", inspector);
+
+  info("Get the player widget");
+  let widget = panel.playerWidgets[0];
+
+  yield setStyle(widget, "animationDuration", "5.5s");
+  is(widget.metaDataComponent.durationValue.textContent, "5.50s",
+    "The widget shows the new duration");
+
+  yield setStyle(widget, "animationIterationCount", "300");
+  is(widget.metaDataComponent.iterationValue.textContent, "300",
+    "The widget shows the new iteration count");
+
+  yield setStyle(widget, "animationDelay", "45s");
+  is(widget.metaDataComponent.delayValue.textContent, "45s",
+    "The widget shows the new delay");
+});
+
+function* setStyle(widget, name, value) {
+  info("Change the animation style via the content DOM. Setting " +
+    name + " to " + value);
+  yield executeInContent("Test:SetNodeStyle", {
+    propertyName: name,
+    propertyValue: value
+  }, {
+    node: getNode(".animated")
+  });
+
+  info("Wait for the next state update");
+  yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
+}
--- a/browser/devtools/animationinspector/test/doc_frame_script.js
+++ b/browser/devtools/animationinspector/test/doc_frame_script.js
@@ -21,8 +21,26 @@ addMessageListener("Test:ToggleAnimation
   if (pause) {
     player.pause();
   } else {
     player.play();
   }
 
   sendAsyncMessage("Test:ToggleAnimationPlayer");
 });
+
+/**
+ * Set a given style property value on a node. This is useful to dynamically
+ * change an animation's duration or delay for instance.
+ * @param {Object} data
+ * - {String} propertyName The name of the property to set.
+ * - {String} propertyValue The value for the property.
+ * @param {Object} objects
+ * - {DOMNode} node The node to use
+ */
+addMessageListener("Test:SetNodeStyle", function(msg) {
+  let {propertyName, propertyValue} = msg.data;
+  let {node} = msg.objects;
+
+  node.style[propertyName] = propertyValue;
+
+  sendAsyncMessage("Test:SetNodeStyle");
+});
--- a/browser/devtools/animationinspector/test/doc_simple_animation.html
+++ b/browser/devtools/animationinspector/test/doc_simple_animation.html
@@ -49,25 +49,25 @@
                  other-animation 4s;
     }
 
     .short {
       top: 500px;
       left: 10px;
       background: red;
 
-      animation: simple-animation 2s
+      animation: simple-animation 2s;
     }
 
     .long {
       top: 600px;
       left: 10px;
       background: blue;
 
-      animation: simple-animation 120s
+      animation: simple-animation 120s;
     }
 
     @keyframes simple-animation {
       100% {
         transform: translateX(300px);
       }
     }
 
--- a/browser/devtools/animationinspector/test/head.js
+++ b/browser/devtools/animationinspector/test/head.js
@@ -1,20 +1,21 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const Cu = Components.utils;
-let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
-let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
-let TargetFactory = devtools.TargetFactory;
-let {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
+const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const TargetFactory = devtools.TargetFactory;
+const {console} = Components.utils.import("resource://gre/modules/devtools/Console.jsm", {});
+const {ViewHelpers} = Cu.import("resource:///modules/devtools/ViewHelpers.jsm", {});
 
 // All tests are asynchronous
 waitForExplicitFinish();
 
 const TEST_URL_ROOT = "http://example.com/browser/browser/devtools/animationinspector/test/";
 const ROOT_TEST_DIR = getRootDirectory(gTestPath);
 const FRAME_SCRIPT_URL = ROOT_TEST_DIR + "doc_frame_script.js";
 
@@ -258,17 +259,35 @@ function executeInContent(name, data={},
     return promise.resolve();
   }
 }
 
 /**
  * Simulate a click on the playPause button of a playerWidget.
  */
 let togglePlayPauseButton = Task.async(function*(widget) {
+  let nextState = widget.player.state.playState === "running" ? "paused" : "running";
+
   // Note that instead of simulating a real event here, the callback is just
   // called. This is better because the callback returns a promise, so we know
   // when the player is paused, and we don't really care to test that simulating
   // a DOM event actually works.
-  yield widget.onPlayPauseBtnClick();
+  let onClicked = widget.onPlayPauseBtnClick();
+
+  // Verify that the button's state is changed immediately, even if it will be
+  // changed anyway with the next auto-refresh.
+  ok(widget.el.classList.contains(nextState),
+    "The button's state was changed in the UI before the request was sent");
+
+  yield onClicked;
 
   // Wait for the next sate change event to make sure the state is updated
   yield widget.player.once(widget.player.AUTO_REFRESH_EVENT);
 });
+
+/**
+ * Is the given node visible in the page (rendered in the frame tree).
+ * @param {DOMNode}
+ * @return {Boolean}
+ */
+function isNodeVisible(node) {
+  return !!node.getClientRects().length;
+}
--- a/browser/devtools/performance/performance-view.js
+++ b/browser/devtools/performance/performance-view.js
@@ -2,77 +2,143 @@
  * 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";
 
 /**
  * Master view handler for the performance tool.
  */
 let PerformanceView = {
+
+  _state: null,
+
+  // Mapping of state to selectors for different panes
+  // of the main profiler view. Used in `PerformanceView.setState()`
+  states: {
+    empty: [
+      { deck: "#performance-view", pane: "#empty-notice" }
+    ],
+    recording: [
+      { deck: "#performance-view", pane: "#performance-view-content" },
+      { deck: "#details-pane-container", pane: "#recording-notice" }
+    ],
+    recorded: [
+      { deck: "#performance-view", pane: "#performance-view-content" },
+      { deck: "#details-pane-container", pane: "#details-pane" }
+    ]
+  },
+
   /**
    * Sets up the view with event binding and main subviews.
    */
   initialize: function () {
     this._recordButton = $("#record-button");
     this._importButton = $("#import-button");
 
     this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
     this._onImportButtonClick = this._onImportButtonClick.bind(this);
     this._lockRecordButton = this._lockRecordButton.bind(this);
     this._unlockRecordButton = this._unlockRecordButton.bind(this);
+    this._onRecordingSelected = this._onRecordingSelected.bind(this);
+    this._onRecordingStopped = this._onRecordingStopped.bind(this);
 
-    this._recordButton.addEventListener("click", this._onRecordButtonClick);
+    for (let button of $$(".record-button")) {
+      button.addEventListener("click", this._onRecordButtonClick);
+    }
     this._importButton.addEventListener("click", this._onImportButtonClick);
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
-    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
+
+    this.setState("empty");
 
     return promise.all([
       RecordingsView.initialize(),
       OverviewView.initialize(),
       ToolbarView.initialize(),
       DetailsView.initialize()
     ]);
   },
 
   /**
    * Unbinds events and destroys subviews.
    */
   destroy: function () {
-    this._recordButton.removeEventListener("click", this._onRecordButtonClick);
+    for (let button of $$(".record-button")) {
+      button.removeEventListener("click", this._onRecordButtonClick);
+    }
     this._importButton.removeEventListener("click", this._onImportButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
-    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
 
     return promise.all([
       RecordingsView.destroy(),
       OverviewView.destroy(),
       ToolbarView.destroy(),
       DetailsView.destroy()
     ]);
   },
 
   /**
+   * Sets the state of the profiler view. Possible options are "empty",
+   * "recording", "recorded".
+   */
+  setState: function (state) {
+    let viewConfig = this.states[state];
+    if (!viewConfig) {
+      throw new Error(`Invalid state for PerformanceView: ${state}`);
+    }
+    for (let { deck, pane } of viewConfig) {
+      $(deck).selectedPanel = $(pane);
+    }
+
+    this._state = state;
+  },
+
+  /**
+   * Returns the state of the PerformanceView.
+   */
+  getState: function () {
+    return this._state;
+  },
+
+  /**
    * Adds the `locked` attribute on the record button. This prevents it
    * from being clicked while recording is started or stopped.
    */
   _lockRecordButton: function () {
     this._recordButton.setAttribute("locked", "true");
   },
 
   /**
    * Removes the `locked` attribute on the record button.
    */
   _unlockRecordButton: function () {
     this._recordButton.removeAttribute("locked");
   },
 
   /**
+   * When a recording is complete.
+   */
+  _onRecordingStopped: function (_, recording) {
+    this._unlockRecordButton();
+
+    // If this recording stopped is the current recording, set the
+    // state to "recorded". A stopped recording doesn't necessarily
+    // have to be the current recording (console.profileEnd, for example)
+    if (recording === PerformanceController.getCurrentRecording()) {
+      this.setState("recorded");
+    }
+  },
+
+  /**
    * Handler for clicking the record button.
    */
   _onRecordButtonClick: function (e) {
     if (this._recordButton.hasAttribute("checked")) {
       this._recordButton.removeAttribute("checked");
       this._lockRecordButton();
       this.emit(EVENTS.UI_STOP_RECORDING);
     } else {
@@ -89,15 +155,26 @@ let PerformanceView = {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
 
     if (fp.show() == Ci.nsIFilePicker.returnOK) {
       this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
     }
+  },
+
+  /**
+   * Fired when a recording is selected. Used to toggle the profiler view state.
+   */
+  _onRecordingSelected: function (_, recording) {
+    if (recording.isRecording()) {
+      this.setState("recording");
+    } else {
+      this.setState("recorded");
+    }
   }
 };
 
 /**
  * Convenient way of emitting events from the view.
  */
 EventEmitter.decorate(PerformanceView);
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -60,17 +60,17 @@
 
   <hbox class="theme-body" flex="1">
     <vbox id="recordings-pane">
       <toolbar id="recordings-toolbar"
                class="devtools-toolbar">
         <hbox id="recordings-controls"
               class="devtools-toolbarbutton-group">
           <toolbarbutton id="record-button"
-                         class="devtools-toolbarbutton"
+                         class="devtools-toolbarbutton record-button"
                          tooltiptext="&profilerUI.recordButton.tooltip;"/>
           <toolbarbutton id="import-button"
                          class="devtools-toolbarbutton"
                          label="&profilerUI.importButton;"/>
           <toolbarbutton id="clear-button"
                          class="devtools-toolbarbutton"
                          label="&profilerUI.clearButton;"/>
         </hbox>
@@ -106,103 +106,127 @@
         <hbox id="performance-toolbar-control-options" class="devtools-toolbarbutton-group">
           <toolbarbutton id="performance-options-button"
                          class="devtools-toolbarbutton devtools-option-toolbarbutton"
                          popup="performance-options-menupopup"
                          tooltiptext="&profilerUI.options.tooltiptext;"/>
         </hbox>
       </toolbar>
 
-      <vbox id="overview-pane">
-        <hbox id="markers-overview"/>
-        <hbox id="memory-overview"/>
-        <hbox id="time-framerate"/>
-      </vbox>
-
-      <deck id="details-pane" flex="1">
-        <hbox id="waterfall-view" flex="1">
-          <vbox id="waterfall-breakdown" flex="1" />
-          <splitter class="devtools-side-splitter"/>
-          <vbox id="waterfall-details"
-                class="theme-sidebar"
-                width="150"
-                height="150"/>
+      <deck id="performance-view" flex="1">
+        <hbox id="empty-notice"
+              class="notice-container"
+              align="center"
+              pack="center"
+              flex="1">
+          <label value="&profilerUI.emptyNotice1;"/>
+          <button class="devtools-toolbarbutton record-button"
+                  standalone="true" />
+          <label value="&profilerUI.emptyNotice2;"/>
         </hbox>
-
-        <vbox id="js-calltree-view" flex="1">
-          <hbox class="call-tree-headers-container">
-            <label class="plain call-tree-header"
-                   type="duration"
-                   crop="end"
-                   value="&profilerUI.table.totalDuration2;"/>
-            <label class="plain call-tree-header"
-                   type="percentage"
-                   crop="end"
-                   value="&profilerUI.table.totalPercentage;"/>
-            <label class="plain call-tree-header"
-                   type="self-duration"
-                   crop="end"
-                   value="&profilerUI.table.selfDuration2;"/>
-            <label class="plain call-tree-header"
-                   type="self-percentage"
-                   crop="end"
-                   value="&profilerUI.table.selfPercentage;"/>
-            <label class="plain call-tree-header"
-                   type="samples"
-                   crop="end"
-                   value="&profilerUI.table.samples;"/>
-            <label class="plain call-tree-header"
-                   type="function"
-                   crop="end"
-                   value="&profilerUI.table.function;"/>
-          </hbox>
-          <vbox class="call-tree-cells-container" flex="1"/>
-        </vbox>
+        <vbox id="performance-view-content" flex="1">
+          <vbox id="overview-pane">
+            <hbox id="markers-overview"/>
+            <hbox id="memory-overview"/>
+            <hbox id="time-framerate"/>
+          </vbox>
+          <deck id="details-pane-container" flex="1">
+            <hbox id="recording-notice"
+                  class="notice-container"
+                  align="center"
+                  pack="center"
+                  flex="1">
+              <label value="&profilerUI.stopNotice1;"/>
+              <button class="devtools-toolbarbutton record-button"
+                      standalone="true"
+                      checked="true" />
+              <label value="&profilerUI.stopNotice2;"/>
+            </hbox>
+            <deck id="details-pane" flex="1">
+              <hbox id="waterfall-view" flex="1">
+                <vbox id="waterfall-breakdown" flex="1" />
+                <splitter class="devtools-side-splitter"/>
+                <vbox id="waterfall-details"
+                      class="theme-sidebar"
+                      width="150"
+                      height="150"/>
+              </hbox>
 
-        <hbox id="js-flamegraph-view" flex="1">
-        </hbox>
+              <vbox id="js-calltree-view" flex="1">
+                <hbox class="call-tree-headers-container">
+                  <label class="plain call-tree-header"
+                         type="duration"
+                         crop="end"
+                         value="&profilerUI.table.totalDuration2;"/>
+                  <label class="plain call-tree-header"
+                         type="percentage"
+                         crop="end"
+                         value="&profilerUI.table.totalPercentage;"/>
+                  <label class="plain call-tree-header"
+                         type="self-duration"
+                         crop="end"
+                         value="&profilerUI.table.selfDuration2;"/>
+                  <label class="plain call-tree-header"
+                         type="self-percentage"
+                         crop="end"
+                         value="&profilerUI.table.selfPercentage;"/>
+                  <label class="plain call-tree-header"
+                         type="samples"
+                         crop="end"
+                         value="&profilerUI.table.samples;"/>
+                  <label class="plain call-tree-header"
+                         type="function"
+                         crop="end"
+                         value="&profilerUI.table.function;"/>
+                </hbox>
+                <vbox class="call-tree-cells-container" flex="1"/>
+              </vbox>
+
+              <hbox id="js-flamegraph-view" flex="1">
+              </hbox>
 
-        <vbox id="memory-calltree-view" flex="1">
-          <hbox class="call-tree-headers-container">
-            <label class="plain call-tree-header"
-                   type="duration"
-                   crop="end"
-                   value="&profilerUI.table.totalDuration2;"/>
-            <label class="plain call-tree-header"
-                   type="percentage"
-                   crop="end"
-                   value="&profilerUI.table.totalPercentage;"/>
-            <label class="plain call-tree-header"
-                   type="allocations"
-                   crop="end"
-                   value="&profilerUI.table.totalAlloc;"/>
-            <label class="plain call-tree-header"
-                   type="self-duration"
-                   crop="end"
-                   value="&profilerUI.table.selfDuration2;"/>
-            <label class="plain call-tree-header"
-                   type="self-percentage"
-                   crop="end"
-                   value="&profilerUI.table.selfPercentage;"/>
-            <label class="plain call-tree-header"
-                   type="self-allocations"
-                   crop="end"
-                   value="&profilerUI.table.selfAlloc;"/>
-            <label class="plain call-tree-header"
-                   type="samples"
-                   crop="end"
-                   value="&profilerUI.table.samples;"/>
-            <label class="plain call-tree-header"
-                   type="function"
-                   crop="end"
-                   value="&profilerUI.table.function;"/>
-          </hbox>
-          <vbox class="call-tree-cells-container" flex="1"/>
+              <vbox id="memory-calltree-view" flex="1">
+                <hbox class="call-tree-headers-container">
+                  <label class="plain call-tree-header"
+                         type="duration"
+                         crop="end"
+                         value="&profilerUI.table.totalDuration2;"/>
+                  <label class="plain call-tree-header"
+                         type="percentage"
+                         crop="end"
+                         value="&profilerUI.table.totalPercentage;"/>
+                  <label class="plain call-tree-header"
+                         type="allocations"
+                         crop="end"
+                         value="&profilerUI.table.totalAlloc;"/>
+                  <label class="plain call-tree-header"
+                         type="self-duration"
+                         crop="end"
+                         value="&profilerUI.table.selfDuration2;"/>
+                  <label class="plain call-tree-header"
+                         type="self-percentage"
+                         crop="end"
+                         value="&profilerUI.table.selfPercentage;"/>
+                  <label class="plain call-tree-header"
+                         type="self-allocations"
+                         crop="end"
+                         value="&profilerUI.table.selfAlloc;"/>
+                  <label class="plain call-tree-header"
+                         type="samples"
+                         crop="end"
+                         value="&profilerUI.table.samples;"/>
+                  <label class="plain call-tree-header"
+                         type="function"
+                         crop="end"
+                         value="&profilerUI.table.function;"/>
+                </hbox>
+                <vbox class="call-tree-cells-container" flex="1"/>
+              </vbox>
+
+              <hbox id="memory-flamegraph-view" flex="1">
+              </hbox>
+            </deck>
+          </deck>
         </vbox>
-
-        <hbox id="memory-flamegraph-view" flex="1">
-          <!-- TODO: bug 1077461 -->
-        </hbox>
       </deck>
-
     </vbox>
   </hbox>
 </window>
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -47,16 +47,18 @@ support-files =
 [browser_perf-overview-render-03.js]
 [browser_perf-overview-selection-01.js]
 [browser_perf-overview-selection-02.js]
 [browser_perf-overview-selection-03.js]
 [browser_perf-overview-time-interval.js]
 [browser_perf-shared-connection-02.js]
 [browser_perf-shared-connection-03.js]
 [browser_perf-ui-recording.js]
+[browser_perf-recording-notices-01.js]
+[browser_perf-recording-notices-02.js]
 [browser_perf_recordings-io-01.js]
 [browser_perf_recordings-io-02.js]
 [browser_perf_recordings-io-03.js]
 [browser_perf_recordings-io-04.js]
 [browser_perf-range-changed-render.js]
 [browser_perf-recording-selected-01.js]
 [browser_perf-recording-selected-02.js]
 [browser_perf-recording-selected-03.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-recording-notices-01.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the recording notice panes are toggled in correct scenarios
+ * for initialization and a single recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, PerformanceView, RecordingsView } = panel.panelWin;
+
+  let MAIN_CONTAINER = $("#performance-view");
+  let EMPTY = $("#empty-notice");
+  let CONTENT = $("#performance-view-content");
+  let CONTENT_CONTAINER = $("#details-pane-container");
+  let RECORDING = $("#recording-notice");
+  let DETAILS = $("#details-pane");
+
+  is(PerformanceView.getState(), "empty", "correct default state");
+  is(MAIN_CONTAINER.selectedPanel, EMPTY, "showing empty panel on load");
+
+  yield startRecording(panel);
+
+  is(PerformanceView.getState(), "recording", "correct state during recording");
+  is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline");
+  is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel");
+
+  yield stopRecording(panel);
+
+  is(PerformanceView.getState(), "recorded", "correct state after recording");
+  is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline");
+  is(CONTENT_CONTAINER.selectedPanel, DETAILS, "showing rendered graphs");
+
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-recording-notices-02.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the recording notice panes are toggled when going between
+ * a completed recording and an in-progress recording.
+ */
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, $, PerformanceController, PerformanceView, RecordingsView } = panel.panelWin;
+
+  let MAIN_CONTAINER = $("#performance-view");
+  let CONTENT = $("#performance-view-content");
+  let CONTENT_CONTAINER = $("#details-pane-container");
+  let RECORDING = $("#recording-notice");
+  let DETAILS = $("#details-pane");
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield startRecording(panel);
+
+  is(PerformanceView.getState(), "recording", "correct state during recording");
+  is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline");
+  is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel");
+
+  let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 0;
+  yield select;
+
+  is(PerformanceView.getState(), "recorded", "correct state during recording but selecting a completed recording");
+  is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline");
+  is(CONTENT_CONTAINER.selectedPanel, DETAILS, "showing recorded panel");
+
+  select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 1;
+  yield select;
+
+  is(PerformanceView.getState(), "recording", "correct state when switching back to recording in progress");
+  is(MAIN_CONTAINER.selectedPanel, CONTENT, "showing main view with timeline");
+  is(CONTENT_CONTAINER.selectedPanel, RECORDING, "showing recording panel");
+
+  yield stopRecording(panel);
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/themes/shared/devtools/performance.inc.css
+++ b/browser/themes/shared/devtools/performance.inc.css
@@ -29,20 +29,39 @@
   min-width: 0;
 }
 
 #performance-toolbar-controls-detail-views .toolbarbutton-text {
   -moz-padding-start: 4px;
   -moz-padding-end: 8px;
 }
 
+/* Recording Notice */
+
+#performance-view .notice-container {
+  font-size: 120%;
+  background-color: var(--theme-toolbar-background);
+  color: var(--theme-body-color);
+  padding-bottom: 20vh;
+}
+
+#performance-view .notice-container button {
+  min-width: 30px;
+  min-height: 28px;
+  margin: 0;
+}
+
+/* Overview Panel */
+
+.notice-container button,
 #record-button {
   list-style-image: url(profiler-stopwatch.svg);
 }
 
+.notice-container button[checked],
 #record-button[checked] {
   list-style-image: url(profiler-stopwatch-checked.svg);
 }
 
 #record-button[locked] {
   pointer-events: none;
 }
 
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -886,32 +886,32 @@
 .theme-light .command-button-invertable:active > image,
 .theme-light .devtools-closebutton > image,
 .theme-light .devtools-toolbarbutton > image,
 .theme-light .devtools-option-toolbarbutton > image,
 .theme-light #breadcrumb-separator-normal,
 .theme-light .scrollbutton-up > .toolbarbutton-icon,
 .theme-light .scrollbutton-down > .toolbarbutton-icon,
 .theme-light #black-boxed-message-button .button-icon,
-.theme-light #profiling-notice-button .button-icon,
-.theme-light #canvas-debugging-empty-notice-button .button-icon,
+.theme-light .notice-container button .button-icon,
 .theme-light #requests-menu-perf-notice-button .button-icon,
 .theme-light #requests-menu-network-summary-button .button-icon,
 .theme-light .event-tooltip-debugger-icon,
 .theme-light .devtools-button::before {
   filter: url(filters.svg#invert);
 }
 
 /* Since selected backgrounds are blue, we want to use the normal
  * (light) icons. */
 .theme-light .command-button-invertable[checked=true]:not(:active) > image,
 .theme-light .devtools-tab[icon-invertable][selected] > image,
 .theme-light .devtools-tab[icon-invertable][highlighted] > image,
 .theme-light #record-snapshot[checked] > image,
-.theme-light #profiler-start[checked] > image {
+.theme-light #profiler-start[checked] > image,
+.theme-light .notice-container button[checked] .button-icon {
   filter: none !important;
 }
 
 .theme-light .command-button:hover {
   background-color: inherit;
 }
 
 .theme-light .command-button:hover:active,
@@ -953,9 +953,9 @@
 
 @keyframes throbber-spin {
   from {
     transform: none;
   }
   to {
     transform: rotate(360deg);
   }
-}
\ No newline at end of file
+}
--- a/mobile/android/base/ReadingListHelper.java
+++ b/mobile/android/base/ReadingListHelper.java
@@ -5,139 +5,126 @@
 package org.mozilla.gecko;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.util.Log;
 import android.widget.Toast;
 
-public final class ReadingListHelper implements GeckoEventListener, NativeEventListener {
+public final class ReadingListHelper implements NativeEventListener {
     private static final String LOGTAG = "ReadingListHelper";
 
     protected final Context context;
 
     public ReadingListHelper(Context context) {
         this.context = context;
 
-        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
-            "Reader:AddToList", "Reader:FaviconRequest");
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this,
-            "Reader:ListStatusRequest", "Reader:RemoveFromList");
+            "Reader:AddToList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
     }
 
     public void uninit() {
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
-            "Reader:AddToList", "Reader:FaviconRequest");
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
-            "Reader:ListStatusRequest", "Reader:RemoveFromList");
-    }
-
-    @Override
-    public void handleMessage(String event, JSONObject message) {
-        switch(event) {
-            case "Reader:AddToList": {
-                handleAddToList(message);
-                break;
-            }
-
-            case "Reader:FaviconRequest": {
-                handleReaderModeFaviconRequest(message.optString("url"));
-                break;
-            }
-        }
+            "Reader:AddToList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
         switch(event) {
+            case "Reader:AddToList": {
+                handleAddToList(callback, message);
+                break;
+            }
+            case "Reader:FaviconRequest": {
+                handleReaderModeFaviconRequest(callback, message.getString("url"));
+                break;
+            }
             case "Reader:RemoveFromList": {
                 handleRemoveFromList(message.getString("url"));
                 break;
             }
-
             case "Reader:ListStatusRequest": {
                 handleReadingListStatusRequest(callback, message.getString("url"));
                 break;
             }
         }
     }
 
     /**
      * A page can be added to the ReadingList by long-tap of the page-action
      * icon, or by tapping the readinglist-add icon in the ReaderMode banner.
+     *
+     * This method will only add new items, not update existing items.
      */
-    private void handleAddToList(final JSONObject message) {
+    private void handleAddToList(final EventCallback callback, final NativeJSObject message) {
         final ContentResolver cr = context.getContentResolver();
-        final String url = message.optString("url");
+        final String url = message.getString("url");
+
+        // We can't access a NativeJSObject from the background thread, so we need to get the
+        // values here, even if we may not use them to insert an item into the DB.
+        final ContentValues values = new ContentValues();
+        values.put(ReadingListItems.URL, url);
+        values.put(ReadingListItems.TITLE, message.getString("title"));
+        values.put(ReadingListItems.LENGTH, message.getInt("length"));
+        values.put(ReadingListItems.EXCERPT, message.getString("excerpt"));
+        values.put(ReadingListItems.CONTENT_STATUS, message.getInt("status"));
 
         final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 if (db.isReadingListItem(cr, url)) {
                     showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
-
+                    callback.sendError("URL already in reading list: " + url);
                 } else {
-                    final ContentValues values = new ContentValues();
-                    values.put(ReadingListItems.URL, url);
-                    values.put(ReadingListItems.TITLE, message.optString("title"));
-                    values.put(ReadingListItems.LENGTH, message.optInt("length"));
-                    values.put(ReadingListItems.EXCERPT, message.optString("excerpt"));
-                    values.put(ReadingListItems.CONTENT_STATUS, message.optInt("status"));
                     db.addReadingListItem(cr, values);
-
                     showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
+                    callback.sendSuccess(url);
                 }
             }
         });
-
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Added", url));
     }
 
     /**
      * Gecko (ReaderMode) requests the page favicon to append to the
      * document head for display.
      */
-    private void handleReaderModeFaviconRequest(final String url) {
+    private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) {
         final BrowserDB db = GeckoProfile.get(context).getDB();
         (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public String doInBackground() {
                 return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url);
             }
 
             @Override
             public void onPostExecute(String faviconUrl) {
                 JSONObject args = new JSONObject();
-
                 if (faviconUrl != null) {
                     try {
                         args.put("url", url);
                         args.put("faviconUrl", faviconUrl);
                     } catch (JSONException e) {
                         Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
                     }
                 }
-
-                GeckoAppShell.sendEventToGecko(
-                    GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
+                callback.sendSuccess(args.toString());
             }
         }).execute();
     }
 
     /**
      * A page can be removed from the ReadingList by panel context menu,
      * or by tapping the readinglist-remove icon in the ReaderMode banner.
      */
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -765,27 +765,29 @@ sync_thirdparty_java_files = [
 
 sync_java_files = [
     'background/BackgroundService.java',
     'background/bagheera/BagheeraClient.java',
     'background/bagheera/BagheeraRequestDelegate.java',
     'background/bagheera/BoundedByteArrayEntity.java',
     'background/bagheera/DeflateHelper.java',
     'background/common/DateUtils.java',
+    'background/common/EditorBranch.java',
     'background/common/GlobalConstants.java',
     'background/common/log/Logger.java',
     'background/common/log/writers/AndroidLevelCachingLogWriter.java',
     'background/common/log/writers/AndroidLogWriter.java',
     'background/common/log/writers/LevelFilteringLogWriter.java',
     'background/common/log/writers/LogWriter.java',
     'background/common/log/writers/PrintLogWriter.java',
     'background/common/log/writers/SimpleTagLogWriter.java',
     'background/common/log/writers/StringLogWriter.java',
     'background/common/log/writers/TagLogWriter.java',
     'background/common/log/writers/ThreadLocalTagLogWriter.java',
+    'background/common/PrefsBranch.java',
     'background/common/telemetry/TelemetryWrapper.java',
     'background/datareporting/TelemetryRecorder.java',
     'background/db/CursorDumper.java',
     'background/db/Tab.java',
     'background/fxa/FxAccount10AuthDelegate.java',
     'background/fxa/FxAccount10CreateDelegate.java',
     'background/fxa/FxAccount20CreateDelegate.java',
     'background/fxa/FxAccount20LoginDelegate.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/common/EditorBranch.java
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common;
+
+import java.util.Set;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+
+public class EditorBranch implements Editor {
+
+  private final String prefix;
+  private Editor editor;
+
+  public EditorBranch(final SharedPreferences prefs, final String prefix) {
+    if (!prefix.endsWith(".")) {
+      throw new IllegalArgumentException("No trailing period in prefix.");
+    }
+    this.prefix = prefix;
+    this.editor = prefs.edit();
+  }
+
+  public void apply() {
+    // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding.
+    this.editor.commit();
+  }
+
+  @Override
+  public Editor clear() {
+    this.editor = this.editor.clear();
+    return this;
+  }
+
+  @Override
+  public boolean commit() {
+    return this.editor.commit();
+  }
+
+  @Override
+  public Editor putBoolean(String key, boolean value) {
+    this.editor = this.editor.putBoolean(prefix + key, value);
+    return this;
+  }
+
+  @Override
+  public Editor putFloat(String key, float value) {
+    this.editor = this.editor.putFloat(prefix + key, value);
+    return this;
+  }
+
+  @Override
+  public Editor putInt(String key, int value) {
+    this.editor = this.editor.putInt(prefix + key, value);
+    return this;
+  }
+
+  @Override
+  public Editor putLong(String key, long value) {
+    this.editor = this.editor.putLong(prefix + key, value);
+    return this;
+  }
+
+  @Override
+  public Editor putString(String key, String value) {
+    this.editor = this.editor.putString(prefix + key, value);
+    return this;
+  }
+
+  // Not marking as Override, because Android <= 10 doesn't have
+  // putStringSet. Neither can we implement it.
+  public Editor putStringSet(String key, Set<String> value) {
+    throw new RuntimeException("putStringSet not available.");
+  }
+
+  @Override
+  public Editor remove(String key) {
+    this.editor = this.editor.remove(prefix + key);
+    return this;
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/common/PrefsBranch.java
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.background.common;
+
+import java.util.Map;
+import java.util.Set;
+
+import android.content.SharedPreferences;
+
+/**
+ * A wrapper around a portion of the SharedPreferences space.
+ */
+public class PrefsBranch implements SharedPreferences {
+  private final SharedPreferences prefs;
+  private final String prefix;                // Including trailing period.
+
+  public PrefsBranch(SharedPreferences prefs, String prefix) {
+    if (!prefix.endsWith(".")) {
+      throw new IllegalArgumentException("No trailing period in prefix.");
+    }
+    this.prefs = prefs;
+    this.prefix = prefix;
+  }
+
+  @Override
+  public boolean contains(String key) {
+    return prefs.contains(prefix + key);
+  }
+
+  @Override
+  public Editor edit() {
+    return new EditorBranch(prefs, prefix);
+  }
+
+  @Override
+  public Map<String, ?> getAll() {
+    // Not implemented. TODO
+    return null;
+  }
+
+  @Override
+  public boolean getBoolean(String key, boolean defValue) {
+    return prefs.getBoolean(prefix + key, defValue);
+  }
+
+  @Override
+  public float getFloat(String key, float defValue) {
+    return prefs.getFloat(prefix + key, defValue);
+  }
+
+  @Override
+  public int getInt(String key, int defValue) {
+    return prefs.getInt(prefix + key, defValue);
+  }
+
+  @Override
+  public long getLong(String key, long defValue) {
+    return prefs.getLong(prefix + key, defValue);
+  }
+
+  @Override
+  public String getString(String key, String defValue) {
+    return prefs.getString(prefix + key, defValue);
+  }
+
+  // Not marking as Override, because Android <= 10 doesn't have
+  // getStringSet. Neither can we implement it.
+  public Set<String> getStringSet(String key, Set<String> defValue) {
+    throw new RuntimeException("getStringSet not available.");
+  }
+
+  @Override
+  public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+    prefs.registerOnSharedPreferenceChangeListener(listener);
+  }
+
+  @Override
+  public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+    prefs.unregisterOnSharedPreferenceChangeListener(listener);
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/sync/SyncConfiguration.java
+++ b/mobile/android/base/sync/SyncConfiguration.java
@@ -8,178 +8,27 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.mozilla.gecko.background.common.PrefsBranch;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.crypto.PersistedCrypto5Keys;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 
 public class SyncConfiguration {
-
-  public class EditorBranch implements Editor {
-
-    private final String prefix;
-    private Editor editor;
-
-    public EditorBranch(SyncConfiguration config, String prefix) {
-      if (!prefix.endsWith(".")) {
-        throw new IllegalArgumentException("No trailing period in prefix.");
-      }
-      this.prefix = prefix;
-      this.editor = config.getEditor();
-    }
-
-    public void apply() {
-      // Android <=r8 SharedPreferences.Editor does not contain apply() for overriding.
-      this.editor.commit();
-    }
-
-    @Override
-    public Editor clear() {
-      this.editor = this.editor.clear();
-      return this;
-    }
-
-    @Override
-    public boolean commit() {
-      return this.editor.commit();
-    }
-
-    @Override
-    public Editor putBoolean(String key, boolean value) {
-      this.editor = this.editor.putBoolean(prefix + key, value);
-      return this;
-    }
-
-    @Override
-    public Editor putFloat(String key, float value) {
-      this.editor = this.editor.putFloat(prefix + key, value);
-      return this;
-    }
-
-    @Override
-    public Editor putInt(String key, int value) {
-      this.editor = this.editor.putInt(prefix + key, value);
-      return this;
-    }
-
-    @Override
-    public Editor putLong(String key, long value) {
-      this.editor = this.editor.putLong(prefix + key, value);
-      return this;
-    }
-
-    @Override
-    public Editor putString(String key, String value) {
-      this.editor = this.editor.putString(prefix + key, value);
-      return this;
-    }
-
-    // Not marking as Override, because Android <= 10 doesn't have
-    // putStringSet. Neither can we implement it.
-    public Editor putStringSet(String key, Set<String> value) {
-      throw new RuntimeException("putStringSet not available.");
-    }
-
-    @Override
-    public Editor remove(String key) {
-      this.editor = this.editor.remove(prefix + key);
-      return this;
-    }
-
-  }
-
-  /**
-   * A wrapper around a portion of the SharedPreferences space.
-   *
-   * @author rnewman
-   *
-   */
-  public class ConfigurationBranch implements SharedPreferences {
-
-    private final SyncConfiguration config;
-    private final String prefix;                // Including trailing period.
-
-    public ConfigurationBranch(SyncConfiguration syncConfiguration,
-        String prefix) {
-      if (!prefix.endsWith(".")) {
-        throw new IllegalArgumentException("No trailing period in prefix.");
-      }
-      this.config = syncConfiguration;
-      this.prefix = prefix;
-    }
-
-    @Override
-    public boolean contains(String key) {
-      return config.getPrefs().contains(prefix + key);
-    }
-
-    @Override
-    public Editor edit() {
-      return new EditorBranch(config, prefix);
-    }
-
-    @Override
-    public Map<String, ?> getAll() {
-      // Not implemented. TODO
-      return null;
-    }
-
-    @Override
-    public boolean getBoolean(String key, boolean defValue) {
-      return config.getPrefs().getBoolean(prefix + key, defValue);
-    }
-
-    @Override
-    public float getFloat(String key, float defValue) {
-      return config.getPrefs().getFloat(prefix + key, defValue);
-    }
-
-    @Override
-    public int getInt(String key, int defValue) {
-      return config.getPrefs().getInt(prefix + key, defValue);
-    }
-
-    @Override
-    public long getLong(String key, long defValue) {
-      return config.getPrefs().getLong(prefix + key, defValue);
-    }
-
-    @Override
-    public String getString(String key, String defValue) {
-      return config.getPrefs().getString(prefix + key, defValue);
-    }
-
-    // Not marking as Override, because Android <= 10 doesn't have
-    // getStringSet. Neither can we implement it.
-    public Set<String> getStringSet(String key, Set<String> defValue) {
-      throw new RuntimeException("getStringSet not available.");
-    }
-
-    @Override
-    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
-      config.getPrefs().registerOnSharedPreferenceChangeListener(listener);
-    }
-
-    @Override
-    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
-      config.getPrefs().unregisterOnSharedPreferenceChangeListener(listener);
-    }
-  }
-
   private static final String LOG_TAG = "SyncConfiguration";
 
   // These must be set in GlobalSession's constructor.
   public URI             clusterURL;
   public KeyBundle       syncKeyBundle;
 
   public CollectionKeys  collectionKeys;
   public InfoCollections infoCollections;
@@ -294,21 +143,21 @@ public class SyncConfiguration {
       engineNames.add(stage.getRepositoryName());
     }
     return engineNames;
   }
 
   /**
    * Return a convenient accessor for part of prefs.
    * @return
-   *        A ConfigurationBranch object representing this
+   *        A PrefsBranch object representing this
    *        section of the preferences space.
    */
-  public ConfigurationBranch getBranch(String prefix) {
-    return new ConfigurationBranch(this, prefix);
+  public PrefsBranch getBranch(String prefix) {
+    return new PrefsBranch(this.getPrefs(), prefix);
   }
 
   /**
    * Gets the engine names that are enabled, declined, or other (depending on pref) in meta/global.
    *
    * @param prefs
    *          SharedPreferences that the engines are associated with.
    * @param pref
--- a/mobile/android/base/sync/SynchronizerConfiguration.java
+++ b/mobile/android/base/sync/SynchronizerConfiguration.java
@@ -2,41 +2,41 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync;
 
 import java.io.IOException;
 
 import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.PrefsBranch;
 import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.sync.SyncConfiguration.ConfigurationBranch;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 
 import android.content.SharedPreferences.Editor;
 
 public class SynchronizerConfiguration {
   private static final String LOG_TAG = "SynczrConfiguration";
 
   public String syncID;
   public RepositorySessionBundle remoteBundle;
   public RepositorySessionBundle localBundle;
 
-  public SynchronizerConfiguration(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException {
+  public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException {
     this.load(config);
   }
 
   public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) {
     this.syncID       = syncID;
     this.remoteBundle = remoteBundle;
     this.localBundle  = localBundle;
   }
 
   // This should get partly shuffled back into SyncConfiguration, I think.
-  public void load(ConfigurationBranch config) throws NonObjectJSONException, IOException, ParseException {
+  public void load(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException {
     if (config == null) {
       throw new IllegalArgumentException("config cannot be null.");
     }
     String remoteJSON = config.getString("remote", null);
     String localJSON  = config.getString("local",  null);
     RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON);
     RepositorySessionBundle lB = new RepositorySessionBundle(localJSON);
     if (remoteJSON == null) {
@@ -46,17 +46,17 @@ public class SynchronizerConfiguration {
       lB.setTimestamp(0);
     }
     syncID = config.getString("syncID", null);
     remoteBundle = rB;
     localBundle  = lB;
     Logger.debug(LOG_TAG, "Loaded SynchronizerConfiguration. syncID: " + syncID + ", remoteBundle: " + remoteBundle + ", localBundle: " + localBundle);
   }
 
-  public void persist(ConfigurationBranch config) {
+  public void persist(PrefsBranch config) {
     if (config == null) {
       throw new IllegalArgumentException("config cannot be null.");
     }
     String jsonRemote = remoteBundle.toJSONString();
     String jsonLocal  = localBundle.toJSONString();
     Editor editor = config.edit();
     editor.putString("remote", jsonRemote);
     editor.putString("local",  jsonLocal);
--- a/mobile/android/base/tests/BaseRobocopTest.java
+++ b/mobile/android/base/tests/BaseRobocopTest.java
@@ -66,17 +66,16 @@ public abstract class BaseRobocopTest ex
 
     protected Map<String, String> mConfig;
     protected String mRootPath;
 
     protected Solo mSolo;
     protected Driver mDriver;
     protected Actions mActions;
 
-    protected Activity mActivity;
     protected String mProfile;
 
     protected abstract Intent createActivityIntent();
 
     /**
      * The browser is started at the beginning of this test. A single test is a
      * class inheriting from <code>BaseRobocopTest</code> that contains test
      * methods.
@@ -135,22 +134,22 @@ public abstract class BaseRobocopTest ex
             mAsserter = new FennecMochitestAssert();
         }
         mAsserter.setLogFile(mLogFile);
         mAsserter.setTestName(getClass().getName());
 
         // Start the activity.
         final Intent intent = createActivityIntent();
         setActivityIntent(intent);
-        mActivity = getActivity();
 
         // Set up Robotium.solo and Driver objects
-        mSolo = new Solo(getInstrumentation(), mActivity);
-        mDriver = new FennecNativeDriver(mActivity, mSolo, mRootPath);
-        mActions = new FennecNativeActions(mActivity, mSolo, getInstrumentation(), mAsserter);
+        Activity tempActivity = getActivity();
+        mSolo = new Solo(getInstrumentation(), tempActivity);
+        mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath);
+        mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter);
     }
 
     /**
      * Function to early abort if we can't reach the given HTTP server. Provides local testers
      * with diagnostic information. Not currently available for TALOS tests, which are rarely run
      * locally in any case.
      */
     public void throwIfHttpGetFails() {
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -98,17 +98,17 @@ abstract class BaseTest extends BaseRobo
         }
     }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
         mDevice = new Device();
-        mDatabaseHelper = new DatabaseHelper(mActivity, mAsserter);
+        mDatabaseHelper = new DatabaseHelper(getActivity(), mAsserter);
 
         // Ensure Robocop tests have access to network, and are run with Display powered on.
         throwIfHttpGetFails();
         throwIfScreenNotOn();
     }
 
     protected GeckoProfile getTestProfile() {
         if (mProfile.startsWith("/")) {
@@ -727,17 +727,17 @@ abstract class BaseTest extends BaseRobo
      */
     public void closeTabAt(final int index) {
         View closeButton = getTabViewAt(index).findViewById(R.id.close);
 
         mSolo.clickOnView(closeButton);
     }
 
     public final void runOnUiThreadSync(Runnable runnable) {
-        RobocopUtils.runOnUiThreadSync(mActivity, runnable);
+        RobocopUtils.runOnUiThreadSync(getActivity(), runnable);
     }
 
     /* Tap the "star" (bookmark) button to bookmark or un-bookmark the current page */
     public void toggleBookmark() {
         mActions.sendSpecialKey(Actions.SpecialKey.MENU);
         waitForText("Settings");
 
         // On ICS+ phones, there is no button labeled "Bookmarks"
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -12,21 +12,16 @@ let Reader = {
   STATUS_UNFETCHED: 0,
   STATUS_FETCH_FAILED_TEMPORARY: 1,
   STATUS_FETCH_FAILED_PERMANENT: 2,
   STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT: 3,
   STATUS_FETCHED_ARTICLE: 4,
 
   observe: function Reader_observe(aMessage, aTopic, aData) {
     switch (aTopic) {
-      case "Reader:Added": {
-        let mm = window.getGroupMessageManager("browsers");
-        mm.broadcastAsyncMessage("Reader:Added", { url: aData });
-        break;
-      }
       case "Reader:Removed": {
         let uri = Services.io.newURI(aData, null, null);
         ReaderMode.removeArticleFromCache(uri).catch(e => Cu.reportError("Error removing article from cache: " + e));
 
         let mm = window.getGroupMessageManager("browsers");
         mm.broadcastAsyncMessage("Reader:Removed", { url: aData });
         break;
       }
@@ -51,35 +46,35 @@ let Reader = {
         ZoomHelper.zoomToRect(newRect, -1);
         break;
       }
     }
   },
 
   receiveMessage: function(message) {
     switch (message.name) {
-      case "Reader:AddToList":
-        this.addArticleToReadingList(message.data.article);
+      case "Reader:AddToList": {
+        // If the article is coming from reader mode, we must have fetched it already.
+        let article = message.data.article;
+        article.status = this.STATUS_FETCHED_ARTICLE;
+        this._addArticleToReadingList(article);
         break;
-
+      }
       case "Reader:ArticleGet":
         this._getArticle(message.data.url, message.target).then((article) => {
           message.target.messageManager.sendAsyncMessage("Reader:ArticleData", { article: article });
         });
         break;
 
       case "Reader:FaviconRequest": {
-        let observer = (s, t, d) => {
-          Services.obs.removeObserver(observer, "Reader:FaviconReturn", false);
-          message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(d));
-        };
-        Services.obs.addObserver(observer, "Reader:FaviconReturn", false);
-        Messaging.sendRequest({
+        Messaging.sendRequestForResult({
           type: "Reader:FaviconRequest",
           url: message.data.url
+        }).then(data => {
+          message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data));
         });
         break;
       }
 
       case "Reader:ListStatusRequest":
         Messaging.sendRequestForResult({
           type: "Reader:ListStatusRequest",
           url: message.data.url
@@ -195,41 +190,40 @@ let Reader = {
       return null;
     });
     if (!article) {
       // If there was a problem getting the article, just store the
       // URL and title from the tab.
       article = {
         url: urlWithoutRef,
         title: tab.browser.contentDocument.title,
+        length: 0,
+        excerpt: "",
         status: this.STATUS_FETCH_FAILED_UNSUPPORTED_FORMAT,
       };
     } else {
       article.status = this.STATUS_FETCHED_ARTICLE;
     }
 
-    this.addArticleToReadingList(article);
+    this._addArticleToReadingList(article);
   }),
 
-  addArticleToReadingList: function(article) {
-    if (!article || !article.url) {
-      Cu.reportError("addArticleToReadingList requires article with valid URL");
-      return;
-    }
-
-    Messaging.sendRequest({
+  _addArticleToReadingList: function(article) {
+    Messaging.sendRequestForResult({
       type: "Reader:AddToList",
       url: truncate(article.url, MAX_URI_LENGTH),
-      title: truncate(article.title || "", MAX_TITLE_LENGTH),
-      length: article.length || 0,
-      excerpt: article.excerpt || "",
+      title: truncate(article.title, MAX_TITLE_LENGTH),
+      length: article.length,
+      excerpt: article.excerpt,
       status: article.status,
-    });
-
-    ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e));
+    }).then((url) => {
+      let mm = window.getGroupMessageManager("browsers");
+      mm.broadcastAsyncMessage("Reader:Added", { url: url });
+      ReaderMode.storeArticleInCache(article).catch(e => Cu.reportError("Error storing article in cache: " + e));
+    }).catch(Cu.reportError);
   },
 
   /**
    * Gets an article for a given URL. This method will download and parse a document
    * if it does not find the article in the tab data or the cache.
    *
    * @param url The article URL.
    * @param browser The browser where the article is currently loaded.
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -141,17 +141,17 @@ let lazilyLoadedObserverScripts = [
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
   ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
   ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
-  ["Reader", ["Reader:Added", "Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"],
+  ["Reader", ["Reader:Removed", "Gesture:DoubleTap"], "chrome://browser/content/Reader.js"],
 ];
 if (AppConstants.MOZ_WEBRTC) {
   lazilyLoadedObserverScripts.push(
     ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"])
 }
 
 lazilyLoadedObserverScripts.forEach(function (aScript) {
   let [name, notifications, script] = aScript;
--- a/mobile/android/locales/filter.py
+++ b/mobile/android/locales/filter.py
@@ -29,15 +29,16 @@ def test(mod, path, entity = None):
     return "error"
 
   # we're in mod == "mobile"
   if re.match(r"searchplugins\/.+\.xml", path):
     return "ignore"
   if path == "chrome/region.properties":
     # only region.properties exceptions remain
     if (re.match(r"browser\.search\.order\.[1-9]", entity) or
+        re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or
         re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or
         re.match(r"gecko\.handlerService\.schemes\.", entity) or
         re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or
         re.match(r"browser\.suggestedsites\.", entity)):
       return "ignore"
 
   return "error"
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
@@ -336,17 +336,17 @@ public class SearchActivity extends Loca
         set.addListener(new Animator.AnimatorListener() {
             @Override
             public void onAnimationStart(Animator animation) {
             }
 
             @Override
             public void onAnimationEnd(Animator animation) {
                 // Don't do anything if the activity is destroyed before the animation ends.
-                if (SearchActivity.this.isDestroyed()) {
+                if (searchEngineManager == null) {
                     return;
                 }
 
                 setEditState(EditState.WAITING);
                 setSearchState(SearchState.POSTSEARCH);
 
                 // We need to manually clear the animation for the views to be hidden on gingerbread.
                 animationCard.clearAnimation();
--- a/mobile/locales/filter.py
+++ b/mobile/locales/filter.py
@@ -29,15 +29,16 @@ def test(mod, path, entity = None):
     return "error"
 
   # we're in mod == "mobile"
   if re.match(r"searchplugins\/.+\.xml", path):
     return "ignore"
   if path == "chrome/region.properties":
     # only region.properties exceptions remain
     if (re.match(r"browser\.search\.order\.[1-9]", entity) or
+        re.match(r"browser\.search\.[a-zA-Z]+\.US", entity) or
         re.match(r"browser\.contentHandlers\.types\.[0-5]", entity) or
         re.match(r"gecko\.handlerService\.schemes\.", entity) or
         re.match(r"gecko\.handlerService\.defaultHandlersVersion", entity) or
         re.match(r"browser\.suggestedsites\.", entity)):
       return "ignore"
 
   return "error"
--- a/services/sync/modules/FxaMigrator.jsm
+++ b/services/sync/modules/FxaMigrator.jsm
@@ -519,14 +519,28 @@ Migrator.prototype = {
       case this.TELEMETRY_ACCEPTED:
       case this.TELEMETRY_UNLINKED:
       case this.TELEMETRY_DECLINED:
         Services.obs.notifyObservers(null, OBSERVER_INTERNAL_TELEMETRY_TOPIC, flag);
         break;
       default:
         throw new Error("Unexpected telemetry flag: " + flag);
     }
-  }
-}
+  },
+
+  get learnMoreLink() {
+    try {
+      var url = Services.prefs.getCharPref("app.support.baseURL");
+    } catch (err) {
+      return null;
+    }
+    url += "sync-upgrade";
+    let sb = Services.strings.createBundle("chrome://weave/locale/services/sync.properties");
+    return {
+      text: sb.GetStringFromName("sync.eol.learnMore.label"),
+      href: Services.urlFormatter.formatURL(url),
+    };
+  },
+};
 
 // We expose a singleton
 this.EXPORTED_SYMBOLS = ["fxaMigrator"];
 let fxaMigrator = new Migrator();
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -79,28 +79,31 @@ this.Notifications = {
   }
 };
 
 
 /**
  * A basic notification.  Subclass this to create more complex notifications.
  */
 this.Notification =
- function Notification(title, description, iconURL, priority, buttons) {
+function Notification(title, description, iconURL, priority, buttons, link) {
   this.title = title;
   this.description = description;
 
   if (iconURL)
     this.iconURL = iconURL;
 
   if (priority)
     this.priority = priority;
 
   if (buttons)
     this.buttons = buttons;
+
+  if (link)
+    this.link = link;
 }
 
 // We set each prototype property individually instead of redefining
 // the entire prototype to avoid blowing away existing properties
 // of the prototype like the the "constructor" property, which we use
 // to bind notification objects to their XBL representations.
 Notification.prototype.priority = Notifications.PRIORITY_INFO;
 Notification.prototype.iconURL = null;
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -103,20 +103,17 @@ function DBConn(aForceNewConnection) {
 
   // If the Places database connection has been closed, create a new connection.
   if (!gDBConn || aForceNewConnection) {
     let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
     file.append("places.sqlite");
     let dbConn = gDBConn = Services.storage.openDatabase(file);
 
     // Be sure to cleanly close this connection.
-    Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(DBCloseCallback, aTopic);
-      dbConn.asyncClose();
-    }, "profile-before-change", false);
+    promiseTopicObserved("profile-before-change").then(() => dbConn.asyncClose());
   }
 
   return gDBConn.connectionReady ? gDBConn : null;
 };
 
 /**
  * Reads data from the provided inputstream.
  *
@@ -374,25 +371,22 @@ function check_no_bookmarks() {
  *        Notification topic to observe.
  *
  * @return {Promise}
  * @resolves The array [aSubject, aData] from the observed notification.
  * @rejects Never.
  */
 function promiseTopicObserved(aTopic)
 {
-  let deferred = Promise.defer();
-
-  Services.obs.addObserver(
-    function PTO_observe(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(PTO_observe, aTopic);
-      deferred.resolve([aSubject, aData]);
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observe(aSubject, aTopic, aData) {
+      Services.obs.removeObserver(observe, aTopic);
+      resolve([aSubject, aData]);
     }, aTopic, false);
-
-  return deferred.promise;
+  });
 }
 
 /**
  * Simulates a Places shutdown.
  */
 function shutdownPlaces(aKeepAliveConnection)
 {
   let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
@@ -601,20 +595,17 @@ function is_time_ordered(before, after) 
 /**
  * Shutdowns Places, invoking the callback when the connection has been closed.
  *
  * @param aCallback
  *        Function to be called when done.
  */
 function waitForConnectionClosed(aCallback)
 {
-  Services.obs.addObserver(function WFCCCallback() {
-    Services.obs.removeObserver(WFCCCallback, "places-connection-closed");
-    aCallback();
-  }, "places-connection-closed", false);
+  promiseTopicObserved("places-connection-closed").then(aCallback);
   shutdownPlaces();
 }
 
 /**
  * Tests if a given guid is valid for use in Places or not.
  *
  * @param aGuid
  *        The guid to test.
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -715,19 +715,20 @@ let Links = {
   maxNumLinks: LINKS_GET_LINKS_LIMIT,
 
   /**
    * The link providers.
    */
   _providers: new Set(),
 
   /**
-   * A mapping from each provider to an object { sortedLinks, linkMap }.
-   * sortedLinks is the cached, sorted array of links for the provider.  linkMap
-   * is a Map from link URLs to link objects.
+   * A mapping from each provider to an object { sortedLinks, siteMap, linkMap }.
+   * sortedLinks is the cached, sorted array of links for the provider.
+   * siteMap is a mapping from base domains to URL count associated with the domain.
+   * linkMap is a Map from link URLs to link objects.
    */
   _providerLinks: new Map(),
 
   /**
    * The properties of link objects used to sort them.
    */
   _sortProperties: [
     "frecency",
@@ -857,16 +858,31 @@ let Links = {
       if (!(prop in aLink1) || !(prop in aLink2))
         throw new Error("Comparable link missing required property: " + prop);
     }
     return aLink2.frecency - aLink1.frecency ||
            aLink2.lastVisitDate - aLink1.lastVisitDate ||
            aLink1.url.localeCompare(aLink2.url);
   },
 
+  _incrementSiteMap: function(map, link) {
+    let site = NewTabUtils.extractSite(link.url);
+    map.set(site, (map.get(site) || 0) + 1);
+  },
+
+  _decrementSiteMap: function(map, link) {
+    let site = NewTabUtils.extractSite(link.url);
+    let previousURLCount = map.get(site);
+    if (previousURLCount === 1) {
+      map.delete(site);
+    } else {
+      map.set(site, previousURLCount - 1);
+    }
+  },
+
   /**
    * Calls getLinks on the given provider and populates our cache for it.
    * @param aProvider The provider whose cache will be populated.
    * @param aCallback The callback to call when finished.
    * @param aForce When true, populates the provider's cache even when it's
    *               already filled.
    */
   _populateProviderCache: function Links_populateProviderCache(aProvider, aCallback, aForce) {
@@ -874,16 +890,20 @@ let Links = {
       aCallback();
     } else {
       aProvider.getLinks(links => {
         // Filter out null and undefined links so we don't have to deal with
         // them in getLinks when merging links from providers.
         links = links.filter((link) => !!link);
         this._providerLinks.set(aProvider, {
           sortedLinks: links,
+          siteMap: links.reduce((map, link) => {
+            this._incrementSiteMap(map, link);
+            return map;
+          }, new Map()),
           linkMap: links.reduce((map, link) => {
             map.set(link.url, link);
             return map;
           }, new Map()),
         });
         aCallback();
       });
     }
@@ -933,17 +953,17 @@ let Links = {
 
     let links = this._providerLinks.get(aProvider);
     if (!links)
       // This is not an error, it just means that between the time the provider
       // was added and the future time we call getLinks on it, it notified us of
       // a change.
       return;
 
-    let { sortedLinks, linkMap } = links;
+    let { sortedLinks, siteMap, linkMap } = links;
     let existingLink = linkMap.get(aLink.url);
     let insertionLink = null;
     let updatePages = false;
 
     if (existingLink) {
       // Update our copy's position in O(lg n) by first removing it from its
       // list.  It's important to do this before modifying its properties.
       if (this._sortProperties.some(prop => prop in aLink)) {
@@ -978,24 +998,26 @@ let Links = {
       }
       // Copy the link object so that changes later made to it by the caller
       // don't affect our copy.
       insertionLink = {};
       for (let prop in aLink) {
         insertionLink[prop] = aLink[prop];
       }
       linkMap.set(aLink.url, insertionLink);
+      this._incrementSiteMap(siteMap, aLink);
     }
 
     if (insertionLink) {
       let idx = this._insertionIndexOf(sortedLinks, insertionLink);
       sortedLinks.splice(idx, 0, insertionLink);
       if (sortedLinks.length > aProvider.maxNumLinks) {
         let lastLink = sortedLinks.pop();
         linkMap.delete(lastLink.url);
+        this._decrementSiteMap(siteMap, lastLink);
       }
       updatePages = true;
     }
 
     if (updatePages) {
       AllPages.update(null, "links-changed");
     }
   },
@@ -1182,16 +1204,24 @@ this.NewTabUtils = {
       this._initialized = true;
       ExpirationFilter.init();
       Telemetry.init();
       return true;
     }
     return false;
   },
 
+  isTopSiteGivenProvider: function(aSite, aProvider) {
+    return Links._providerLinks.get(aProvider).siteMap.has(aSite);
+  },
+
+  isTopPlacesSite: function(aSite) {
+    return this.isTopSiteGivenProvider(aSite, PlacesProvider);
+  },
+
   /**
    * Restores all sites that have been removed from the grid.
    */
   restore: function NewTabUtils_restore() {
     Storage.clear();
     Links.resetCache();
     PinnedLinks.resetCache();
     BlockedLinks.resetCache();
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -6,17 +6,56 @@
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 function run_test() {
   run_next_test();
 }
 
-add_test(function multipleProviders() {
+add_task(function isTopSiteGivenProvider() {
+  let expectedLinks = makeLinks(0, 10, 2);
+
+  // The lowest 2 frecencies have the same base domain.
+  expectedLinks[expectedLinks.length - 2].url = expectedLinks[expectedLinks.length - 1].url + "Test";
+
+  let provider = new TestProvider(done => done(expectedLinks));
+  provider.maxNumLinks = expectedLinks.length;
+
+  NewTabUtils.initWithoutProviders();
+  NewTabUtils.links.addProvider(provider);
+  NewTabUtils.links.populateCache(function () {}, false);
+
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example1.com", provider), false);
+
+  // Push out frecency 2 because the maxNumLinks is reached when adding frecency 3
+  let newLink = makeLink(3);
+  provider.notifyLinkChanged(newLink);
+
+  // There is still a frecent url with example2 domain, so it's still frecent.
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example3.com", provider), true);
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), true);
+
+  // Push out frecency 3
+  newLink = makeLink(5);
+  provider.notifyLinkChanged(newLink);
+
+  // Push out frecency 4
+  newLink = makeLink(9);
+  provider.notifyLinkChanged(newLink);
+
+  // Our count reached 0 for the example2.com domain so it's no longer a frecent site.
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example5.com", provider), true);
+  do_check_eq(NewTabUtils.isTopSiteGivenProvider("example2.com", provider), false);
+
+  NewTabUtils.links.removeProvider(provider);
+});
+
+add_task(function multipleProviders() {
   // Make each provider generate NewTabUtils.links.maxNumLinks links to check
   // that no more than maxNumLinks are actually returned in the merged list.
   let evenLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks, 2);
   let evenProvider = new TestProvider(done => done(evenLinks));
   let oddLinks = makeLinks(0, 2 * NewTabUtils.links.maxNumLinks - 1, 2);
   let oddProvider = new TestProvider(done => done(oddLinks));
 
   NewTabUtils.initWithoutProviders();
@@ -30,20 +69,19 @@ add_test(function multipleProviders() {
   let expectedLinks = makeLinks(NewTabUtils.links.maxNumLinks,
                                 2 * NewTabUtils.links.maxNumLinks,
                                 1);
   do_check_eq(links.length, NewTabUtils.links.maxNumLinks);
   do_check_links(links, expectedLinks);
 
   NewTabUtils.links.removeProvider(evenProvider);
   NewTabUtils.links.removeProvider(oddProvider);
-  run_next_test();
 });
 
-add_test(function changeLinks() {
+add_task(function changeLinks() {
   let expectedLinks = makeLinks(0, 20, 2);
   let provider = new TestProvider(done => done(expectedLinks));
 
   NewTabUtils.initWithoutProviders();
   NewTabUtils.links.addProvider(provider);
 
   // This is sync since the provider's getLinks is sync.
   NewTabUtils.links.populateCache(function () {}, false);
@@ -86,17 +124,16 @@ add_test(function changeLinks() {
   // Notify of many links changed.
   expectedLinks = makeLinks(0, 3, 1);
   provider.notifyManyLinksChanged();
   // NewTabUtils.links will now repopulate its cache, which is sync since
   // the provider's getLinks is sync.
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   NewTabUtils.links.removeProvider(provider);
-  run_next_test();
 });
 
 add_task(function oneProviderAlreadyCached() {
   let links1 = makeLinks(0, 10, 1);
   let provider1 = new TestProvider(done => done(links1));
 
   NewTabUtils.initWithoutProviders();
   NewTabUtils.links.addProvider(provider1);
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -20,9 +20,10 @@ run-if = appname == "firefox"
 [test_provider_markSafe.js]
 [test_provider_shutdown.js]
 [test_provider_unsafe_access_shutdown.js]
 [test_provider_unsafe_access_startup.js]
 [test_shutdown.js]
 [test_XPIcancel.js]
 [test_XPIStates.js]
 
+
 [include:xpcshell-shared.ini]
--- a/toolkit/themes/linux/global/notification.css
+++ b/toolkit/themes/linux/global/notification.css
@@ -1,28 +1,44 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
+notification,
+.messageText > .text-link {
+  color: InfoText !important;
+}
+
 notification {
   background-color: InfoBackground;
-  color: InfoText;
   text-shadow: none;
 }
 
+notification[type="info"],
+notification[type="info"] .messageText > .text-link {
+  color: -moz-DialogText !important;
+}
+
 notification[type="info"] {
   background-color: -moz-Dialog;
-  color: -moz-DialogText;
+}
+
+notification[type="critical"],
+notification[type="critical"] .messageText > .text-link {
+  color: white !important;
 }
 
 notification[type="critical"] {
   background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
-  color: white;
+}
+
+.messageText > .text-link {
+  text-decoration: underline;
 }
 
 .notification-inner {
   padding-top: 1px;
   padding-bottom: 1px;
 }
 
 .messageText {
--- a/toolkit/themes/osx/global/notification.css
+++ b/toolkit/themes/osx/global/notification.css
@@ -5,37 +5,53 @@
 %include shared.inc
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
 notification {
   padding: 3px 3px 4px;
   text-shadow: none;
 }
 
+notification[type="info"],
+notification[type="info"] .messageText > .text-link {
+  color: rgba(255,255,255,0.95) !important;
+}
+
 notification[type="info"] {
-  color: rgba(255,255,255,0.95);
   background: url("chrome://global/skin/notification/info-bar-background.png") #404040 repeat-x top left;
   border-top: 1px solid #707070;
   border-bottom: 1px solid #2a2a2a;
 }
 
+notification[type="warning"],
+notification[type="warning"] .messageText > .text-link {
+  color: rgba(0,0,0,0.95) !important;
+}
+
 notification[type="warning"] {
-  color: rgba(0,0,0,0.95);
   background: url("chrome://global/skin/notification/warning-bar-background.png") #ffc703 repeat-x top left;
   border-top: 1px solid #ffe970;
   border-bottom: 1px solid #bf8a01;
 }
 
+notification[type="critical"],
+notification[type="critical"] .messageText > .text-link {
+  color: rgba(255,255,255,0.95) !important;
+}
+
 notification[type="critical"] {
-  color: rgba(255,255,255,0.95);
   background: url("chrome://global/skin/notification/critical-bar-background.png") #980000 repeat-x top left;
   border-top: 1px solid #e35959;
   border-bottom: 1px solid #5d0000;
 }
 
+.messageText > .text-link {
+  text-decoration: underline;
+}
+
 .messageImage {
   width: 16px;
   height: 16px;
   margin: 0 4px;
 }
 
 /* Default icons for notifications */
 
--- a/toolkit/themes/windows/global/notification.css
+++ b/toolkit/themes/windows/global/notification.css
@@ -1,28 +1,44 @@
 /* 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/. */
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
+notification,
+.messageText > .text-link {
+  color: InfoText !important;
+}
+
 notification {
   background-color: InfoBackground;
-  color: InfoText;
   text-shadow: none;
 }
 
+notification[type="info"],
+notification[type="info"] .messageText > .text-link {
+  color: -moz-DialogText !important;
+}
+
 notification[type="info"] {
   background-color: -moz-Dialog;
-  color: -moz-DialogText;
+}
+
+notification[type="critical"],
+notification[type="critical"] .messageText > .text-link {
+  color: white !important;
 }
 
 notification[type="critical"] {
   background-image: linear-gradient(rgb(212,0,0), rgb(152,0,0));
-  color: white;
+}
+
+.messageText > .text-link {
+  text-decoration: underline;
 }
 
 .messageImage {
   width: 16px;
   height: 16px;
   -moz-margin-start: 6px;
   -moz-margin-end: 1px;
 }