[WIP] Bug 972193 - Part 1: introduce BrowserDB.getBookmarkFolders draft
authorAndrzej Hunt <ahunt@mozilla.com>
Fri, 01 Apr 2016 13:10:17 -0700
changeset 346988 a5ff4f92c4ebc7e8b536ed645c44bf4e52755185
parent 346987 8407593f03504c172f8c2d62135f3a058002f267
child 517526 65f9949d1a4b10977c7052db84a8277ef6aba36d
push id14468
push userahunt@mozilla.com
push dateFri, 01 Apr 2016 20:14:57 +0000
bugs972193
milestone48.0a1
[WIP] Bug 972193 - Part 1: introduce BrowserDB.getBookmarkFolders MozReview-Commit-ID: A5x4HXa7Pe0
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import java.io.File;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.TreeMap;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 
 import android.content.ContentProviderOperation;
@@ -27,16 +28,34 @@ import android.graphics.drawable.BitmapD
  * that implements this, you should go through GeckoProfile. E.g.,
  * <code>GeckoProfile.get(context).getDB()</code>.
  *
  * GeckoProfile itself will construct an appropriate subclass using
  * a factory that the containing application can set with
  * {@link GeckoProfile#setBrowserDBFactory(BrowserDB.Factory)}.
  */
 public interface BrowserDB {
+    public class BookmarkFolderTree {
+        // Using a TreeMap allows walking the folders in alphabetical order
+        // In most cases the Key will correspond to the folder's name, however this is not true
+        // in all cases. We use artificial keys for the Mobile/Desktop folder to guarantee
+        // ordering within the root.
+        public TreeMap<String, BookmarkFolderTree> children = new TreeMap<>();
+
+        public final int id;
+        public final int parent;
+        public final String name;
+
+        public BookmarkFolderTree(int bookmarkID, int parentID, String bookmarkName) {
+            this.id = bookmarkID;
+            this.parent = parentID;
+            this.name = bookmarkName;
+        }
+    }
+
     public interface Factory {
         public BrowserDB get(String profileName, File profileDir);
     }
 
     public static enum FilterFlags {
         EXCLUDE_PINNED_SITES
     }
 
@@ -116,16 +135,18 @@ public interface BrowserDB {
     public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
     public abstract boolean hasBookmarkWithGuid(ContentResolver cr, String guid);
 
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
 
+    public BookmarkFolderTree getBookmarkFolders(ContentResolver cr, Context context);
+
     /**
      * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
      * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
      * @param cr The ContentResolver to use.
      * @param faviconURL The URL of the favicon to fetch from the database.
      * @return The decoded Bitmap from the database, if any. null if none is stored.
      */
     public abstract LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL);
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -1714,9 +1714,133 @@ public class LocalBrowserDB implements B
         rb.add(-1);
         rb.add("");
         rb.add("");
         rb.add(TopSites.TYPE_BLANK);
 
         return new MergeCursor(new Cursor[] {topSitesCursor, blanksCursor});
 
     }
+
+    private static void debugDumpFolder(BookmarkFolderTree folder, String prefix) {
+        if (folder == null) {
+            return;
+        }
+
+        Log.w("FOLDERS", prefix + folder.name);
+        for (BookmarkFolderTree f : folder.children.values()) {
+            debugDumpFolder(f, prefix + "*");
+        }
+    }
+
+    public BookmarkFolderTree getBookmarkFolders(ContentResolver cr, Context context) {
+        final Cursor c = cr.query(mBookmarksUriWithProfile,
+                new String[] {
+                        Bookmarks._ID,
+                        Bookmarks.PARENT,
+                        Bookmarks.GUID,
+                        Bookmarks.TITLE
+                },
+                Bookmarks.TYPE + " = " + Bookmarks.TYPE_FOLDER +
+                " AND " + Bookmarks.GUID + " IS NOT ?" +
+                " AND " + Bookmarks.GUID + " IS NOT ?" +
+                " AND " + Bookmarks._ID + " IS NOT " + Bookmarks.FIXED_READING_LIST_ID,
+                new String[] {
+                        Bookmarks.PINNED_FOLDER_GUID,
+                        Bookmarks.TAGS_FOLDER_GUID
+                },
+                null);
+
+        if (c == null) {
+            return null;
+        }
+
+        final HashMap<Integer, BookmarkFolderTree> nodes = new HashMap<>();
+
+        final String desktopFolderName = "Desktop";
+        final BookmarkFolderTree fakeDesktop = new BookmarkFolderTree(Bookmarks.FAKE_DESKTOP_FOLDER_ID, Bookmarks.FIXED_ROOT_ID, desktopFolderName);
+        nodes.put(Bookmarks.FAKE_DESKTOP_FOLDER_ID, fakeDesktop);
+
+        final int idIndex = c.getColumnIndexOrThrow(Bookmarks._ID);
+        final int parentIndex = c.getColumnIndexOrThrow(Bookmarks.PARENT);
+        final int nameIndex = c.getColumnIndexOrThrow(Bookmarks.TITLE);
+        final int guidIndex = c.getColumnIndexOrThrow(Bookmarks.GUID);
+
+        try {
+            if (!c.moveToFirst()) {
+                return null;
+            }
+
+            do {
+                final String guid = c.getString(guidIndex);
+                final int parentId;
+
+                switch (guid) {
+                    // By default these are subfolders of places, however we want to make them
+                    // subfolders of Desktop Bookmarks.
+                    case Bookmarks.TOOLBAR_FOLDER_GUID:
+                    case Bookmarks.UNFILED_FOLDER_GUID:
+                    case Bookmarks.MENU_FOLDER_GUID:
+                        parentId = Bookmarks.FAKE_DESKTOP_FOLDER_ID;
+                        break;
+
+                    default:
+                        parentId = c.getInt(parentIndex);
+                }
+
+                final String folderName = c.getString(nameIndex);
+                final int id = c.getInt(idIndex);
+
+                BookmarkFolderTree folder = new BookmarkFolderTree(id, parentId, folderName);
+
+                // We want to ignore any folders that aren't either a subfolder of one of the 3 desktop folders,
+                // or a subfolder of mobile. I.e. we want to discard all folders with places as their parent
+                // (which includes the pinned folder, and "All Bookmarks"), except for those 3 desktop folders (handled
+                // above), and mobile bookmarks and places itself (which has itself as its parent) -
+                // handled here.
+                if (parentId != Bookmarks.FIXED_ROOT_ID ||
+                        guid.equals(Bookmarks.MOBILE_FOLDER_GUID) ||
+                        id == Bookmarks.FIXED_ROOT_ID) {
+                    nodes.put(folder.id, folder);
+                }
+            } while (c.moveToNext());
+        } finally {
+            c.close();
+        }
+
+        // Build the tree!
+        for (BookmarkFolderTree folder : nodes.values()) {
+            BookmarkFolderTree parent = nodes.get(folder.parent);
+
+            if (parent == null || parent == folder) {
+                // We need to avoid parenting:
+                // (1) the root / places folder: it is its own parent, attaching it would create a loop
+                // (2) orphaned folders - this can happen if sync has problems
+                continue;
+            }
+
+            // We want to ensure that Mobile bookmarks appear before desktop bookmarks.
+            if (parent.id == Bookmarks.FIXED_ROOT_ID) {
+                if (folder.id == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
+                    parent.children.put("B", folder);
+                } else {
+                    // Mobile folder: it's the only remaining folder (we've filtered out all
+                    // other first-level folders in our first pass). We could uniquely identify
+                    // it by the GUID, however adding an additional field to track GUID in
+                    // BookmarkFolderTree seems wasteful.
+                    parent.children.put("A", folder);
+                }
+            } else {
+                parent.children.put(folder.name, folder);
+            }
+        }
+
+        final BookmarkFolderTree root = nodes.get(0);
+
+        if (root == null) {
+            throw new IllegalStateException("root / places bookmarks folder must exist");
+        }
+
+        debugDumpFolder(root, "*");
+
+        return root;
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -287,16 +287,20 @@ public class StubBrowserDB implements Br
     public void clearHistory(ContentResolver cr, boolean clearSearchHistory) {
     }
 
     @RobocopTarget
     public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
         return null;
     }
 
+    public BookmarkFolderTree getBookmarkFolders(ContentResolver cr, Context context) {
+        return null;
+    }
+
     public Cursor getReadingList(ContentResolver cr) {
         return null;
     }
 
     public Cursor getReadingListUnfetched(ContentResolver cr) {
         return null;
     }