Bug 1302901 - Create a Places mobile bookmarks root. r?mak draft
authorKit Cambridge <kit@yakshaving.ninja>
Thu, 29 Sep 2016 13:44:08 -0700
changeset 422559 6cff6aa8ec50b5adb4be16dc16d72934c806ffe7
parent 419886 344920af45b92da6d4f5b84738e1c7a3fb582461
child 422560 11fa774388d91b07d9b18989ba328a89c8666e5f
push id31740
push userbmo:kcambridge@mozilla.com
push dateFri, 07 Oct 2016 20:40:57 +0000
reviewersmak
bugs1302901
milestone52.0a1
Bug 1302901 - Create a Places mobile bookmarks root. r?mak MozReview-Commit-ID: IESvIHCM2fK
toolkit/components/places/BookmarkJSONUtils.jsm
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/nsINavBookmarksService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavBookmarks.h
toolkit/components/places/nsNavHistoryQuery.cpp
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_bookmarks_eraseEverything.js
toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
toolkit/components/places/tests/bookmarks/test_protectRoots.js
toolkit/components/places/tests/expiration/test_annos_expire_session.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/places_v35.sqlite
toolkit/components/places/tests/migration/test_current_from_v24.js
toolkit/components/places/tests/migration/test_current_from_v34.js
toolkit/components/places/tests/migration/xpcshell.ini
toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
toolkit/components/places/tests/unit/test_telemetry.js
toolkit/components/places/tests/unit/xpcshell.ini
toolkit/locales/en-US/chrome/places/places.properties
--- a/toolkit/components/places/BookmarkJSONUtils.jsm
+++ b/toolkit/components/places/BookmarkJSONUtils.jsm
@@ -317,43 +317,51 @@ BookmarkImporter.prototype = {
                   container = PlacesUtils.tagsFolderId;
                   break;
                 case "unfiledBookmarksFolder":
                   container = PlacesUtils.unfiledBookmarksFolderId;
                   break;
                 case "toolbarFolder":
                   container = PlacesUtils.toolbarFolderId;
                   break;
+                case "mobileFolder":
+                  container = PlacesUtils.mobileFolderId;
+                  break;
               }
 
               // Insert the data into the db
               for (let child of node.children) {
                 let index = child.index;
                 let [folders, searches] =
                   this.importJSONNode(child, container, index, 0);
                 for (let i = 0; i < folders.length; i++) {
                   if (folders[i])
                     folderIdMap[i] = folders[i];
                 }
                 searchIds = searchIds.concat(searches);
               }
             } else {
-              this.importJSONNode(
+              let [folders, searches] = this.importJSONNode(
                 node, PlacesUtils.placesRootId, node.index, 0);
+              for (let i = 0; i < folders.length; i++) {
+                if (folders[i])
+                  folderIdMap[i] = folders[i];
+              }
+              searchIds = searchIds.concat(searches);
             }
           }
 
           // Fixup imported place: uris that contain folders
-          searchIds.forEach(function(aId) {
-            let oldURI = PlacesUtils.bookmarks.getBookmarkURI(aId);
+          for (let id of searchIds) {
+            let oldURI = PlacesUtils.bookmarks.getBookmarkURI(id);
             let uri = fixupQuery(oldURI, folderIdMap);
             if (!uri.equals(oldURI)) {
-              PlacesUtils.bookmarks.changeBookmarkURI(aId, uri, this._source);
+              PlacesUtils.bookmarks.changeBookmarkURI(id, uri, this._source);
             }
-          });
+          }
 
           deferred.resolve();
         }.bind(this)
       };
 
       PlacesUtils.bookmarks.runInBatchMode(batch, null);
     }
     yield deferred.promise;
