Bug 852041 - Part 1: Use BookmarkJSONUtils.exportToFile in browser and toolkit components. r=mano
authorRaymond Lee <raymond@raysquare.com>
Tue, 09 Apr 2013 16:23:40 +0800
changeset 128313 d946c9ecdea6e72f3788aaa53640b5c3fdfeb98c
parent 128312 f10884c6a91e01699b19aff130f58b7b2649252c
child 128314 7b3b57c68f998929243c068a743fd26411ad228a
push idunknown
push userunknown
push dateunknown
reviewersmano
bugs852041
milestone23.0a1
Bug 852041 - Part 1: Use BookmarkJSONUtils.exportToFile in browser and toolkit components. r=mano
browser/components/nsBrowserGlue.js
browser/components/places/content/places.js
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
toolkit/components/places/tests/bookmarks/test_448584.js
toolkit/components/places/tests/bookmarks/test_458683.js
toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/unit/test_384370.js
toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
toolkit/components/places/tests/unit/test_utils_backups_create.js
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -51,16 +51,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource:///modules/webrtcUI.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
                                   "resource:///modules/RecentWindow.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // We try to backup bookmarks at idle times, to avoid doing that at shutdown.
 // Number of idle seconds before trying to backup bookmarks.  15 minutes.
 const BOOKMARKS_BACKUP_IDLE_TIME = 15 * 60;
 // Minimum interval in milliseconds between backups.
 const BOOKMARKS_BACKUP_INTERVAL = 86400 * 1000;
