Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 04 Feb 2015 17:21:04 -0800
changeset 227534 34a66aaaca81826f0b838016aa15a077b50fa2fc
parent 227505 505ff14b24ee2630c8f7395d278659de2d302f9e (current diff)
parent 227533 cb757c948bc0800c43760407ab9b316d5fa04360 (diff)
child 227537 2037891bcdc84fdacd7e9b93f1b9cedc5f73b804
child 227568 04dda44ac71a4907a06e4445f1f17114e783ed3b
child 227630 d381f2e0d41799ecebfc96486a5434148fdde90a
push id28232
push userkwierso@gmail.com
push dateThu, 05 Feb 2015 01:21:17 +0000
treeherdermozilla-central@34a66aaaca81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
34a66aaaca81 / 38.0a1 / 20150205030205 / files
nightly linux64
34a66aaaca81 / 38.0a1 / 20150205030205 / files
nightly mac
34a66aaaca81 / 38.0a1 / 20150205030205 / files
nightly win32
34a66aaaca81 / 38.0a1 / 20150205030205 / files
nightly win64
34a66aaaca81 / 38.0a1 / 20150205030205 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c 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;
 }