@@ -434,18 +442,30 @@ BookmarkImporter.prototype = {
                                                        this._source);
               if (aData.annos && aData.annos.length)
                 PlacesUtils.setAnnotationsForItem(id, aData.annos,
                                                   this._source);
             });
             this._importPromises.push(lmPromise);
           }
         } else {
-          id = PlacesUtils.bookmarks.createFolder(
-                 aContainer, aData.title, aIndex, aData.guid, this._source);
+          let isMobileFolder = aData.annos &&
+                               aData.annos.some(anno => anno.name == PlacesUtils.MOBILE_ROOT_ANNO);
+          if (isMobileFolder) {
+            // Mobile bookmark folders are special: we move their children to
+            // the mobile root instead of importing them. We also rewrite
+            // queries to use the special folder ID, and ignore generic
+            // properties like timestamps and annotations set on the folder.
+            id = PlacesUtils.mobileFolderId;
+          } else {
+            // For other folders, set `id` so that we can import timestamps
+            // and annotations at the end of this function.
+            id = PlacesUtils.bookmarks.createFolder(
+                   aContainer, aData.title, aIndex, aData.guid, this._source);
+          }
           folderIdMap[aData.id] = id;
           // Process children
           if (aData.children) {
             for (let i = 0; i < aData.children.length; i++) {
               let child = aData.children[i];
               let [folders, searches] =
                 this.importJSONNode(child, id, i, aContainer);
               for (let j = 0; j < folders.length; j++) {
@@ -520,18 +540,20 @@ BookmarkImporter.prototype = {
         break;
       case PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR:
         id = PlacesUtils.bookmarks.insertSeparator(aContainer, aIndex, aData.guid, this._source);
         break;
       default:
         // Unknown node type
     }
 
-    // Set generic properties, valid for all nodes
-    if (id != -1 && aContainer != PlacesUtils.tagsFolderId &&
+    // Set generic properties, valid for all nodes except tags and the mobile
+    // root.
+    if (id != -1 && id != PlacesUtils.mobileFolderId &&
+        aContainer != PlacesUtils.tagsFolderId &&
         aGrandParentId != PlacesUtils.tagsFolderId) {
       if (aData.dateAdded)
         PlacesUtils.bookmarks.setItemDateAdded(id, aData.dateAdded,
                                                this._source);
       if (aData.lastModified)
         PlacesUtils.bookmarks.setItemLastModified(id, aData.lastModified,
                                                   this._source);
       if (aData.annos && aData.annos.length)
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -113,16 +113,17 @@ var Bookmarks = Object.freeze({
    * Special GUIDs associated with bookmark roots.
    * It's guaranteed that the roots will always have these guids.
    */
 
    rootGuid:    "root________",
    menuGuid:    "menu________",
    toolbarGuid: "toolbar_____",
    unfiledGuid: "unfiled_____",
+   mobileGuid:  "mobile______",
 
    // With bug 424160, tags will stop being bookmarks, thus this root will
    // be removed.  Do not rely on this, rather use the tagging service API.
    tagsGuid:    "tags________",
 
   /**
    * Inserts a bookmark-item into the bookmarks tree.
    *
@@ -405,17 +406,17 @@ var Bookmarks = Object.freeze({
     let info = guidOrInfo;
     if (!info)
       throw new Error("Input should be a valid object");
     if (typeof(guidOrInfo) != "object")
       info = { guid: guidOrInfo };
 
     // Disallow removing the root folders.
     if ([this.rootGuid, this.menuGuid, this.toolbarGuid, this.unfiledGuid,
-         this.tagsGuid].includes(info.guid)) {
+         this.tagsGuid, this.mobileGuid].includes(info.guid)) {
       throw new Error("It's not possible to remove Places root folders.");
     }
 
     // Even if we ignore any other unneeded property, we still validate any
     // known property to reduce likelihood of hidden bugs.
     let removeInfo = validateBookmarkObject(info);
 
     return Task.spawn(function* () {
@@ -460,17 +461,18 @@ var Bookmarks = Object.freeze({
    *           Defaults to nsINavBookmarksService::SOURCE_DEFAULT.
    *
    * @return {Promise} resolved when the removal is complete.
    * @resolves once the removal is complete.
    */
   eraseEverything: function(options={}) {
     return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
       db => db.executeTransaction(function* () {
-        const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
+        const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid,
+                             this.mobileGuid];
         yield removeFoldersContents(db, folderGuids, options);
         const time = PlacesUtils.toPRTime(new Date());
         for (let folderGuid of folderGuids) {
           yield db.executeCached(
             `UPDATE moz_bookmarks SET lastModified = :time
              WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
             `, { folderGuid, time });
         }
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/DebugOnly.h"
 
 #include "Database.h"
 
+#include "nsIAnnotationService.h"
 #include "nsINavBookmarksService.h"
 #include "nsIInterfaceRequestorUtils.h"
 #include "nsIFile.h"
 #include "nsIWritablePropertyBag2.h"
 
 #include "nsNavHistory.h"
 #include "nsPlacesTables.h"
 #include "nsPlacesIndexes.h"
@@ -84,16 +85,25 @@
 
 // Places string bundle, contains internationalized bookmark root names.
 #define PLACES_BUNDLE "chrome://places/locale/places.properties"
 
 // Livemarks annotations.
 #define LMANNO_FEEDURI "livemark/feedURI"
 #define LMANNO_SITEURI "livemark/siteURI"
 
+#define MOBILE_ROOT_GUID "mobile______"
+#define MOBILE_ROOT_ANNO "mobile/bookmarksRoot"
+#define MOBILE_QUERY_ANNO "MobileBookmarks"
+
+// We use a fixed title for the mobile root to avoid marking the database as
+// corrupt if we can't look up the localized title in the string bundle. Sync
+// sets the title to the localized version when it creates the left pane query.
+#define MOBILE_ROOT_TITLE "mobile"
+
 using namespace mozilla;
 
 namespace mozilla {
 namespace places {
 
 namespace {
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -856,16 +866,23 @@ Database::InitSchema(bool* aDatabaseMigr
 
       if (currentSchemaVersion < 34) {
         rv = MigrateV34Up();
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
       // Firefox 51 uses schema version 34.
 
+      if (currentSchemaVersion < 35) {
+        rv = MigrateV35Up();
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // Firefox 52 uses schema version 35.
+
       // Schema Upgrades must add migration code here.
 
       rv = UpdateBookmarkRootTitles();
       // We don't want a broken localization to cause us to think
       // the database is corrupt and needs to be replaced.
       MOZ_ASSERT(NS_SUCCEEDED(rv));
     }
   }
@@ -1010,30 +1027,33 @@ Database::CreateBookmarkRoots()
 
   rv = bundle->GetStringFromName(u"OtherBookmarksFolderTitle",
                                  getter_Copies(rootTitle));
   if (NS_FAILED(rv)) return rv;
   rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("unfiled"),
                   NS_LITERAL_CSTRING("unfiled_____"), rootTitle);
   if (NS_FAILED(rv)) return rv;
 
+  int64_t mobileRootId = CreateMobileRoot();
+  if (mobileRootId <= 0) return NS_ERROR_FAILURE;
+
 #if DEBUG
   nsCOMPtr<mozIStorageStatement> stmt;
   rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT count(*), sum(position) FROM moz_bookmarks"
   ), getter_AddRefs(stmt));
   if (NS_FAILED(rv)) return rv;
 
   bool hasResult;
   rv = stmt->ExecuteStep(&hasResult);
   if (NS_FAILED(rv)) return rv;
   MOZ_ASSERT(hasResult);
   int32_t bookmarkCount = stmt->AsInt32(0);
   int32_t positionSum = stmt->AsInt32(1);
-  MOZ_ASSERT(bookmarkCount == 5 && positionSum == 6);
+  MOZ_ASSERT(bookmarkCount == 6 && positionSum == 10);
 #endif
 
   return NS_OK;
 }
 
 nsresult
 Database::InitFunctions()
 {
@@ -1122,21 +1142,23 @@ Database::UpdateBookmarkRootTitles()
   nsCOMPtr<mozIStorageBindingParamsArray> paramsArray;
   rv = stmt->NewBindingParamsArray(getter_AddRefs(paramsArray));
   if (NS_FAILED(rv)) return rv;
 
   const char *rootGuids[] = { "menu________"
                             , "toolbar_____"
                             , "tags________"
                             , "unfiled_____"
+                            , "mobile______"
                             };
   const char *titleStringIDs[] = { "BookmarksMenuFolderTitle"
                                  , "BookmarksToolbarFolderTitle"
                                  , "TagsFolderTitle"
                                  , "OtherBookmarksFolderTitle"
+                                 , "MobileBookmarksFolderTitle"
                                  };
 
   for (uint32_t i = 0; i < ArrayLength(rootGuids); ++i) {
     nsXPIDLString title;
     rv = bundle->GetStringFromName(NS_ConvertASCIItoUTF16(titleStringIDs[i]).get(),
                                    getter_Copies(title));
     if (NS_FAILED(rv)) return rv;
 
@@ -1830,16 +1852,261 @@ Database::MigrateV34Up() {
       "WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) "
     ")"
   ));
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+nsresult
+Database::MigrateV35Up() {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  int64_t mobileRootId = CreateMobileRoot();
+  if (mobileRootId <= 0) return NS_ERROR_FAILURE;
+
+  // At this point, we should have no more than two folders with the mobile
+  // bookmarks anno: the new root, and the old folder if one exists. If, for
+  // some reason, we have multiple folders with the anno, we append their
+  // children to the new root.
+  nsTArray<int64_t> folderIds;
+  nsresult rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO),
+                                 nsINavBookmarksService::TYPE_FOLDER,
+                                 folderIds);
+  if (NS_FAILED(rv)) return rv;
+
+  for (uint32_t i = 0; i < folderIds.Length(); ++i) {
+    if (folderIds[i] == mobileRootId) {
+      // Ignore the new mobile root. We'll remove this anno from the root in
+      // bug 1306445.
+      continue;
+    }
+
+    // Append the folder's children to the new root.
+    nsCOMPtr<mozIStorageStatement> moveStmt;
+    rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+      "UPDATE moz_bookmarks "
+      "SET parent = :root_id, "
+          "position = position + IFNULL("
+            "(SELECT MAX(position) + 1 FROM moz_bookmarks "
+             "WHERE parent = :root_id), 0)"
+      "WHERE parent = :folder_id"
+    ), getter_AddRefs(moveStmt));
+    if (NS_FAILED(rv)) return rv;
+    mozStorageStatementScoper moveScoper(moveStmt);
+
+    rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"),
+                                   mobileRootId);
+    if (NS_FAILED(rv)) return rv;
+    rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("folder_id"),
+                                   folderIds[i]);
+    if (NS_FAILED(rv)) return rv;
+
+    rv = moveStmt->Execute();
+    if (NS_FAILED(rv)) return rv;
+
+    // Delete the old folder.
+    rv = DeleteBookmarkItem(folderIds[i]);
+    if (NS_FAILED(rv)) return rv;
+  }
+
+  // Delete any left pane queries that point to the old folder. We'll
+  // automatically create one for the new mobile root during the next sync.
+  // Other queries like `place:folder={folderId}` might become invalid; we
+  // ignore them because they're unlikely to exist, and rewriting query URLs
+  // is tedious.
+  nsTArray<int64_t> queryIds;
+  rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_QUERY_ANNO),
+                        nsINavBookmarksService::TYPE_BOOKMARK,
+                        queryIds);
+  if (NS_FAILED(rv)) return rv;
+
+  for (uint32_t i = 0; i < queryIds.Length(); ++i) {
+    rv = DeleteBookmarkItem(queryIds[i]);
+    if (NS_FAILED(rv)) return rv;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+                           nsTArray<int64_t>& aItemIds)
+{
+  nsCOMPtr<mozIStorageStatement> stmt;
+  nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT b.id FROM moz_items_annos a "
+    "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
+    "JOIN moz_bookmarks b ON b.id = a.item_id "
+    "WHERE n.name = :anno_name AND "
+          "b.type = :item_type"
+  ), getter_AddRefs(stmt));
+  if (NS_FAILED(rv)) return rv;
+  mozStorageStatementScoper scoper(stmt);
+
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aAnnoName);
+  if (NS_FAILED(rv)) return rv;
+  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_type"), aItemType);
+  if (NS_FAILED(rv)) return rv;
+
+  bool hasMore = false;
+  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
+    int64_t itemId;
+    rv = stmt->GetInt64(0, &itemId);
+    if (NS_FAILED(rv)) return rv;
+    aItemIds.AppendElement(itemId);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+Database::DeleteBookmarkItem(int32_t aItemId)
+{
+  // Delete the old bookmark.
+  nsCOMPtr<mozIStorageStatement> deleteStmt;
+  nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_bookmarks WHERE id = :item_id"
+  ), getter_AddRefs(deleteStmt));
+  if (NS_FAILED(rv)) return rv;
+  mozStorageStatementScoper deleteScoper(deleteStmt);
+
+  rv = deleteStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+                                   aItemId);
+  if (NS_FAILED(rv)) return rv;
+
+  rv = deleteStmt->Execute();
+  if (NS_FAILED(rv)) return rv;
+
+  // Clean up orphan annotations.
+  nsCOMPtr<mozIStorageStatement> removeAnnosStmt;
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "DELETE FROM moz_items_annos WHERE item_id = :item_id"
+  ), getter_AddRefs(removeAnnosStmt));
+  if (NS_FAILED(rv)) return rv;
+  mozStorageStatementScoper removeAnnosScoper(removeAnnosStmt);
+
+  rv = removeAnnosStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"),
+                                        aItemId);
+  if (NS_FAILED(rv)) return rv;
+
+  rv = removeAnnosStmt->Execute();
+  if (NS_FAILED(rv)) return rv;
+
+  return NS_OK;
+}
+
+int64_t
+Database::CreateMobileRoot()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  // Create the mobile root, ignoring conflicts if one already exists (for
+  // example, if the user downgraded to an earlier release channel).
+  nsCOMPtr<mozIStorageStatement> createStmt;
+  nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "INSERT OR IGNORE INTO moz_bookmarks "
+      "(type, title, dateAdded, lastModified, guid, position, parent) "
+    "SELECT :item_type, :item_title, :timestamp, :timestamp, :guid, "
+      "(SELECT COUNT(*) FROM moz_bookmarks p WHERE p.parent = b.id), b.id "
+    "FROM moz_bookmarks b WHERE b.parent = 0"
+  ), getter_AddRefs(createStmt));
+  if (NS_FAILED(rv)) return -1;
+  mozStorageStatementScoper createScoper(createStmt);
+
+  rv = createStmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"),
+                                   nsINavBookmarksService::TYPE_FOLDER);
+  if (NS_FAILED(rv)) return -1;
+  rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
+                                        NS_LITERAL_CSTRING(MOBILE_ROOT_TITLE));
+  if (NS_FAILED(rv)) return -1;
+  rv = createStmt->BindInt64ByName(NS_LITERAL_CSTRING("timestamp"),
+                                   RoundedPRNow());
+  if (NS_FAILED(rv)) return -1;
+  rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+                                        NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+  if (NS_FAILED(rv)) return -1;
+
+  rv = createStmt->Execute();
+  if (NS_FAILED(rv)) return -1;
+
+  // Find the mobile root ID. We can't use the last inserted ID because the
+  // root might already exist, and we ignore on conflict.
+  nsCOMPtr<mozIStorageStatement> findIdStmt;
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT id FROM moz_bookmarks WHERE guid = :guid"
+  ), getter_AddRefs(findIdStmt));
+  if (NS_FAILED(rv)) return -1;
+  mozStorageStatementScoper findIdScoper(findIdStmt);
+
+  rv = findIdStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"),
+                                        NS_LITERAL_CSTRING(MOBILE_ROOT_GUID));
+  if (NS_FAILED(rv)) return -1;
+
+  bool hasResult = false;
+  rv = findIdStmt->ExecuteStep(&hasResult);
+  if (NS_FAILED(rv) || !hasResult) return -1;
+
+  int64_t rootId;
+  rv = findIdStmt->GetInt64(0, &rootId);
+  if (NS_FAILED(rv)) return -1;
+
+  // Set the mobile bookmarks anno on the new root, so that Sync code on an
+  // older channel can still find it in case of a downgrade. This can be
+  // removed in bug 1306445.
+  nsCOMPtr<mozIStorageStatement> addAnnoNameStmt;
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)"
+  ), getter_AddRefs(addAnnoNameStmt));
+  if (NS_FAILED(rv)) return -1;
+  mozStorageStatementScoper addAnnoNameScoper(addAnnoNameStmt);
+
+  rv = addAnnoNameStmt->BindUTF8StringByName(
+    NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+  if (NS_FAILED(rv)) return -1;
+  rv = addAnnoNameStmt->Execute();
+  if (NS_FAILED(rv)) return -1;
+
+  nsCOMPtr<mozIStorageStatement> addAnnoStmt;
+  rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
+    "INSERT OR IGNORE INTO moz_items_annos "
+      "(id, item_id, anno_attribute_id, content, flags, "
+       "expiration, type, dateAdded, lastModified) "
+    "SELECT "
+      "(SELECT a.id FROM moz_items_annos a "
+       "WHERE a.anno_attribute_id = n.id AND "
+             "a.item_id = :root_id), "
+      ":root_id, n.id, 1, 0, :expiration, :type, :timestamp, :timestamp "
+    "FROM moz_anno_attributes n WHERE name = :anno_name"
+  ), getter_AddRefs(addAnnoStmt));
+  if (NS_FAILED(rv)) return -1;
+  mozStorageStatementScoper addAnnoScoper(addAnnoStmt);
+
+  rv = addAnnoStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"), rootId);
+  if (NS_FAILED(rv)) return -1;
+  rv = addAnnoStmt->BindUTF8StringByName(
+    NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO));
+  if (NS_FAILED(rv)) return -1;
+  rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expiration"),
+                                    nsIAnnotationService::EXPIRE_NEVER);
+  if (NS_FAILED(rv)) return -1;
+  rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("type"),
+                                    nsIAnnotationService::TYPE_INT32);
+  if (NS_FAILED(rv)) return -1;
+  rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("timestamp"),
+                                    RoundedPRNow());
+  if (NS_FAILED(rv)) return -1;
+
+  rv = addAnnoStmt->Execute();
+  if (NS_FAILED(rv)) return -1;
+
+  return rootId;
+}
+
 void
 Database::Shutdown()
 {
   // As the last step in the shutdown path, finalize the database handle.
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mClosed);
 
   // Break cycles with the shutdown blockers.
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -13,17 +13,17 @@
 #include "mozilla/storage.h"
 #include "mozilla/storage/StatementCache.h"
 #include "mozilla/Attributes.h"
 #include "nsIEventTarget.h"
 #include "Shutdown.h"
 
 // This is the schema version. Update it at any schema change and add a
 // corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 34