@@ -231,17 +234,18 @@ BrowserGlue.prototype = {
         if (this._isPlacesShutdownObserver) {
           Services.obs.removeObserver(this, "places-shutdown");
           this._isPlacesShutdownObserver = false;
         }
         // places-shutdown is fired when the profile is about to disappear.
         this._onPlacesShutdown();
         break;
       case "idle":
-        if (this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000)
+        if ((this._idleService.idleTime > BOOKMARKS_BACKUP_IDLE_TIME * 1000) &&
+             this._shouldBackupBookmarks())
           this._backupBookmarks();
         break;
       case "distribution-customization-complete":
         Services.obs.removeObserver(this, "distribution-customization-complete");
         // Customization has finished, we don't need the customizer anymore.
         delete this._distributionCustomizer;
         break;
       case "browser-glue-test": // used by tests
@@ -976,136 +980,139 @@ BrowserGlue.prototype = {
     var importBookmarksHTML = false;
     try {
       importBookmarksHTML =
         Services.prefs.getBoolPref("browser.places.importBookmarksHTML");
       if (importBookmarksHTML)
         importBookmarks = true;
     } catch(ex) {}
 
-    // Check if Safe Mode or the user has required to restore bookmarks from
-    // default profile's bookmarks.html
-    var restoreDefaultBookmarks = false;
-    try {
-      restoreDefaultBookmarks =
-        Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks");
-      if (restoreDefaultBookmarks) {
-        // Ensure that we already have a bookmarks backup for today.
-        this._backupBookmarks();
-        importBookmarks = true;
-      }
-    } catch(ex) {}
+    Task.spawn(function() {
+      // Check if Safe Mode or the user has required to restore bookmarks from
+      // default profile's bookmarks.html
+      var restoreDefaultBookmarks = false;
+      try {
+        restoreDefaultBookmarks =
+          Services.prefs.getBoolPref("browser.bookmarks.restore_default_bookmarks");
+        if (restoreDefaultBookmarks) {
+          // Ensure that we already have a bookmarks backup for today.
+          if (this._shouldBackupBookmarks())
+            yield this._backupBookmarks();
+          importBookmarks = true;
+        }
+      } catch(ex) {}
 
-    // If the user did not require to restore default bookmarks, or import
-    // from bookmarks.html, we will try to restore from JSON
-    if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
-      // get latest JSON backup
-      var bookmarksBackupFile = PlacesUtils.backups.getMostRecent("json");
-      if (bookmarksBackupFile) {
-        // restore from JSON backup
-        PlacesUtils.restoreBookmarksFromJSONFile(bookmarksBackupFile);
-        importBookmarks = false;
-      }
-      else {
-        // We have created a new database but we don't have any backup available
-        importBookmarks = true;
-        var dirService = Cc["@mozilla.org/file/directory_service;1"].
-                         getService(Ci.nsIProperties);
-        var bookmarksHTMLFile = dirService.get("BMarks", Ci.nsILocalFile);
-        if (bookmarksHTMLFile.exists()) {
-          // If bookmarks.html is available in current profile import it...
-          importBookmarksHTML = true;
+      // If the user did not require to restore default bookmarks, or import
+      // from bookmarks.html, we will try to restore from JSON
+      if (importBookmarks && !restoreDefaultBookmarks && !importBookmarksHTML) {
+        // get latest JSON backup
+        var bookmarksBackupFile = PlacesUtils.backups.getMostRecent("json");
+        if (bookmarksBackupFile) {
+          // restore from JSON backup
+          PlacesUtils.restoreBookmarksFromJSONFile(bookmarksBackupFile);
+          importBookmarks = false;
         }
         else {
-          // ...otherwise we will restore defaults
-          restoreDefaultBookmarks = true;
+          // We have created a new database but we don't have any backup available
+          importBookmarks = true;
+          var dirService = Cc["@mozilla.org/file/directory_service;1"].
+                           getService(Ci.nsIProperties);
+          var bookmarksHTMLFile = dirService.get("BMarks", Ci.nsILocalFile);
+          if (bookmarksHTMLFile.exists()) {
+            // If bookmarks.html is available in current profile import it...
+            importBookmarksHTML = true;
+          }
+          else {
+            // ...otherwise we will restore defaults
+            restoreDefaultBookmarks = true;
+          }
         }
       }
-    }
 
-    // If bookmarks are not imported, then initialize smart bookmarks.  This
-    // happens during a common startup.
-    // Otherwise, if any kind of import runs, smart bookmarks creation should be
-    // delayed till the import operations has finished.  Not doing so would
-    // cause them to be overwritten by the newly imported bookmarks.
-    if (!importBookmarks) {
-      // Now apply distribution customized bookmarks.
-      // This should always run after Places initialization.
-      this._distributionCustomizer.applyBookmarks();
-      this.ensurePlacesDefaultQueriesInitialized();
-    }
-    else {
-      // An import operation is about to run.
-      // Don't try to recreate smart bookmarks if autoExportHTML is true or
-      // smart bookmarks are disabled.
-      var autoExportHTML = false;
-      try {
-        autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML");
-      } catch(ex) {}
-      var smartBookmarksVersion = 0;
-      try {
-        smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion");
-      } catch(ex) {}
-      if (!autoExportHTML && smartBookmarksVersion != -1)
-        Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
-
-      // Get bookmarks.html file location
-      var dirService = Cc["@mozilla.org/file/directory_service;1"].
-                       getService(Ci.nsIProperties);
-
-      var bookmarksURI = null;
-      if (restoreDefaultBookmarks) {
-        // User wants to restore bookmarks.html file from default profile folder
-        bookmarksURI = NetUtil.newURI("resource:///defaults/profile/bookmarks.html");
+      // If bookmarks are not imported, then initialize smart bookmarks.  This
+      // happens during a common startup.
+      // Otherwise, if any kind of import runs, smart bookmarks creation should be
+      // delayed till the import operations has finished.  Not doing so would
+      // cause them to be overwritten by the newly imported bookmarks.
+      if (!importBookmarks) {
+        // Now apply distribution customized bookmarks.
+        // This should always run after Places initialization.
+        this._distributionCustomizer.applyBookmarks();
+        this.ensurePlacesDefaultQueriesInitialized();
       }
       else {
-        var bookmarksFile = dirService.get("BMarks", Ci.nsILocalFile);
-        if (bookmarksFile.exists())
-          bookmarksURI = NetUtil.newURI(bookmarksFile);
+        // An import operation is about to run.
+        // Don't try to recreate smart bookmarks if autoExportHTML is true or
+        // smart bookmarks are disabled.
+        var autoExportHTML = false;
+        try {
+          autoExportHTML = Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML");
+        } catch(ex) {}
+        var smartBookmarksVersion = 0;
+        try {
+          smartBookmarksVersion = Services.prefs.getIntPref("browser.places.smartBookmarksVersion");
+        } catch(ex) {}
+        if (!autoExportHTML && smartBookmarksVersion != -1)
+          Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
+
+        // Get bookmarks.html file location
+        var dirService = Cc["@mozilla.org/file/directory_service;1"].
+                         getService(Ci.nsIProperties);
+
+        var bookmarksURI = null;
+        if (restoreDefaultBookmarks) {
+          // User wants to restore bookmarks.html file from default profile folder
+          bookmarksURI = NetUtil.newURI("resource:///defaults/profile/bookmarks.html");
+        }
+        else {
+          var bookmarksFile = dirService.get("BMarks", Ci.nsILocalFile);
+          if (bookmarksFile.exists())
+            bookmarksURI = NetUtil.newURI(bookmarksFile);
+        }
+
+        if (bookmarksURI) {
+          // Import from bookmarks.html file.
+          try {
+            BookmarkHTMLUtils.importFromURL(bookmarksURI.spec, true).then(null,
+              function onFailure() {
+                Cu.reportError("Bookmarks.html file could be corrupt.");
+              }
+            ).then(
+              function onComplete() {
+                // Now apply distribution customized bookmarks.
+                // This should always run after Places initialization.
+                this._distributionCustomizer.applyBookmarks();
+                // Ensure that smart bookmarks are created once the operation is
+                // complete.
+                this.ensurePlacesDefaultQueriesInitialized();
+              }.bind(this)
+            );
+          } catch (err) {
+            Cu.reportError("Bookmarks.html file could be corrupt. " + err);
+          }
+        }
+        else {
+          Cu.reportError("Unable to find bookmarks.html file.");
+        }
+
+        // Reset preferences, so we won't try to import again at next run
+        if (importBookmarksHTML)
+          Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false);
+        if (restoreDefaultBookmarks)
+          Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks",
+                                     false);
       }
 
-      if (bookmarksURI) {
-        // Import from bookmarks.html file.
-        try {
-          BookmarkHTMLUtils.importFromURL(bookmarksURI.spec, true).then(null,
-            function onFailure() {
-              Cu.reportError("Bookmarks.html file could be corrupt.");
-            }
-          ).then(
-            function onComplete() {
-              // Now apply distribution customized bookmarks.
-              // This should always run after Places initialization.
-              this._distributionCustomizer.applyBookmarks();
-              // Ensure that smart bookmarks are created once the operation is
-              // complete.
-              this.ensurePlacesDefaultQueriesInitialized();
-            }.bind(this)
-          );
-        } catch (err) {
-          Cu.reportError("Bookmarks.html file could be corrupt. " + err);
-        }
+      // Initialize bookmark archiving on idle.
+      // Once a day, either on idle or shutdown, bookmarks are backed up.
+      if (!this._isIdleObserver) {
+        this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
+        this._isIdleObserver = true;
       }
-      else {
-        Cu.reportError("Unable to find bookmarks.html file.");
-      }
-
-      // Reset preferences, so we won't try to import again at next run
-      if (importBookmarksHTML)
-        Services.prefs.setBoolPref("browser.places.importBookmarksHTML", false);
-      if (restoreDefaultBookmarks)
-        Services.prefs.setBoolPref("browser.bookmarks.restore_default_bookmarks",
-                                   false);
-    }
-
-    // Initialize bookmark archiving on idle.
-    // Once a day, either on idle or shutdown, bookmarks are backed up.
-    if (!this._isIdleObserver) {
-      this._idleService.addIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
-      this._isIdleObserver = true;
-    }
+    }.bind(this));
   },
 
   /**
    * Places shut-down tasks
    * - back up bookmarks if needed.
    * - export bookmarks as HTML, if so configured.
    * - finalize components depending on Places.
    */
@@ -1113,65 +1120,87 @@ BrowserGlue.prototype = {
     this._sanitizer.onShutdown();
     PageThumbs.uninit();
 
     if (this._isIdleObserver) {
       this._idleService.removeIdleObserver(this, BOOKMARKS_BACKUP_IDLE_TIME);
       this._isIdleObserver = false;
     }
 
-    this._backupBookmarks();
+    let waitingForBackupToComplete = true;
+    if (this._shouldBackupBookmarks()) {
+      waitingForBackupToComplete = false;
+      this._backupBookmarks().then(
+        function onSuccess() {
+          waitingForBackupToComplete = true;
+        },
+        function onFailure() {
+          Cu.reportError("Unable to backup bookmarks.");
+          waitingForBackupToComplete = true;
+        }
+      );
+    }
 
     // Backup bookmarks to bookmarks.html to support apps that depend
     // on the legacy format.
-    try {
-      // If this fails to get the preference value, we don't export.
-      if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
-        // Exceptionally, since this is a non-default setting and HTML format is
-        // discouraged in favor of the JSON backups, we spin the event loop on
-        // shutdown, to wait for the export to finish.  We cannot safely spin
-        // the event loop on shutdown until we include a watchdog to prevent
-        // potential hangs (bug 518683).  The asynchronous shutdown operations
-        // will then be handled by a shutdown service (bug 435058).
-        let shutdownComplete = false;
-        BookmarkHTMLUtils.exportToFile(FileUtils.getFile("BMarks", [])).then(
-          function onSuccess() {
-            shutdownComplete = true;
-          },
-          function onFailure() {
-            // There is no point in reporting errors since we are shutting down.
-            shutdownComplete = true;
-          }
-        );
-        let thread = Services.tm.currentThread;
-        while (!shutdownComplete) {
-          thread.processNextEvent(true);
+    let waitingForHTMLExportToComplete = true;
+    // If this fails to get the preference value, we don't export.
+    if (Services.prefs.getBoolPref("browser.bookmarks.autoExportHTML")) {
+      // Exceptionally, since this is a non-default setting and HTML format is
+      // discouraged in favor of the JSON backups, we spin the event loop on
+      // shutdown, to wait for the export to finish.  We cannot safely spin
+      // the event loop on shutdown until we include a watchdog to prevent
+      // potential hangs (bug 518683).  The asynchronous shutdown operations
+      // will then be handled by a shutdown service (bug 435058).
+      waitingForHTMLExportToComplete = false;
+      BookmarkHTMLUtils.exportToFile(FileUtils.getFile("BMarks", [])).then(
+        function onSuccess() {
+          waitingForHTMLExportToComplete = true;
+        },
+        function onFailure() {
+          Cu.reportError("Unable to auto export html.");
+          waitingForHTMLExportToComplete = true;
         }
-      }
-    } catch(ex) { /* Don't export */ }
+      );
+    }
+
+    let thread = Services.tm.currentThread;
+    while (!waitingForBackupToComplete || !waitingForHTMLExportToComplete) {
+      thread.processNextEvent(true);
+    }
   },
 
   /**
-   * Backup bookmarks if needed.
+   * Determine whether to backup bookmarks or not.
+   * @return true if bookmarks should be backed up, false if not.
    */
-  _backupBookmarks: function BG__backupBookmarks() {
+  _shouldBackupBookmarks: function BG__shouldBackupBookmarks() {
     let lastBackupFile = PlacesUtils.backups.getMostRecent();
 
-    // Backup bookmarks if there are no backups or the maximum interval between
+    // Should backup bookmarks if there are no backups or the maximum interval between
     // backups elapsed.
-    if (!lastBackupFile ||
-        new Date() - PlacesUtils.backups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL) {
+    return (!lastBackupFile ||
+            new Date() - PlacesUtils.backups.getDateForFile(lastBackupFile) > BOOKMARKS_BACKUP_INTERVAL);
+  },
+
+  /**
+   * Backup bookmarks.
+   */
+  _backupBookmarks: function BG__backupBookmarks() {
+    return Task.spawn(function() {
+      // Backup bookmarks if there are no backups or the maximum interval between
+      // backups elapsed.
       let maxBackups = BOOKMARKS_BACKUP_MAX_BACKUPS;
       try {
         maxBackups = Services.prefs.getIntPref("browser.bookmarks.max_backups");
       }
       catch(ex) { /* Use default. */ }
 
-      PlacesUtils.backups.create(maxBackups); // Don't force creation.
-    }
+      yield PlacesUtils.backups.create(maxBackups); // Don't force creation.
+    });
   },
 
   /**
    * Show the notificationBox for a locked places database.
    */
   _showPlacesLockedNotificationBox: function BG__showPlacesLockedNotificationBox() {
     var brandBundle  = Services.strings.createBundle("chrome://branding/locale/brand.properties");
     var applicationName = brandBundle.GetStringFromName("brandShortName");
--- a/browser/components/places/content/places.js
+++ b/browser/components/places/content/places.js
@@ -465,17 +465,17 @@ var PlacesOrganizer = {
    */
   backupBookmarks: function PO_backupBookmarks() {
     let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
                  getService(Ci.nsIProperties);
     let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     let fpCallback = function fpCallback_done(aResult) {
       if (aResult != Ci.nsIFilePicker.returnCancel) {
-        PlacesUtils.backups.saveBookmarksToJSONFile(fp.file);
+        BookmarkJSONUtils.exportToFile(fp.file);
       }
     };
 
     fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
             Ci.nsIFilePicker.modeSave);
     fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
                     PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
     fp.defaultString = PlacesUtils.backups.getFilenameForDate();
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -42,16 +42,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
                                   "resource://gre/modules/Deprecated.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
+                                  "resource://gre/modules/BookmarkJSONUtils.jsm");
+
 // The minimum amount of transactions before starting a batch. Usually we do
 // do incremental updates, a batch will cause views to completely
 // refresh instead.
 const MIN_TRANSACTIONS_FOR_BATCH = 5;
 
 #ifdef XP_MACOSX
 // On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
 // really just want "\n".
@@ -1714,28 +1717,31 @@ this.PlacesUtils = {
   },
 
   /**
    * Serializes bookmarks using JSON, and writes to the supplied file.
    *
    * @see backups.saveBookmarksToJSONFile(aFile)
    */
   backupBookmarksToFile: function PU_backupBookmarksToFile(aFile) {
-    this.backups.saveBookmarksToJSONFile(aFile);
+    Deprecated.warning(
+      "backupBookmarksToFile is deprecated and will be removed in a future version",
+      "https://bugzilla.mozilla.org/show_bug.cgi?id=852041");
+    return this.backups.saveBookmarksToJSONFile(aFile);
   },
 
   /**
    * Creates a dated backup in <profile>/bookmarkbackups.
    * Stores the bookmarks using JSON.
    *
    * @see backups.create(aMaxBackups, aForceBackup)
    */
   archiveBookmarksFile:
   function PU_archiveBookmarksFile(aMaxBackups, aForceBackup) {
-    this.backups.create(aMaxBackups, aForceBackup);
+    return this.backups.create(aMaxBackups, aForceBackup);
   },
 
   /**
    * Helper to create and manage backups.
    */
   backups: {
 
     get _filenamesRegex() {
@@ -1849,154 +1855,119 @@ this.PlacesUtils = {
      * saveBookmarksToJSONFile()
      *
      * Serializes bookmarks using JSON, and writes to the supplied file.
      * Note: any item that should not be backed up must be annotated with
      *       "places/excludeFromBackup".
      *
      * @param aFile
      *        nsIFile where to save JSON backup.
+     *
+     * @return {Promise}
      */
     saveBookmarksToJSONFile:
     function PU_B_saveBookmarksToFile(aFile) {
-      if (!aFile.exists())
-        aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
-      if (!aFile.exists() || !aFile.isWritable()) {
-        Cu.reportError("Unable to create bookmarks backup file: " + aFile.leafName);
-        return;
-      }
-
-      this._writeBackupFile(aFile);
-
-      if (aFile.parent.equals(this.folder)) {
-        // Update internal cache.
-        this.entries.push(aFile);
-      }
-      else {
-        // If we are saving to a folder different than our backups folder, then
-        // we also want to copy this new backup to it.
-        // This way we ensure the latest valid backup is the same saved by the
-        // user.  See bug 424389.
-        var latestBackup = this.getMostRecent("json");
-        if (!latestBackup || latestBackup != aFile) {
-          let name = this.getFilenameForDate();
-          let file = this.folder.clone();
-          file.append(name);
-          if (file.exists())
-            file.remove(false);
-          else {
-            // Update internal cache if we are not replacing an existing
-            // backup file.
-            this.entries.push(file);
-          }
-          aFile.copyTo(this.folder, name);
+      return Task.spawn(function() {
+        if (!aFile.exists())
+          aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+        if (!aFile.exists() || !aFile.isWritable()) {
+          throw new Error("Unable to create bookmarks backup file: " + aFile.leafName);
+        }
+
+        yield BookmarkJSONUtils.exportToFile(aFile);
+
+        if (aFile.parent.equals(this.folder)) {
+          // Update internal cache.
+          this.entries.push(aFile);
         }
-      }
-    },
-
-    _writeBackupFile:
-    function PU_B__writeBackupFile(aFile) {
-      // Init stream.
-      let stream = Cc["@mozilla.org/network/file-output-stream;1"].
-                   createInstance(Ci.nsIFileOutputStream);
-      stream.init(aFile, 0x02 | 0x08 | 0x20, 0600, 0);
-
-      // UTF-8 converter stream.
-      let converter = Cc["@mozilla.org/intl/converter-output-stream;1"].
-                   createInstance(Ci.nsIConverterOutputStream);
-      converter.init(stream, "UTF-8", 0, 0x0000);
-
-      // Weep over stream interface variance.
-      let streamProxy = {
-        converter: converter,
-        write: function(aData, aLen) {
-          this.converter.writeString(aData);
+        else {
+          // If we are saving to a folder different than our backups folder, then
+          // we also want to copy this new backup to it.
+          // This way we ensure the latest valid backup is the same saved by the
+          // user.  See bug 424389.
+          var latestBackup = this.getMostRecent("json");
+          if (!latestBackup || latestBackup != aFile) {
+            let name = this.getFilenameForDate();
+            let file = this.folder.clone();
+            file.append(name);
+            if (file.exists())
+              file.remove(false);
+            else {
+              // Update internal cache if we are not replacing an existing
+              // backup file.
+              this.entries.push(file);
+            }
+            aFile.copyTo(this.folder, name);
+          }
         }
-      };
-
-      // Get list of itemIds that must be excluded from the backup.
-      let excludeItems =
-        PlacesUtils.annotations.getItemsWithAnnotation(PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO);
-
-      // Query the Places root.
-      let options = PlacesUtils.history.getNewQueryOptions();
-      options.expandQueries = false;
-      let query = PlacesUtils.history.getNewQuery();
-      query.setFolders([PlacesUtils.placesRootId], 1);
-      let root = PlacesUtils.history.executeQuery(query, options).root;
-      root.containerOpen = true;
-      // Serialize to JSON and write to stream.
-      PlacesUtils.serializeNodeAsJSONToOutputStream(root, streamProxy,
-                                                    false, false, excludeItems);
-      root.containerOpen = false;
-
-      // Close converter and stream.
-      converter.close();
-      stream.close();
+      }.bind(this));
     },
 
     /**
      * create()
      *
      * Creates a dated backup in <profile>/bookmarkbackups.
      * Stores the bookmarks using JSON.
      * Note: any item that should not be backed up must be annotated with
      *       "places/excludeFromBackup".
      *
      * @param [optional] int aMaxBackups
      *                       The maximum number of backups to keep.
      *
      * @param [optional] bool aForceBackup
      *                        Forces creating a backup even if one was already
      *                        created that day (overwrites).
+     *
+     * @return {Promise}
      */
     create:
     function PU_B_create(aMaxBackups, aForceBackup) {
-      // Construct the new leafname.
-      let newBackupFilename = this.getFilenameForDate();
-      let mostRecentBackupFile = this.getMostRecent();
-
-      if (!aForceBackup) {
-        let numberOfBackupsToDelete = 0;
-        if (aMaxBackups !== undefined && aMaxBackups > -1)
-          numberOfBackupsToDelete = this.entries.length - aMaxBackups;
-
-        if (numberOfBackupsToDelete > 0) {
-          // If we don't have today's backup, remove one more so that
-          // the total backups after this operation does not exceed the
-          // number specified in the pref.
-          if (!mostRecentBackupFile ||
-              mostRecentBackupFile.leafName != newBackupFilename)
-            numberOfBackupsToDelete++;
-
-          while (numberOfBackupsToDelete--) {
-            let oldestBackup = this.entries.pop();
-            oldestBackup.remove(false);
+      return Task.spawn(function() {
+        // Construct the new leafname.
+        let newBackupFilename = this.getFilenameForDate();
+        let mostRecentBackupFile = this.getMostRecent();
+
+        if (!aForceBackup) {
+          let numberOfBackupsToDelete = 0;
+          if (aMaxBackups !== undefined && aMaxBackups > -1)
+            numberOfBackupsToDelete = this.entries.length - aMaxBackups;
+
+          if (numberOfBackupsToDelete > 0) {
+            // If we don't have today's backup, remove one more so that
+            // the total backups after this operation does not exceed the
+            // number specified in the pref.
+            if (!mostRecentBackupFile ||
+                mostRecentBackupFile.leafName != newBackupFilename)
+              numberOfBackupsToDelete++;
+
+            while (numberOfBackupsToDelete--) {
+              let oldestBackup = this.entries.pop();
+              oldestBackup.remove(false);
+            }
           }
+
+          // Do nothing if we already have this backup or we don't want backups.
+          if (aMaxBackups === 0 ||
+              (mostRecentBackupFile &&
+               mostRecentBackupFile.leafName == newBackupFilename))
+            return;
         }
 
-        // Do nothing if we already have this backup or we don't want backups.
-        if (aMaxBackups === 0 ||
-            (mostRecentBackupFile &&
-             mostRecentBackupFile.leafName == newBackupFilename))
+        let newBackupFile = this.folder.clone();
+        newBackupFile.append(newBackupFilename);
+
+        if (aForceBackup && newBackupFile.exists())
+          newBackupFile.remove(false);
+
+        if (newBackupFile.exists())
           return;
-      }
-
-      let newBackupFile = this.folder.clone();
-      newBackupFile.append(newBackupFilename);
-
-      if (aForceBackup && newBackupFile.exists())
-        newBackupFile.remove(false);
-
-      if (newBackupFile.exists())
-        return;
-
-      this.saveBookmarksToJSONFile(newBackupFile);
+
+        yield this.saveBookmarksToJSONFile(newBackupFile);
+      }.bind(this));
     }
-
   },
 
   /**
    * Given a uri returns list of itemIds associated to it.
    *
    * @param aURI
    *        nsIURI or spec of the page.
    * @param aCallback
--- a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
+++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js
@@ -181,16 +181,18 @@ var test = {
       do_check_eq(child.title, "bookmark" + i)
     }
     aNode.containerOpen = false;
   }
 }
 tests.push(test);
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
@@ -200,30 +202,34 @@ function run_test() {
   // populate db
   tests.forEach(function(aTest) {
     aTest.populate();
     // sanity
     aTest.validate();
   });
 
   // export json to file
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't export to file: " + ex); }
+  Task.spawn(function() {
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) { do_throw("couldn't export to file: " + ex); }
 
-  // clean
-  tests.forEach(function(aTest) {
-    aTest.clean();
-  });
+    // clean
+    tests.forEach(function(aTest) {
+      aTest.clean();
+    });
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
 
-  // validate
-  tests.forEach(function(aTest) {
-    aTest.validate();
+    // validate
+    tests.forEach(function(aTest) {
+      aTest.validate();
+    });
+
+    // clean up
+    jsonFile.remove(false);
+
+    do_test_finished();
   });
-
-  // clean up
-  jsonFile.remove(false);
 }
--- a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
@@ -102,57 +102,62 @@ var test = {
     do_check_eq(restoreRootChildNode.uri, this._restoreRootURI.spec);
     restoreRootNode.containerOpen = false;
 
     rootNode.containerOpen = false;
   }
 }
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
   if (!jsonFile.exists())
     do_throw("couldn't create file: bookmarks.exported.json");
 
   // populate db
   test.populate();
 
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) {
-    do_throw("couldn't export to file: " + ex);
-  }
+  Task.spawn(function() {
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) {
+      do_throw("couldn't export to file: " + ex);
+    }
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) {
-    do_throw("couldn't import the exported file: " + ex);
-  }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) {
+      do_throw("couldn't import the exported file: " + ex);
+    }
 
-  // validate without removing all bookmarks
-  // restore do not remove backup exclude entries
-  test.validate(false);
+    // validate without removing all bookmarks
+    // restore do not remove backup exclude entries
+    test.validate(false);
 
-  // cleanup
-  remove_all_bookmarks();
-  // manually remove the excluded root
-  PlacesUtils.bookmarks.removeItem(test._excludeRootId);
+    // cleanup
+    remove_all_bookmarks();
+    // manually remove the excluded root
+    PlacesUtils.bookmarks.removeItem(test._excludeRootId);
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) {
+      do_throw("couldn't import the exported file: " + ex);
+    }
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) {
-    do_throw("couldn't import the exported file: " + ex);
-  }
+    // validate after a complete bookmarks cleanup
+    test.validate(true);
 
-  // validate after a complete bookmarks cleanup
-  test.validate(true);
+    // clean up
+    jsonFile.remove(false);
 
-  // clean up
-  jsonFile.remove(false);
+    do_test_finished();
+  });
 }
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -116,16 +116,18 @@ tests.push({
       }
     }
     do_check_eq(foundTestFolder, 1);
     rootNode.containerOpen = false;
   }
 });
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
@@ -140,29 +142,33 @@ function run_test() {
     aTest.populate();
     // sanity
     aTest.validate();
 
     if (aTest.excludedItemsFromRestore)
       excludedItemsFromRestore = excludedItems.concat(aTest.excludedItemsFromRestore);
   });
 
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't export to file: " + ex); }
+  Task.spawn(function() {
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) { do_throw("couldn't export to file: " + ex); }
 
-  tests.forEach(function(aTest) {
-    aTest.inbetween();
-  });
+    tests.forEach(function(aTest) {
+      aTest.inbetween();
+    });
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile, excludedItemsFromRestore);
-  } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile, excludedItemsFromRestore);
+    } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
 
-  // validate
-  tests.forEach(function(aTest) {
-    aTest.validate();
+    // validate
+    tests.forEach(function(aTest) {
+      aTest.validate();
+    });
+
+    // clean up
+    jsonFile.remove(false);
+
+    do_test_finished();
   });
-
-  // clean up
-  jsonFile.remove(false);
 }
--- a/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
+++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
@@ -50,16 +50,18 @@ var quotesTest = {
 
     // clean up
     toolbar.containerOpen = false;
   }
 }
 tests.push(quotesTest);
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
@@ -68,31 +70,35 @@ function run_test() {
 
   // populate db
   tests.forEach(function(aTest) {
     aTest.populate();
     // sanity
     aTest.validate();
   });
 
-  // export json to file
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't export to file: " + ex); }
+  Task.spawn(function() {
+    // export json to file
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) { do_throw("couldn't export to file: " + ex); }
 
-  // clean
-  tests.forEach(function(aTest) {
-    aTest.clean();
-  });
+    // clean
+    tests.forEach(function(aTest) {
+      aTest.clean();
+    });
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
 
-  // validate
-  tests.forEach(function(aTest) {
-    aTest.validate();
+    // validate
+    tests.forEach(function(aTest) {
+      aTest.validate();
+    });
+
+    // clean up
+    jsonFile.remove(false);
+
+    do_test_finished();
   });
-
-  // clean up
-  jsonFile.remove(false);
 }
--- a/toolkit/components/places/tests/bookmarks/test_448584.js
+++ b/toolkit/components/places/tests/bookmarks/test_448584.js
@@ -62,16 +62,18 @@ var invalidURITest = {
 
     // clean up
     toolbar.containerOpen = false;
   }
 }
 tests.push(invalidURITest);
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
@@ -90,31 +92,35 @@ function run_test() {
     stmt.bindByIndex(0, aTest._itemId);
     try {
       stmt.execute();
     } finally {
       stmt.finalize();
     }
   });
 
-  // export json to file
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't export to file: " + ex); }
+  Task.spawn(function() {
+    // export json to file
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) { do_throw("couldn't export to file: " + ex); }
 
-  // clean
-  tests.forEach(function(aTest) {
-    aTest.clean();
-  });
+    // clean
+    tests.forEach(function(aTest) {
+      aTest.clean();
+    });
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
 
-  // validate
-  tests.forEach(function(aTest) {
-    aTest.validate(1);
+    // validate
+    tests.forEach(function(aTest) {
+      aTest.validate(1);
+    });
+
+    // clean up
+    jsonFile.remove(false);
+
+    do_test_finished();
   });
-
-  // clean up
-  jsonFile.remove(false);
 }
--- a/toolkit/components/places/tests/bookmarks/test_458683.js
+++ b/toolkit/components/places/tests/bookmarks/test_458683.js
@@ -93,16 +93,18 @@ var invalidTagChildTest = {
     var tags = PlacesUtils.tagging.getTagsForURI(PlacesUtils._uri(this._itemUrl));
     do_check_eq(tags.length, 1);
     do_check_eq(tags[0], this._tag);
   }
 }
 tests.push(invalidTagChildTest);
 
 function run_test() {
+  do_test_pending();
+
   do_check_eq(typeof PlacesUtils, "object");
 
   // make json file
   var jsonFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   jsonFile.append("bookmarks.json");
   if (jsonFile.exists())
     jsonFile.remove(false);
   jsonFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
@@ -111,31 +113,35 @@ function run_test() {
 
   // populate db
   tests.forEach(function(aTest) {
     aTest.populate();
     // sanity
     aTest.validate();
   });
 
-  // export json to file
-  try {
-    PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't export to file: " + ex); }
+  Task.spawn(function() {
+    // export json to file
+    try {
+      yield BookmarkJSONUtils.exportToFile(jsonFile);
+    } catch(ex) { do_throw("couldn't export to file: " + ex); }
 
-  // clean
-  tests.forEach(function(aTest) {
-    aTest.clean();
-  });
+    // clean
+    tests.forEach(function(aTest) {
+      aTest.clean();
+    });
 
-  // restore json file
-  try {
-    PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
-  } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
+    // restore json file
+    try {
+      PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
+    } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
 
-  // validate
-  tests.forEach(function(aTest) {
-    aTest.validate();
+    // validate
+    tests.forEach(function(aTest) {
+      aTest.validate();
+    });
+
+    // clean up
+    jsonFile.remove(false);
+
+    do_test_finished();
   });
-
-  // clean up
-  jsonFile.remove(false);
 }
--- a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
+++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js
@@ -2,16 +2,18 @@
 /* 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 NUMBER_OF_BACKUPS = 1;
 
 function run_test() {
+  do_test_pending();
+
   // Get bookmarkBackups directory
   var bookmarksBackupDir = PlacesUtils.backups.folder;
 
   // Create an html dummy backup in the past
   var htmlBackupFile = bookmarksBackupDir.clone();
   htmlBackupFile.append("bookmarks-2008-01-01.html");
   if (htmlBackupFile.exists())
     htmlBackupFile.remove(false);
@@ -30,20 +32,25 @@ function run_test() {
 
   // Export bookmarks to JSON.
   var backupFilename = PlacesUtils.backups.getFilenameForDate();
   var lastBackupFile = bookmarksBackupDir.clone();
   lastBackupFile.append(backupFilename);
   if (lastBackupFile.exists())
     lastBackupFile.remove(false);
   do_check_false(lastBackupFile.exists());
-  PlacesUtils.backups.create(NUMBER_OF_BACKUPS);
-  do_check_true(lastBackupFile.exists());
+
+  Task.spawn(function() {
+    yield PlacesUtils.backups.create(NUMBER_OF_BACKUPS);
+    do_check_true(lastBackupFile.exists());
 
-  // Check that last backup has been retained
-  do_check_false(htmlBackupFile.exists());
-  do_check_false(jsonBackupFile.exists());
-  do_check_true(lastBackupFile.exists());
+    // Check that last backup has been retained
+    do_check_false(htmlBackupFile.exists());
+    do_check_false(jsonBackupFile.exists());
+    do_check_true(lastBackupFile.exists());
 
-  // cleanup
-  lastBackupFile.remove(false);
-  do_check_false(lastBackupFile.exists());
+    // cleanup
+    lastBackupFile.remove(false);
+    do_check_false(lastBackupFile.exists());
+
+    do_test_finished();
+  });
 }
--- a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
+++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 function run_test() {
+  do_test_pending();
+
   let bookmarksBackupDir = PlacesUtils.backups.folder;
   // Remove all files from backups folder.
   let files = bookmarksBackupDir.directoryEntries;
   while (files.hasMoreElements()) {
     let entry = files.getNext().QueryInterface(Ci.nsIFile);
     entry.remove(false);
   }
 
@@ -23,24 +25,27 @@ function run_test() {
   if (futureBackupFile.exists())
     futureBackupFile.remove(false);
   do_check_false(futureBackupFile.exists());
   futureBackupFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, 0600);
   do_check_true(futureBackupFile.exists());
 
   do_check_eq(PlacesUtils.backups.entries.length, 0);
 
-  PlacesUtils.backups.create();
+  Task.spawn(function() {
+    yield PlacesUtils.backups.create();
+    // Check that a backup for today has been created.
+    do_check_eq(PlacesUtils.backups.entries.length, 1);
+    let mostRecentBackupFile = PlacesUtils.backups.getMostRecent();
+    do_check_neq(mostRecentBackupFile, null);
+    let todayName = PlacesUtils.backups.getFilenameForDate();
+    do_check_eq(mostRecentBackupFile.leafName, todayName);
 
-  // Check that a backup for today has been created.
-  do_check_eq(PlacesUtils.backups.entries.length, 1);
-  let mostRecentBackupFile = PlacesUtils.backups.getMostRecent();
-  do_check_neq(mostRecentBackupFile, null);
-  let todayName = PlacesUtils.backups.getFilenameForDate();
-  do_check_eq(mostRecentBackupFile.leafName, todayName);
+    // Check that future backup has been removed.
+    do_check_false(futureBackupFile.exists());
 
-  // Check that future backup has been removed.
-  do_check_false(futureBackupFile.exists());
+    // Cleanup.
+    mostRecentBackupFile.remove(false);
+    do_check_false(mostRecentBackupFile.exists());
 
-  // Cleanup.
-  mostRecentBackupFile.remove(false);
-  do_check_false(mostRecentBackupFile.exists());
+    do_test_finished()
+  });
 }
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -31,16 +31,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/commonjs/sdk/core/promise.js");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 
 // This imports various other objects in addition to PlacesUtils.
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
+Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
   return NetUtil.newURI(
          "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
          "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
 });
 
 function LOG(aMsg) {
--- a/toolkit/components/places/tests/unit/test_384370.js
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -57,17 +57,17 @@ function run_test() {
     // 2. run the test-suite
     Task.spawn(function() {
       yield validate();
       yield promiseAsyncUpdates();
       
       // Test exporting a Places canonical json file.
       // 1. export to bookmarks.exported.json
       try {
-        PlacesUtils.backups.saveBookmarksToJSONFile(jsonFile);
+        yield BookmarkJSONUtils.exportToFile(jsonFile);
       } catch(ex) { do_throw("couldn't export to file: " + ex); }
       LOG("exported json");
 
       // 2. empty bookmarks db
       // 3. import bookmarks.exported.json
       try {
         PlacesUtils.restoreBookmarksFromJSONFile(jsonFile);
       } catch(ex) { do_throw("couldn't import the exported file: " + ex); }
--- a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
@@ -45,24 +45,26 @@ var tests = [
     desc:       "JSON restore: normal restore should succeed",
     currTopic:  NSIOBSERVER_TOPIC_BEGIN,
     finalTopic: NSIOBSERVER_TOPIC_SUCCESS,
     data:       NSIOBSERVER_DATA_JSON,
     folderId:   null,
     run:        function () {
       this.file = createFile("bookmarks-test_restoreNotification.json");
       addBookmarks();
-      PlacesUtils.backups.saveBookmarksToJSONFile(this.file);
-      remove_all_bookmarks();
-      try {
-        PlacesUtils.restoreBookmarksFromJSONFile(this.file);
-      }
-      catch (e) {
-        do_throw("  Restore should not have failed");
-      }
+      Task.spawn(function() {
+        yield BookmarkJSONUtils.exportToFile(this.file);
+        remove_all_bookmarks();
+        try {
+          PlacesUtils.restoreBookmarksFromJSONFile(this.file);
+        }
+        catch (e) {
+          do_throw("  Restore should not have failed");
+        }
+      }.bind(this));
     }
   },
 
   {
     desc:       "JSON restore: empty file should succeed",
     currTopic:  NSIOBSERVER_TOPIC_BEGIN,
     finalTopic: NSIOBSERVER_TOPIC_SUCCESS,
     data:       NSIOBSERVER_DATA_JSON,
--- a/toolkit/components/places/tests/unit/test_utils_backups_create.js
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -10,16 +10,18 @@
 
 const PREFIX = "bookmarks-";
 // The localized prefix must be "bigger" and associated to older backups.
 const LOCALIZED_PREFIX = "segnalibri-";
 const SUFFIX = ".json";
 const NUMBER_OF_BACKUPS = 10;
 
 function run_test() {
+  do_test_pending();
+
   // Generate random dates.
   var dateObj = new Date();
   var dates = [];
   while (dates.length < NUMBER_OF_BACKUPS) {
     // Use last year to ensure today's backup is the newest.
     let randomDate = new Date(dateObj.getFullYear() - 1,
                               Math.floor(12 * Math.random()),
                               Math.floor(28 * Math.random()));
@@ -50,36 +52,40 @@ function run_test() {
   }
  
   // Replace PlacesUtils getFormattedString so that it will return the localized
   // string we want.
   PlacesUtils.getFormattedString = function (aKey, aValue) {
     return LOCALIZED_PREFIX + aValue;
   }
 
-  PlacesUtils.backups.create(Math.floor(dates.length/2));
-  // Add today's backup.
-  dates.push(dateObj.toLocaleFormat("%Y-%m-%d"));
+  Task.spawn(function() {
+    yield PlacesUtils.backups.create(Math.floor(dates.length/2));
+    // Add today's backup.
+    dates.push(dateObj.toLocaleFormat("%Y-%m-%d"));
 
-  // Check backups.
-  for (var i = 0; i < dates.length; i++) {
-    let backupFilename;
-    let shouldExist;
-    if (i > Math.floor(dates.length/2)) {
-      backupFilename = PREFIX + dates[i] + SUFFIX;
-      shouldExist = true;
+    // Check backups.
+    for (var i = 0; i < dates.length; i++) {
+      let backupFilename;
+      let shouldExist;
+      if (i > Math.floor(dates.length/2)) {
+        backupFilename = PREFIX + dates[i] + SUFFIX;
+        shouldExist = true;
+      }
+      else {
+        backupFilename = LOCALIZED_PREFIX + dates[i] + SUFFIX;
+        shouldExist = false;
+      }
+      var backupFile = bookmarksBackupDir.clone();
+      backupFile.append(backupFilename);
+      if (backupFile.exists() != shouldExist)
+        do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
     }
-    else {
-      backupFilename = LOCALIZED_PREFIX + dates[i] + SUFFIX;
-      shouldExist = false;
-    }
-    var backupFile = bookmarksBackupDir.clone();
-    backupFile.append(backupFilename);
-    if (backupFile.exists() != shouldExist)
-      do_throw("Backup should " + (shouldExist ? "" : "not") + " exist: " + backupFilename);
-  }
 
-  // Cleanup backups folder.
-  bookmarksBackupDir.remove(true);
-  do_check_false(bookmarksBackupDir.exists());
-  // Recreate the folder.
-  PlacesUtils.backups.folder;
+    // Cleanup backups folder.
+    bookmarksBackupDir.remove(true);
+    do_check_false(bookmarksBackupDir.exists());
+    // Recreate the folder.
+    PlacesUtils.backups.folder;
+
+    do_test_finished();
+  });
 }