+#define DATABASE_SCHEMA_VERSION 35
 
 // Fired after Places inited.
 #define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
 // Fired when initialization fails due to a locked database.
 #define TOPIC_DATABASE_LOCKED "places-database-locked"
 // This topic is received when the profile is about to be lost.  Places does
 // initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
 // Any shutdown work that requires the Places APIs should happen here.
@@ -265,21 +265,27 @@ protected:
   nsresult MigrateV26Up();
   nsresult MigrateV27Up();
   nsresult MigrateV28Up();
   nsresult MigrateV30Up();
   nsresult MigrateV31Up();
   nsresult MigrateV32Up();
   nsresult MigrateV33Up();
   nsresult MigrateV34Up();
+  nsresult MigrateV35Up();
 
   nsresult UpdateBookmarkRootTitles();
 
   friend class ConnectionShutdownBlocker;
 
+  int64_t CreateMobileRoot();
+  nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
+                            nsTArray<int64_t>& aItemIds);
+  nsresult DeleteBookmarkItem(int32_t aItemId);
+
 private:
   ~Database();
 
   /**
    * Singleton getter, invoked by class instantiation.
    */
   static already_AddRefed<Database> GetSingleton();
 
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -303,16 +303,17 @@ this.PlacesUtils = {
   TYPE_X_MOZ_PLACE_ACTION: "text/x-moz-place-action",
 
   EXCLUDE_FROM_BACKUP_ANNO: "places/excludeFromBackup",
   LMANNO_FEEDURI: "livemark/feedURI",
   LMANNO_SITEURI: "livemark/siteURI",
   POST_DATA_ANNO: "bookmarkProperties/POSTData",
   READ_ONLY_ANNO: "placesInternal/READ_ONLY",
   CHARSET_ANNO: "URIProperties/characterSet",
+  MOBILE_ROOT_ANNO: "mobile/bookmarksRoot",
 
   TOPIC_SHUTDOWN: "places-shutdown",
   TOPIC_INIT_COMPLETE: "places-init-complete",
   TOPIC_DATABASE_LOCKED: "places-database-locked",
   TOPIC_EXPIRATION_FINISHED: "places-expiration-finished",
   TOPIC_FEEDBACK_UPDATED: "places-autocomplete-feedback-updated",
   TOPIC_FAVICONS_EXPIRED: "places-favicons-expired",
   TOPIC_VACUUM_STARTING: "places-vacuum-starting",
@@ -1105,29 +1106,35 @@ this.PlacesUtils = {
     return this.tagsFolderId = this.bookmarks.tagsFolder;
   },
 
   get unfiledBookmarksFolderId() {
     delete this.unfiledBookmarksFolderId;
     return this.unfiledBookmarksFolderId = this.bookmarks.unfiledBookmarksFolder;
   },
 
+  get mobileFolderId() {
+    delete this.mobileFolderId;
+    return this.mobileFolderId = this.bookmarks.mobileFolder;
+  },
+
   /**
    * Checks if aItemId is a root.
    *
    *   @param aItemId
    *          item id to look for.
    *   @returns true if aItemId is a root, false otherwise.
    */
   isRootItem: function PU_isRootItem(aItemId) {
     return aItemId == PlacesUtils.bookmarksMenuFolderId ||
            aItemId == PlacesUtils.toolbarFolderId ||
            aItemId == PlacesUtils.unfiledBookmarksFolderId ||
            aItemId == PlacesUtils.tagsFolderId ||
-           aItemId == PlacesUtils.placesRootId;
+           aItemId == PlacesUtils.placesRootId ||
+           aItemId == PlacesUtils.mobileFolderId;
   },
 
   /**
    * Set the POST data associated with a bookmark, if any.
    * Used by POST keywords.
    *   @param aBookmarkId
    *
    * @deprecated Use PlacesUtils.keywords.insert() API instead.
@@ -1810,16 +1817,18 @@ this.PlacesUtils = {
           if (itemId == PlacesUtils.placesRootId)
             item.root = "placesRoot";
           else if (itemId == PlacesUtils.bookmarksMenuFolderId)
             item.root = "bookmarksMenuFolder";
           else if (itemId == PlacesUtils.unfiledBookmarksFolderId)
             item.root = "unfiledBookmarksFolder";
           else if (itemId == PlacesUtils.toolbarFolderId)
             item.root = "toolbarFolder";
+          else if (itemId == PlacesUtils.mobileFolderId)
+            item.root = "mobileFolder";
           break;
         case Ci.nsINavBookmarksService.TYPE_SEPARATOR:
           item.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
           break;
         default:
           Cu.reportError("Unexpected bookmark type");
           break;
       }
--- a/toolkit/components/places/nsINavBookmarksService.idl
+++ b/toolkit/components/places/nsINavBookmarksService.idl
@@ -276,16 +276,21 @@ interface nsINavBookmarksService : nsISu
   readonly attribute long long unfiledBookmarksFolder;
 
   /**
    * The item ID of the personal toolbar folder.
    */
   readonly attribute long long toolbarFolder;
 
   /**
+   * The item ID of the mobile bookmarks folder.
+   */
+  readonly attribute long long mobileFolder;
+
+  /**
    * This value should be used for APIs that allow passing in an index
    * where an index is not known, or not required to be specified.
    * e.g.: When appending an item to a folder.
    */
   const short DEFAULT_INDEX = -1;
 
   const unsigned short TYPE_BOOKMARK = 1;
   const unsigned short TYPE_FOLDER = 2;
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -115,16 +115,17 @@ private:
 
 nsNavBookmarks::nsNavBookmarks()
   : mItemCount(0)
   , mRoot(0)
   , mMenuRoot(0)
   , mTagsRoot(0)
   , mUnfiledRoot(0)
   , mToolbarRoot(0)
+  , mMobileRoot(0)
   , mCanNotify(false)
   , mCacheObservers("bookmark-observers")
   , mBatching(false)
 {
   NS_ASSERTION(!gBookmarksService,
                "Attempting to create two instances of the service!");
   gBookmarksService = this;
 }
@@ -195,17 +196,17 @@ nsNavBookmarks::Init()
 
 nsresult
 nsNavBookmarks::ReadRoots()
 {
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT guid, id FROM moz_bookmarks WHERE guid IN ( "
       "'root________', 'menu________', 'toolbar_____', "
-      "'tags________', 'unfiled_____' )"
+      "'tags________', 'unfiled_____', 'mobile______' )"
   ), getter_AddRefs(stmt));
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool hasResult;
   while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
     nsAutoCString guid;
     rv = stmt->GetUTF8String(0, guid);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -223,19 +224,23 @@ nsNavBookmarks::ReadRoots()
       mToolbarRoot = id;
     }
     else if (guid.EqualsLiteral("tags________")) {
       mTagsRoot = id;
     }
     else if (guid.EqualsLiteral("unfiled_____")) {
       mUnfiledRoot = id;
     }
+    else if (guid.EqualsLiteral("mobile______")) {
+      mMobileRoot = id;
+    }
   }
 
-  if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot)
+  if (!mRoot || !mMenuRoot || !mToolbarRoot || !mTagsRoot || !mUnfiledRoot ||
+      !mMobileRoot)
     return NS_ERROR_FAILURE;
 
   return NS_OK;
 }
 
 // nsNavBookmarks::IsBookmarkedInDatabase
 //
 //    This checks to see if the specified place_id is actually bookmarked.
@@ -326,16 +331,24 @@ nsNavBookmarks::GetTagsFolder(int64_t* a
 NS_IMETHODIMP
 nsNavBookmarks::GetUnfiledBookmarksFolder(int64_t* aRoot)
 {
   *aRoot = mUnfiledRoot;
   return NS_OK;
 }
 
 
+NS_IMETHODIMP
+nsNavBookmarks::GetMobileFolder(int64_t* aRoot)
+{
+  *aRoot = mMobileRoot;
+  return NS_OK;
+}
+
+
 nsresult
 nsNavBookmarks::InsertBookmarkInDB(int64_t aPlaceId,
                                    enum ItemType aItemType,
                                    int64_t aParentId,
                                    int32_t aIndex,
                                    const nsACString& aTitle,
                                    PRTime aDateAdded,
                                    PRTime aLastModified,
--- a/toolkit/components/places/nsNavBookmarks.h
+++ b/toolkit/components/places/nsNavBookmarks.h
@@ -275,21 +275,22 @@ private:
 
   nsMaybeWeakPtrArray<nsINavBookmarkObserver> mObservers;
 
   int64_t mRoot;
   int64_t mMenuRoot;
   int64_t mTagsRoot;
   int64_t mUnfiledRoot;
   int64_t mToolbarRoot;
+  int64_t mMobileRoot;
 
   inline bool IsRoot(int64_t aFolderId) {
     return aFolderId == mRoot || aFolderId == mMenuRoot ||
            aFolderId == mTagsRoot || aFolderId == mUnfiledRoot ||
-           aFolderId == mToolbarRoot;
+           aFolderId == mToolbarRoot || aFolderId == mMobileRoot;
   }
 
   nsresult IsBookmarkedInDatabase(int64_t aBookmarkID, bool* aIsBookmarked);
 
   nsresult SetItemDateInternal(enum mozilla::places::BookmarkDate aDateType,
                                int64_t aItemId,
                                PRTime aValue);
 
--- a/toolkit/components/places/nsNavHistoryQuery.cpp
+++ b/toolkit/components/places/nsNavHistoryQuery.cpp
@@ -169,17 +169,18 @@ inline void AppendInt64(nsACString& str,
 }
 
 namespace PlacesFolderConversion {
   #define PLACES_ROOT_FOLDER "PLACES_ROOT"
   #define BOOKMARKS_MENU_FOLDER "BOOKMARKS_MENU"
   #define TAGS_FOLDER "TAGS"
   #define UNFILED_BOOKMARKS_FOLDER "UNFILED_BOOKMARKS"
   #define TOOLBAR_FOLDER "TOOLBAR"
-  
+  #define MOBILE_BOOKMARKS_FOLDER "MOBILE_BOOKMARKS"
+
   /**
    * Converts a folder name to a folder id.
    *
    * @param aName
    *        The name of the folder to convert to a folder id.
    * @returns the folder id if aName is a recognizable name, -1 otherwise.
    */
   inline int64_t DecodeFolder(const nsCString &aName)
@@ -193,16 +194,18 @@ namespace PlacesFolderConversion {
     else if (aName.EqualsLiteral(BOOKMARKS_MENU_FOLDER))
       (void)bs->GetBookmarksMenuFolder(&folderID);
     else if (aName.EqualsLiteral(TAGS_FOLDER))
       (void)bs->GetTagsFolder(&folderID);
     else if (aName.EqualsLiteral(UNFILED_BOOKMARKS_FOLDER))
       (void)bs->GetUnfiledBookmarksFolder(&folderID);
     else if (aName.EqualsLiteral(TOOLBAR_FOLDER))
       (void)bs->GetToolbarFolder(&folderID);
+    else if (aName.EqualsLiteral(MOBILE_BOOKMARKS_FOLDER))
+      (void)bs->GetMobileFolder(&folderID);
 
     return folderID;
   }
 
   /**
    * Converts a folder id to a named constant, or a string representation of the
    * folder id if there is no named constant for the folder, and appends it to
    * aQuery.
@@ -234,16 +237,20 @@ namespace PlacesFolderConversion {
     else if (NS_SUCCEEDED(bs->GetUnfiledBookmarksFolder(&folderID)) &&
              aFolderID == folderID) {
       aQuery.AppendLiteral(UNFILED_BOOKMARKS_FOLDER);
     }
     else if (NS_SUCCEEDED(bs->GetToolbarFolder(&folderID)) &&
              aFolderID == folderID) {
       aQuery.AppendLiteral(TOOLBAR_FOLDER);
     }
+    else if (NS_SUCCEEDED(bs->GetMobileFolder(&folderID)) &&
+             aFolderID == folderID) {
+      aQuery.AppendLiteral(MOBILE_BOOKMARKS_FOLDER);
+    }
     else {
       // It wasn't one of our named constants, so just convert it to a string.
       aQuery.AppendInt(aFolderID);
     }
 
     return NS_OK;
   }
 } // namespace PlacesFolderConversion
--- a/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-exclude-from-backup.js
@@ -1,17 +1,17 @@
 /* -*- 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 EXCLUDE_FROM_BACKUP_ANNO = "places/excludeFromBackup";
-// Menu, Toolbar, Unsorted, Tags
-const PLACES_ROOTS_COUNT  = 4;
+// Menu, Toolbar, Unsorted, Tags, Mobile
+const PLACES_ROOTS_COUNT  = 5;
 var tests = [];
 
 /*
 
 Backup/restore tests example:
 
 var myTest = {
   populate: function () { ... add bookmarks ... },
--- a/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
+++ b/toolkit/components/places/tests/bookmarks/test_417228-other-roots.js
@@ -20,49 +20,50 @@ this.push(myTest);
 */
 
 tests.push({
   excludeItemsFromRestore: [],
   populate: function populate() {
     // check initial size
     var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
                                                  false, false).root;
-    do_check_eq(rootNode.childCount, 4);
+    do_check_eq(rootNode.childCount, 5);
 
     // create a test root
     this._folderTitle = "test folder";
     this._folderId =
       PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
                                          this._folderTitle,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX);
-    do_check_eq(rootNode.childCount, 5);
+    do_check_eq(rootNode.childCount, 6);
 
     // add a tag
     this._testURI = PlacesUtils._uri("http://test");
     this._tags = ["a", "b"];
     PlacesUtils.tagging.tagURI(this._testURI, this._tags);
 
     // add a child to each root, including our test root
     this._roots = [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.toolbarFolderId,
-                   PlacesUtils.unfiledBookmarksFolderId, this._folderId];
+                   PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.mobileFolderId,
+                   this._folderId];
     this._roots.forEach(function(aRootId) {
       // clean slate
       PlacesUtils.bookmarks.removeFolderChildren(aRootId);
       // add a test bookmark
       PlacesUtils.bookmarks.insertBookmark(aRootId, this._testURI,
                                            PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
     }, this);
 
     // add a folder to exclude from replacing during restore
     // this will still be present post-restore
     var excludedFolderId =
       PlacesUtils.bookmarks.createFolder(PlacesUtils.placesRootId,
                                          "excluded",
                                          PlacesUtils.bookmarks.DEFAULT_INDEX);
-    do_check_eq(rootNode.childCount, 6);
+    do_check_eq(rootNode.childCount, 7);
     this.excludeItemsFromRestore.push(excludedFolderId);
 
     // add a test bookmark to it
     PlacesUtils.bookmarks.insertBookmark(excludedFolderId, this._testURI,
                                          PlacesUtils.bookmarks.DEFAULT_INDEX, "test");
   },
 
   inbetween: function inbetween() {
@@ -85,17 +86,17 @@ tests.push({
 
     var rootNode = PlacesUtils.getFolderContents(PlacesUtils.placesRootId,
                                                  false, false).root;
 
     // validate litter is gone
     do_check_neq(rootNode.getChild(0).title, this._litterTitle);
 
     // test root count is the same
-    do_check_eq(rootNode.childCount, 6);
+    do_check_eq(rootNode.childCount, 7);
 
     var foundTestFolder = 0;
     for (var i = 0; i < rootNode.childCount; i++) {
       var node = rootNode.getChild(i);
 
       do_print("validating " + node.title);
       if (node.itemId != PlacesUtils.tagsFolderId) {
         if (node.title == this._folderTitle) {
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
@@ -59,20 +59,24 @@ add_task(function* test_eraseEverything(
 
   yield PlacesUtils.bookmarks.eraseEverything();
 
   Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample);
   Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla);
 
   // Check there are no orphan annotations.
   let conn = yield PlacesUtils.promiseDBConnection();
-  let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
-  Assert.equal(rows.length, 0);
-  rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
-  Assert.equal(rows.length, 0);
+  let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+  // Bug 1306445 will eventually remove the mobile root anno.
+  Assert.equal(annoAttrs.length, 1);
+  Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+  let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+  Assert.equal(annos.length, 1);
+  Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+  Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
 });
 
 add_task(function* test_eraseEverything_roots() {
   yield PlacesUtils.bookmarks.eraseEverything();
 
   // Ensure the roots have not been removed.
   Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid));
   Assert.ok(yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid));
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -43,17 +43,18 @@ add_task(function* remove_nonexistent_gu
   }
 });
 
 add_task(function* remove_roots_fail() {
   let guids = [PlacesUtils.bookmarks.rootGuid,
                PlacesUtils.bookmarks.unfiledGuid,
                PlacesUtils.bookmarks.menuGuid,
                PlacesUtils.bookmarks.toolbarGuid,
-               PlacesUtils.bookmarks.tagsGuid];
+               PlacesUtils.bookmarks.tagsGuid,
+               PlacesUtils.bookmarks.mobileGuid];
   for (let guid of guids) {
     Assert.throws(() => PlacesUtils.bookmarks.remove(guid),
                   /It's not possible to remove Places root folders/);
   }
 });
 
 add_task(function* remove_normal_folder_under_root_succeeds() {
   let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.rootGuid,
@@ -93,20 +94,24 @@ add_task(function* remove_bookmark_orpha
   PlacesUtils.annotations.setItemAnnotation((yield PlacesUtils.promiseItemId(bm1.guid)),
                                             "testanno", "testvalue", 0, 0);
 
   let bm2 = yield PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
 
   // Check there are no orphan annotations.
   let conn = yield PlacesUtils.promiseDBConnection();
-  let rows = yield conn.execute(`SELECT * FROM moz_items_annos`);
-  Assert.equal(rows.length, 0);
-  rows = yield conn.execute(`SELECT * FROM moz_anno_attributes`);
-  Assert.equal(rows.length, 0);
+  let annoAttrs = yield conn.execute(`SELECT id, name FROM moz_anno_attributes`);
+  // Bug 1306445 will eventually remove the mobile root anno.
+  Assert.equal(annoAttrs.length, 1);
+  Assert.equal(annoAttrs[0].getResultByName("name"), PlacesUtils.MOBILE_ROOT_ANNO);
+  let annos = rows = yield conn.execute(`SELECT item_id, anno_attribute_id FROM moz_items_annos`);
+  Assert.equal(annos.length, 1);
+  Assert.equal(annos[0].getResultByName("item_id"), PlacesUtils.mobileFolderId);
+  Assert.equal(annos[0].getResultByName("anno_attribute_id"), annoAttrs[0].getResultByName("id"));
 });
 
 add_task(function* remove_bookmark_empty_title() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  url: "http://example.com/",
                                                  title: "" });
   checkBookmarkObject(bm1);
--- a/toolkit/components/places/tests/bookmarks/test_protectRoots.js
+++ b/toolkit/components/places/tests/bookmarks/test_protectRoots.js
@@ -3,17 +3,18 @@
 
 function run_test()
 {
   const ROOTS = [
     PlacesUtils.bookmarksMenuFolderId,
     PlacesUtils.toolbarFolderId,
     PlacesUtils.unfiledBookmarksFolderId,
     PlacesUtils.tagsFolderId,
-    PlacesUtils.placesRootId
+    PlacesUtils.placesRootId,
+    PlacesUtils.mobileFolderId,
   ];
 
   for (let root of ROOTS) {
     do_check_true(PlacesUtils.isRootItem(root));
 
     try {
       PlacesUtils.bookmarks.removeItem(root);
       do_throw("Trying to remove a root should throw");
--- a/toolkit/components/places/tests/expiration/test_annos_expire_session.js
+++ b/toolkit/components/places/tests/expiration/test_annos_expire_session.js
@@ -53,18 +53,20 @@ add_task(function* test_annos_expire_ses
   items = as.getItemsWithAnnotation("test2");
   do_check_eq(items.length, 10);
 
   let deferred = Promise.defer();
   waitForConnectionClosed(function() {
     let stmt = DBConn(true).createAsyncStatement(
       `SELECT id FROM moz_annos
        UNION ALL
-       SELECT id FROM moz_items_annos`
+       SELECT id FROM moz_items_annos
+       WHERE expiration = :expiration`
     );
+    stmt.params.expiration = as.EXPIRE_SESSION;
     stmt.executeAsync({
       handleResult: function(aResultSet) {
         dump_table("moz_annos");
         dump_table("moz_items_annos");
         do_throw("Should not find any leftover session annotations");
       },
       handleError: function(aError) {
         do_throw("Error code " + aError.result + " with message '" +
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
  * 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 CURRENT_SCHEMA_VERSION = 34;
+const CURRENT_SCHEMA_VERSION = 35;
 const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
 
 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
 
 // Shortcuts to transitions type.
 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
new file mode 100644
index 0000000000000000000000000000000000000000..5e157d778091339a19f42bfa917be8635140b30f
GIT binary patch
literal 1146880
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
z0000000000000000000000000000000RCC^q{{|{&e}aQvb?M;94;AFR=gye?aUln
zQ`6nuJ*qvmD%sW+AC*cku1R!^TsXg`C7DjNq*l~)Cp(%`-I<Z2Yiqg_ZB426M9q<1
z+4hD^sw>@;IDBKVk&WqGp+x$?d{%p+IoZ{|-K?fmM>f%s&F5vZ>8_5Wd&|Uz$4=<&
zCpNlvRBUb{-I8d^#<Ho!iR_X@dQ|K;XI>ne)jP(;>C~urTU&Bfs(n;`=%tOJ;<^!I
z@7-rZ^US(=N7t`f-m<u^WBB0Rp5N=LX{7^0$At!!m6e6cLc_{SI@{t+i9_>$ZT|4_
z2WC>~?BS!w9@rjVF)xwnYRhI0&vkynxP_UiQ(NZDp1X8pDD>Lcg~vQR;_#Azp~>4c
zQ>Z_2tLk&=TbeS*&Y3%A=BhFI>f?WT!{<*Z9vE5}8kwseF3MGZkBN^rrn<5<TVG{y
zXl>h+MN`Mr&YiZhcJj*MgFpADsSPJb2ZkE{?uPSopH4P*WfR-99u2KM_M{0N6Hi(>
zf8n^+<Ax7z`RTgTn<4{4?SFUcee0#EE0ayNCs%FTNhGwkD?52)vg!DEOXt)%!v`Ny
zUVQ$kMFT@k|8OTu5^bH^v|kijo1E0$)ZQ?6LPMry`S8J!V{iS&Y2ksPWTE}?f8-)^
zYcw5i+op@q+Ho@`j$Ar_<x%P7t4_-G@a3N#TAABi;oy&<+z$W%000000000000000
z000000000000000000000DM5~94-x&g-goI%0k6;BgWpl&xXo;@|o}RumAu600000
z000000000000000000000000000000;C)t+Kb-kK4*~!H00000000000000000000
z0000000000000000N!VnxlcHFK9u_b00000000000000000000000000000000000
z004jwf?dNUq5X@>LhY$l$+oums8o7!=u;KBjBxOBDE9*Z00000000000000000000
z0000000000000000018x10(yFx2IMm+uGuzQt8E&xuij7+z|>M4!#(i7Syi!)0%Is
z3D!(r6J34p>PuFiy856q-pXYI000000000000000000000000000000000000095|
z4jnWg6b_Y#M}|TrCBurMvr>)8wnS`7D%IK^Pq$`5#fRTf{N%tDePMeczoFoef%&@O
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
z0000000000000000000000000000000002~=?#gD3%92llWmEb##E}cJ)UmO%uA)R
zk)e_L^1hV0=~QPTolPb(HO+}kQ##q1O{O~XRp++Fn{ui1bEUFVHkK}`%r|`I`+Q*l
z00000000000000000000000000000000000008hltIU0(!2zM*;$TT|c<uxM00000
z000000000000000000000000000000004HND!NlB91aaC3ztWugCe7&#ZL}CE&r*G
z6f%aEg~KH!l|}OsO^J@|g81T0G!iPV8&S9Z-#7GyhZGfx?q4W6plDVqlRZ9}NoEtx
z8_Ry~lvrQ5G&~>_E)Vte7LH7AZjLpkQmyUrbZaJ-O~tZHl9^abs;xPZ&POweL@p`O
z9&1i!I@{ta6V0(yM?NJsWmByMsZ?8IJiW26?jhIrg_XJM8@T$UQ1EbYUC<pI8yv9a
z?KKarxnfP@n%Xs?)%UKxB$o*Q000000000000000000000000000000000000Pz3h
z(1FpKSyk<+RSip$nQSV(a(Vvmy=S&bXy|N<Hzk_m*+jCVIk94NEcb6rWwhp~UH^`{
zE$LKyU$rp<{?1DM`x>=V^q|A`$d}plC+4?^G$gZ$jXjKw)y3*+E20M-`EKPq<LN|4
zwy(yR*yuycqX*4@uO8asnQVKiIoXm-H23RmOj-1x`rY59?&7Xwb3<20a#>fRuiEH?
zOQS;%E6hV-W;2`58d~DZlTE3PLeYt_x?v^Jq2sn+a!D$a-B@hwfyL3G6SiNhFr!VW
zu8wT3Ty-=$^za>YoK7?)I+|8)Y;w$~NOb7*?HA2YMWNYEw<*{3h@$Awqqbju>nWHR
z8(kgV;Xu05Z4FD}nI(NiD{?EfEcjU{cq{mG@JjH9;CI23!7p+N000000000000000
z000000000000000000000002+zg%%794;-(AB&6g$7nQvj70LsqM}HoygV8Sg-i2?
zl8RjUvY<N@ycxU}JQ>^@d?UCj_*5<d00000000000000000000000000000000000
z0002~zqeatWO#8pH7efLmRyx;AJyI6J*sSbiN)JXjBYP6vc1HjT_YpQw`nsnC_JjP
za4f0F4W=x3EEN1Tcs+O}cp-Q$csh6@mjD0&000000000000000000000000000000
z000000RIZ2k@9d^G*T8W%^ynghvNJpnm<JHhoVTNth^#uv@G~>D0nOQbMR8|`{3!|
zSHZ)%1ONa400000000000000000000000000000000000_+P6eQWOrYE$$7Xy+Ncm
zC@PK=mFMyz;c#PFG*T8W%^ylCa<$5W2SdSMgV%#sf)|44f~SKgatQzc0000000000
z00000000000000000000000000Pw%rfJjL=R9rV=?7jPJD2qly;nMt}B!4K*AENm~
zB!4K1L_*~ixhmn{l~C>n000000000000000000000000000000000000000!Kq{l*
zlJc@paovcq_wKWyB9{^l{u;{t000000000000000000000000000000000000002M
z2T~+bR$h?{!@+Bz+z$W%000000000000000000000000000000000000DPeA6e%eW
z71xazd+$CQDsu5b!52coo58EWAA)CsCxS<U2ZA33-wVDS+!EXrTo<g%Wdi^J00000
z000000000000000000000000000000!26>l8ZHl)Ru+Pi0fnG=r$P{|C<Kx6LQqsz
z94*fkDT#){;mE*3P&BAFDD4f3dxL0i5a|tyigQ7EQAMt+LBXY=;H}_K!HdCj!BfF6
zf`@|pg1dw71m6g54z3T@<+1?)000000000000000000000000000000000000N{O5
z8V!fTrIDR_!=j3kXt+FFTAmBa!lh+}Sfr#7iwr1~DJm*0q!tY<q!v|{M9ahB=%7Ln
zDJ}#>(c)-%u43+(D-n&Bm6ccI`Wh5ehk{pv-v&Pqei(c!SRY&&To{}kWP?O-d@wzj
zn9BwL000000000000000000000000000000000000DxWtqv4WpX{fkvMBV!5AE+!u
zOA678r!5&!h!z*3D;|4tr$RK^8@>Frib6Ef8(s89c_CU<h;~1_p{z1mUJ~w&T{EMk
zG8)Rq4!@)L$$={dZH(peHoUNC;1;o>0l8RtUu<*M>h?P$n`0|(TUWX{wtW1M;!Uy9
z*W9x%T9Lc7vf!Ff@TcJU;K|_O;GW=);A^=A000000000000000000000000000000
z000000002^XEPvD5)Or48@uqBhewq5MaKW~hR>H&M2f@12Y>EQQyYqVPg{Pv?(}Hy
z=^^FC=SO-^cYA)XtIEnF#pS~XN51^iLwge=$KLvlqKaJeWx>i&@KW&W;K#vtg0BRZ
z2j}M!000000000000000000000000000000000000002s{|`GyhK0MblSd|-j*qu=
zPMs6UC$vpjG<8hv+-WOoC$B8dCnP6zH?=p+ozReJSzeS+Xr5U&@96qf%Uc%Lb(G~3
zP8hc^Gj(dqoY`}ijx5b5%&Bi_${agq?wFaY#+2j}jy-8Y$HbFX&R;mL^|<Ju$guKp
zGbWB)I)CL+>E)|Vs>lsxP*4;KUJRZH?hn2l+!$OItPNHKOM(-Eql0n50l}`hYybcN
z00000000000000000000000000000000000{^ji)85ZuIdwga}Dw&+yy5yJv`Gl^H
zmSkI^xgoDOpODS8EU2%oZEZbnMQddtA(d)tjHe4FB87zb;!MLv(R@N?($vLs=cksY
zYuhH342ldZPp4AZz7qKY?Wx9GC%y4Og@i;$S6_jme8NfD1!EU1nYgrV#gg{Ye8TZl
znzNG|XQ!tnX0_~;PnbNp>%_%%Gp5(io0k}vPngscPn>YVvZL$gO_)}mPna+xxw7q~
zk*TE%>Lyj>6Y4ta7R_GTI(}7ib7NUWehSWfpKk#G0000000000000000000000000
z0000000000006wtD)NUj-{(O900000000000000000000000000000000000006-I
ztRnY`2EPdfZw9Xfzt5cj00000000000000000000000000000000000006+hq|#_O
Y93B*j7Q)J+NFf|hR8$-dmzPKWFS)u{#sB~S
--- a/toolkit/components/places/tests/migration/test_current_from_v24.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v24.js
@@ -18,17 +18,18 @@ add_task(function* test_bookmark_guid_an
   yield PlacesUtils.bookmarks.eraseEverything();
 
   let db = yield PlacesUtils.promiseDBConnection();
   let m = new Map([
     [PlacesUtils.placesRootId, PlacesUtils.bookmarks.rootGuid],
     [PlacesUtils.bookmarksMenuFolderId, PlacesUtils.bookmarks.menuGuid],
     [PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.toolbarGuid],
     [PlacesUtils.unfiledBookmarksFolderId, PlacesUtils.bookmarks.unfiledGuid],
-    [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid]
+    [PlacesUtils.tagsFolderId, PlacesUtils.bookmarks.tagsGuid],
+    [PlacesUtils.mobileFolderId, PlacesUtils.bookmarks.mobileGuid],
   ]);
 
   let rows = yield db.execute(`SELECT id, guid FROM moz_bookmarks`);
   for (let row of rows) {
     let id = row.getResultByName("id");
     let guid = row.getResultByName("guid");
     Assert.equal(m.get(id), guid, "The root folder has the correct GUID");
   }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v34.js
@@ -0,0 +1,177 @@
+Cu.importGlobalProperties(["URL", "crypto"]);
+
+const { TYPE_BOOKMARK, TYPE_FOLDER } = Ci.nsINavBookmarksService;
+const { EXPIRE_NEVER, TYPE_INT32 } = Ci.nsIAnnotationService;
+
+function makeGuid() {
+  return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
+    pad: false,
+  });
+}
+
+// These queries are more or less copied directly from Bookmarks.jsm, but
+// operate on the old, pre-migration DB. We can't use any of the Places SQL
+// functions yet, because those are only registered for the main connection.
+function* insertItem(db, info) {
+  let [parentInfo] = yield db.execute(`
+    SELECT b.id, (SELECT count(*) FROM moz_bookmarks
+                  WHERE parent = b.id) AS childCount
+    FROM moz_bookmarks b
+    WHERE b.guid = :parentGuid`,
+    { parentGuid: info.parentGuid });
+
+  let guid = makeGuid();
+  yield db.execute(`
+    INSERT INTO moz_bookmarks (fk, type, parent, position, guid)
+    VALUES ((SELECT id FROM moz_places WHERE url = :url),
+            :type, :parent, :position, :guid)`,
+    { url: info.url || "nonexistent", type: info.type, guid,
+      // Just append items.
+      position: parentInfo.getResultByName("childCount"),
+      parent: parentInfo.getResultByName("id") });
+
+  let id = (yield db.execute(`
+    SELECT id FROM moz_bookmarks WHERE guid = :guid LIMIT 1`,
+    { guid }))[0].getResultByName("id");
+
+  return { id, guid };
+}
+
+function insertBookmark(db, info) {
+  return db.executeTransaction(function* () {
+    if (info.type == TYPE_BOOKMARK) {
+      // We don't have access to the hash function here, so we omit the
+      // `url_hash` column. These will be fixed up automatically during
+      // migration.
+      let url = new URL(info.url);
+      let placeGuid = makeGuid();
+      yield db.execute(`
+        INSERT INTO moz_places (url, rev_host, hidden, frecency, guid)
+        VALUES (:url, :rev_host, 0, -1, :guid)`,
+        { url: url.href, guid: placeGuid,
+          rev_host: PlacesUtils.getReversedHost(url) });
+    }
+    return yield* insertItem(db, info);
+  });
+}
+
+function* insertAnno(db, itemId, name, value) {
+  yield db.execute(`INSERT OR IGNORE INTO moz_anno_attributes (name)
+                    VALUES (:name)`, { name });
+  yield db.execute(`
+    INSERT INTO moz_items_annos
+           (item_id, anno_attribute_id, content, flags,
+            expiration, type, dateAdded, lastModified)
+    VALUES (:itemId,
+      (SELECT id FROM moz_anno_attributes
+       WHERE name = :name),
+      1, 0, :expiration, :type, 0, 0)
+    `, { itemId, name, expiration: EXPIRE_NEVER, type: TYPE_INT32 });
+}
+
+function insertMobileFolder(db) {
+  return db.executeTransaction(function* () {
+    let item = yield* insertItem(db, {
+      type: TYPE_FOLDER,
+      parentGuid: "root________",
+    });
+    yield* insertAnno(db, item.id, "mobile/bookmarksRoot", 1);
+    return item;
+  });
+}
+
+function insertMobileQuery(db, parentGuid, folderId) {
+  return db.executeTransaction(function* () {
+    let item = yield* insertItem(db, {
+      type: TYPE_BOOKMARK,
+      parentGuid,
+      url: `place:folder=${folderId}`,
+    });
+    yield* insertAnno(db, item.id, "MobileBookmarks", 0);
+    return item;
+  });
+}
+
+var mobileId, mobileGuid, fxGuid, queryGuid;
+var dupeMobileId, dupeMobileGuid, tbGuid, dupeQueryGuid;
+
+add_task(function* setup() {
+  yield setupPlacesDatabase("places_v34.sqlite");
+  // Setup database contents to be migrated.
+  let path = OS.Path.join(OS.Constants.Path.profileDir, DB_FILENAME);
+  let db = yield Sqlite.openConnection({ path });
+
+  do_print("Create mobile folder with bookmarks");
+  ({ id: mobileId, guid: mobileGuid } = yield insertMobileFolder(db));
+  ({ guid: fxGuid } = yield insertBookmark(db, {
+    type: TYPE_BOOKMARK,
+    url: "http://getfirefox.com",
+    parentGuid: mobileGuid,
+  }));
+
+  // We should only have one mobile folder, but, in case an old version of Sync
+  // did the wrong thing and created multiple mobile folders, we should merge
+  // their contents into the new mobile root.
+  do_print("Create second mobile folder with different bookmarks");
+  ({ id: dupeMobileId, guid: dupeMobileGuid } = yield insertMobileFolder(db));
+  ({ guid: tbGuid } = yield insertBookmark(db, {
+    type: TYPE_BOOKMARK,
+    url: "http://getthunderbird.com",
+    parentGuid: dupeMobileGuid,
+  }));
+
+  // Add queries that point to the old mobile folders. These should be
+  // deleted so that Sync can create a new one.
+  do_print("Insert query for mobile folder");
+  ({ guid: queryGuid} = yield insertMobileQuery(db, "toolbar_____", mobileId));
+
+  do_print("Insert query for second mobile folder");
+  ({ guid: dupeQueryGuid} = yield insertMobileQuery(db, "menu________",
+                                                    dupeMobileId));
+
+  yield db.close();
+});
+
+add_task(function* database_is_valid() {
+  // Accessing the database for the first time triggers migration.
+  Assert.equal(PlacesUtils.history.databaseStatus,
+               PlacesUtils.history.DATABASE_STATUS_UPGRADED);
+
+  let db = yield PlacesUtils.promiseDBConnection();
+  Assert.equal((yield db.getSchemaVersion()), CURRENT_SCHEMA_VERSION);
+});
+
+add_task(function* test_mobile_root() {
+  let fxBmk = yield PlacesUtils.bookmarks.fetch(fxGuid);
+  equal(fxBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+    "Firefox bookmark should be moved to new mobile root");
+  equal(fxBmk.index, 0, "Firefox bookmark should be first child of new root");
+
+  let tbBmk = yield PlacesUtils.bookmarks.fetch(tbGuid);
+  equal(tbBmk.parentGuid, PlacesUtils.bookmarks.mobileGuid,
+    "Thunderbird bookmark should be moved to new mobile root");
+  equal(tbBmk.index, 1,
+    "Thunderbird bookmark should be second child of new root");
+
+  let mobileRootId = PlacesUtils.promiseItemId(
+    PlacesUtils.bookmarks.mobileGuid);
+  let annoItemIds = PlacesUtils.annotations.getItemsWithAnnotation(
+    PlacesUtils.MOBILE_ROOT_ANNO, {});
+  deepEqual(annoItemIds, [mobileRootId],
+    "Only mobile root should have mobile anno");
+});
+
+add_task(function* test_mobile_queries() {
+  let mobileRootId = PlacesUtils.promiseItemId(
+    PlacesUtils.bookmarks.mobileGuid);
+
+  let query = yield PlacesUtils.bookmarks.fetch(queryGuid);
+  ok(!query, "Query should be removed");
+
+  let dupeQuery = yield PlacesUtils.bookmarks.fetch(dupeQueryGuid);
+  ok(!dupeQuery, "Dupe query should be removed");
+
+  let annoQueryIds = PlacesUtils.annotations.getItemsWithAnnotation(
+    "MobileBookmarks", {});
+  deepEqual(annoQueryIds, [], "All mobile query annos should be removed");
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -16,18 +16,20 @@ support-files =
   places_v26.sqlite
   places_v27.sqlite
   places_v28.sqlite
   places_v30.sqlite
   places_v31.sqlite
   places_v32.sqlite
   places_v33.sqlite
   places_v34.sqlite
+  places_v35.sqlite
 
 [test_current_from_downgraded.js]
 [test_current_from_v6.js]
 [test_current_from_v11.js]
 [test_current_from_v19.js]
 [test_current_from_v24.js]
 [test_current_from_v25.js]
 [test_current_from_v26.js]
 [test_current_from_v27.js]
 [test_current_from_v31.js]
+[test_current_from_v34.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"},{"guid":"XF4yRP6bTuil","title":"Mobile bookmarks query","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":5,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":9,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"KIa9iKZab2Z5","title":"Add-ons","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":10,"type":"text/x-moz-place","uri":"https://addons.mozilla.org"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":5,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":6,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":7,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":8,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"buy7711R3ZgE","title":"MDN","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":3,"type":"text/x-moz-place","uri":"https://developer.mozilla.org"},{"guid":"F_LBgd1fS_uQ","title":"Mobile bookmarks query for first folder","index":1,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":11,"type":"text/x-moz-place","uri":"place:folder=101"},{"guid":"oIpmQXMWsXvY","title":"Mobile bookmarks query for second folder","index":2,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":12,"type":"text/x-moz-place","uri":"place:folder=102"}]},{"guid":"3qmd_imziEBE","title":"Mobile Bookmarks","index":5,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":101,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1},{"name":"bookmarkProperties/description","flags":0,"expires":4,"mimeType":null,"type":3,"value":"A description of the mobile folder that should be ignored on import"}],"type":"text/x-moz-place-container","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":5,"type":"text/x-moz-place","uri":"https://mozilla.org/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":6,"type":"text/x-moz-place-container","root":"toolbarFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":7,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"o4YjJpgsufU-","title":"Mobile Bookmarks","index":7,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":102,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","children":[{"guid":"sSZ86WT9WbN3","title":"DXR","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":9,"type":"text/x-moz-place","uri":"https://dxr.mozilla.org"}]},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":10,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder","children":[{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":11,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731768000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"X6lUyOspVYwi","title":"Test Pilot","index":0,"dateAdded":1475084731768000,"lastModified":1475084731768000,"id":3,"type":"text/x-moz-place","uri":"https://testpilot.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731742000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731770000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"_o8e1_zxTJFg","title":"Get Firefox!","index":0,"dateAdded":1475084731769000,"lastModified":1475084731769000,"id":7,"type":"text/x-moz-place","uri":"http://getfirefox.com/"},{"guid":"QCtSqkVYUbXB","title":"Get Thunderbird!","index":1,"dateAdded":1475084731770000,"lastModified":1475084731770000,"id":8,"type":"text/x-moz-place","uri":"http://getthunderbird.com/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
@@ -0,0 +1,1 @@
+{"guid":"root________","title":"","index":0,"dateAdded":1475084731479000,"lastModified":1475084731479000,"id":1,"type":"text/x-moz-place-container","root":"placesRoot","children":[{"guid":"menu________","title":"Bookmarks Menu","index":0,"dateAdded":1475084731479000,"lastModified":1475084731955000,"id":2,"type":"text/x-moz-place-container","root":"bookmarksMenuFolder","children":[{"guid":"Utodo9b0oVws","title":"Firefox Accounts","index":0,"dateAdded":1475084731955000,"lastModified":1475084731955000,"id":3,"type":"text/x-moz-place","uri":"https://accounts.firefox.com/"}]},{"guid":"toolbar_____","title":"Bookmarks Toolbar","index":1,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":4,"type":"text/x-moz-place-container","root":"toolbarFolder"},{"guid":"unfiled_____","title":"Other Bookmarks","index":3,"dateAdded":1475084731479000,"lastModified":1475084731938000,"id":5,"type":"text/x-moz-place-container","root":"unfiledBookmarksFolder"},{"guid":"mobile______","title":"Mobile Bookmarks","index":4,"dateAdded":1475084731479000,"lastModified":1475084731961000,"id":6,"annos":[{"name":"mobile/bookmarksRoot","flags":0,"expires":4,"value":1}],"type":"text/x-moz-place-container","root":"mobileFolder","children":[{"guid":"a17yW6-nTxEJ","title":"Mozilla","index":0,"dateAdded":1475084731959000,"lastModified":1475084731959000,"id":7,"type":"text/x-moz-place","uri":"https://mozilla.org/"},{"guid":"xV10h9Wi3FBM","title":"Bugzilla","index":1,"dateAdded":1475084731961000,"lastModified":1475084731961000,"id":8,"type":"text/x-moz-place","uri":"https://bugzilla.mozilla.org/"}]}]}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -0,0 +1,292 @@
+function* importFromFixture(fixture, replace) {
+  let cwd = yield OS.File.getCurrentDirectory();
+  let path = OS.Path.join(cwd, fixture);
+
+  do_print(`Importing from ${path}`);
+  yield BookmarkJSONUtils.importFromFile(path, replace);
+  yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+function* treeEquals(guid, expected, message) {
+  let root = yield PlacesUtils.promiseBookmarksTree(guid);
+  let bookmarks = (function nodeToEntry(node) {
+    let entry = { guid: node.guid, index: node.index }
+    if (node.children) {
+      entry.children = node.children.map(nodeToEntry);
+    }
+    if (node.annos) {
+      entry.annos = node.annos;
+    }
+    return entry;
+  }(root));
+
+  do_print(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`);
+  do_print(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`);
+
+  deepEqual(bookmarks, expected, message);
+}
+
+add_task(function* test_restore_mobile_bookmarks_root() {
+  yield* importFromFixture("mobile_bookmarks_root_import.json",
+                           /* replace */ true);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "X6lUyOspVYwi", index: 0 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "_o8e1_zxTJFg", index: 0 },
+        { guid: "QCtSqkVYUbXB", index: 1 },
+      ],
+    }],
+  }, "Should restore mobile bookmarks from root");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_root() {
+  yield* importFromFixture("mobile_bookmarks_root_import.json",
+                           /* replace */ false);
+  yield* importFromFixture("mobile_bookmarks_root_merge.json",
+                           /* replace */ false);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "Utodo9b0oVws", index: 0 },
+        { guid: "X6lUyOspVYwi", index: 1 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "a17yW6-nTxEJ", index: 0 },
+        { guid: "xV10h9Wi3FBM", index: 1 },
+        { guid: "_o8e1_zxTJFg", index: 2 },
+        { guid: "QCtSqkVYUbXB", index: 3 },
+      ],
+    }],
+  }, "Should merge bookmarks root contents");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_mobile_bookmarks_folder() {
+  yield* importFromFixture("mobile_bookmarks_folder_import.json",
+                           /* replace */ true);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "X6lUyOspVYwi", index: 0 },
+        { guid: "XF4yRP6bTuil", index: 1 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+      children: [{ guid: "buy7711R3ZgE", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+      children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "_o8e1_zxTJFg", index: 0 },
+        { guid: "QCtSqkVYUbXB", index: 1 },
+      ],
+    }],
+  }, "Should restore mobile bookmark folder contents into mobile root");
+
+  // We rewrite queries to point to the root ID instead of the name
+  // ("MOBILE_BOOKMARKS") so that we don't break them if the user downgrades
+  // to an earlier release channel. This can be removed along with the anno in
+  // bug 1306445.
+  let queryById = yield PlacesUtils.bookmarks.fetch("XF4yRP6bTuil");
+  equal(queryById.url.href, "place:folder=" + PlacesUtils.mobileFolderId,
+    "Should rewrite mobile query to point to root ID");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_folder() {
+  yield* importFromFixture("mobile_bookmarks_folder_import.json",
+                           /* replace */ false);
+  yield* importFromFixture("mobile_bookmarks_folder_merge.json",
+                           /* replace */ false);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "Utodo9b0oVws", index: 0 },
+        { guid: "X6lUyOspVYwi", index: 1 },
+        { guid: "XF4yRP6bTuil", index: 2 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+      children: [{ guid: "buy7711R3ZgE", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+      children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "a17yW6-nTxEJ", index: 0 },
+        { guid: "xV10h9Wi3FBM", index: 1 },
+        { guid: "_o8e1_zxTJFg", index: 2 },
+        { guid: "QCtSqkVYUbXB", index: 3 },
+      ],
+    }],
+  }, "Should merge bookmarks folder contents into mobile root");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_multiple_bookmarks_folders() {
+  yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+                           /* replace */ true);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "buy7711R3ZgE", index: 0 },
+        { guid: "F_LBgd1fS_uQ", index: 1 },
+        { guid: "oIpmQXMWsXvY", index: 2 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+      children: [{ guid: "Utodo9b0oVws", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+      children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "sSZ86WT9WbN3", index: 0 },
+        { guid: "a17yW6-nTxEJ", index: 1 },
+      ],
+    }],
+  }, "Should restore multiple bookmarks folder contents into root");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_multiple_bookmarks_folders() {
+  yield* importFromFixture("mobile_bookmarks_root_import.json",
+                           /* replace */ false);
+  yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+                           /* replace */ false);
+
+  yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+    guid: PlacesUtils.bookmarks.rootGuid,
+    index: 0,
+    children: [{
+      guid: PlacesUtils.bookmarks.menuGuid,
+      index: 0,
+      children: [
+        { guid: "buy7711R3ZgE", index: 0 },
+        { guid: "F_LBgd1fS_uQ", index: 1 },
+        { guid: "oIpmQXMWsXvY", index: 2 },
+        { guid: "X6lUyOspVYwi", index: 3 },
+      ],
+    }, {
+      guid: PlacesUtils.bookmarks.toolbarGuid,
+      index: 1,
+      children: [{ guid: "Utodo9b0oVws", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.unfiledGuid,
+      index: 3,
+      children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+    }, {
+      guid: PlacesUtils.bookmarks.mobileGuid,
+      index: 4,
+      annos: [{
+        name: "mobile/bookmarksRoot",
+        flags: 0,
+        expires: 4,
+        value: 1,
+      }],
+      children: [
+        { guid: "sSZ86WT9WbN3", index: 0 },
+        { guid: "a17yW6-nTxEJ", index: 1 },
+        { guid: "_o8e1_zxTJFg", index: 2 },
+        { guid: "QCtSqkVYUbXB", index: 3 },
+      ],
+    }],
+  }, "Should merge multiple mobile folders into root");
+
+  yield PlacesUtils.bookmarks.eraseEverything();
+});
--- a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -112,16 +112,19 @@ function* compareToNode(aItem, aNode, aI
         compare_prop_to_value("root", "bookmarksMenuFolder");
         break;
       case PlacesUtils.toolbarFolderId:
         compare_prop_to_value("root", "toolbarFolder");
         break;
       case PlacesUtils.unfiledBookmarksFolderId:
         compare_prop_to_value("root", "unfiledBookmarksFolder");
         break;
+      case PlacesUtils.mobileFolderId:
+        compare_prop_to_value("root", "mobileFolder");
+        break;
       default:
         check_unset("root");
       }
       break;
     case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
       do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
       check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
       break;
@@ -243,11 +246,11 @@ add_task(function* () {
   let placesRootWithoutTheMenu =
   yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, {
     excludeItemsCallback: aItem =>  {
       guidsPassedToExcludeCallback.add(aItem.guid);
       return aItem.root == "bookmarksMenuFolder";
     },
     includeItemIds: true
   }, [PlacesUtils.bookmarks.menuGuid]);
-  do_check_eq(guidsPassedToExcludeCallback.size, 4);
-  do_check_eq(placesRootWithoutTheMenu.children.length, 2);
+  do_check_eq(guidsPassedToExcludeCallback.size, 5);
+  do_check_eq(placesRootWithoutTheMenu.children.length, 3);
 });
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -14,17 +14,19 @@ var histograms = {
   PLACES_TAGGED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
   PLACES_DATABASE_FILESIZE_MB: val => do_check_true(val > 0),
   PLACES_DATABASE_PAGESIZE_B: val => do_check_eq(val, 32768),
   PLACES_DATABASE_SIZE_PER_PAGE_B: val => do_check_true(val > 0),
   PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => do_check_true(val > 1),
   //PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS:  val => do_check_true(val > 1),
   PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val > 0),
   PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0),
-  PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 1),
+  // One from the `setItemAnnotation` call; the other from the mobile root.
+  // This can be removed along with the anno in bug 1306445.
+  PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 2),
   PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1),
   PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0),
 }
 
 /**
  * Forces an expiration run.
  *
  * @param [optional] aLimit
--- a/toolkit/components/places/tests/unit/xpcshell.ini
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -6,16 +6,21 @@ skip-if = toolkit == 'android' || toolki
 support-files =
   bookmarks.corrupt.html
   bookmarks.json
   bookmarks.preplaces.html
   bookmarks_html_singleframe.html
   bug476292.sqlite
   default.sqlite
   livemark.xml
+  mobile_bookmarks_folder_import.json
+  mobile_bookmarks_folder_merge.json
+  mobile_bookmarks_multiple_folders.json
+  mobile_bookmarks_root_import.json
+  mobile_bookmarks_root_merge.json
   nsDummyObserver.js
   nsDummyObserver.manifest
   places.sparse.sqlite
 
 [test_000_frecency.js]
 [test_317472.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
@@ -98,16 +103,17 @@ skip-if = os == "android"
 [test_history_catobs.js]
 [test_history_clear.js]
 [test_history_notifications.js]
 [test_history_observer.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_history_sidebar.js]
 [test_hosts_triggers.js]
+[test_import_mobile_bookmarks.js]
 [test_isPageInDB.js]
 [test_isURIVisited.js]
 [test_isvisited.js]
 [test_keywords.js]
 [test_lastModified.js]
 [test_markpageas.js]
 [test_mozIAsyncLivemarks.js]
 [test_multi_queries.js]
--- a/toolkit/locales/en-US/chrome/places/places.properties
+++ b/toolkit/locales/en-US/chrome/places/places.properties
@@ -1,16 +1,17 @@
 # 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/.
 
 BookmarksMenuFolderTitle=Bookmarks Menu
 BookmarksToolbarFolderTitle=Bookmarks Toolbar
 OtherBookmarksFolderTitle=Other Bookmarks
 TagsFolderTitle=Tags
+MobileBookmarksFolderTitle=Mobile Bookmarks
 
 # LOCALIZATION NOTE (dateName):
 # These are used to generate history containers when history is grouped by date
 finduri-AgeInDays-is-0=Today
 finduri-AgeInDays-is-1=Yesterday
 finduri-AgeInDays-is=%S days ago
 finduri-AgeInDays-last-is=Last %S days
 finduri-AgeInDays-isgreater=Older than %S days