[WIP] Bug 1232439 (972193) - Part 1: introduce BrowserDB.getBookmarkFolders draft
authorAndrzej Hunt <ahunt@mozilla.com>
Fri, 01 Apr 2016 13:10:17 -0700
changeset 453951 a007bfd879cc0579c1d3966f19abf977ddd13ffe
parent 453950 cec4609611602aca8330b1e469ad965636c27d30
child 453952 28d57b8b4ec6f8972181294c8ad7738dbfbbca83
push id39776
push userbmo:twointofive@gmail.com
push dateMon, 26 Dec 2016 21:15:51 +0000
bugs1232439, 972193
milestone53.0a1
[WIP] Bug 1232439 (972193) - Part 1: introduce BrowserDB.getBookmarkFolders Rebased by Tom Klein. MozReview-Commit-ID: 53xErLzTiq5
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.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.icons.decoders.LoadFaviconResult;
 
 import android.content.ContentProviderClient;
@@ -25,16 +26,34 @@ import android.graphics.drawable.BitmapD
 import android.support.v4.content.CursorLoader;
 
 /**
  * Interface for interactions with all databases. If you want an instance
  * that implements this, you should go through GeckoProfile. E.g.,
  * <code>BrowserDB.from(context)</code>.
  */
 public abstract class 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 static enum FilterFlags {
         EXCLUDE_PINNED_SITES
     }
 
     public abstract Searches getSearches();
     public abstract TabsAccessor getTabsAccessor();
     public abstract URLMetadata getURLMetadata();
     @RobocopTarget public abstract UrlAnnotations getUrlAnnotations();
@@ -116,16 +135,18 @@ public abstract class BrowserDB {
 
     public abstract boolean insertPageMetadata(ContentProviderClient contentProviderClient, String pageUrl, boolean hasImage, String metadataJSON);
     public abstract int deletePageMetadata(ContentProviderClient contentProviderClient, String pageUrl);
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
 
+    public abstract BookmarkFolderTree getBookmarkFolders(ContentResolver cr);
+
     public abstract int getBookmarkCountForFolder(ContentResolver cr, long folderId);
 
     /**
      * 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.
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -1999,9 +1999,132 @@ public class LocalBrowserDB extends Brow
 
     @Override
     public void blockActivityStreamSite(ContentResolver cr, String url) {
         final ContentValues values = new ContentValues();
         values.put(ActivityStreamBlocklist.URL, url);
         cr.insert(mActivityStreamBlockedUriWithProfile, values);
     }
 
+    private static void debugDumpFolder(BookmarkFolderTree folder, String prefix) {
+        if (folder == null) {
+            return;
+        }
+
+        Log.d("***FOLDERS", prefix + folder.name);
+        for (BookmarkFolderTree f : folder.children.values()) {
+            debugDumpFolder(f, prefix + "*");
+        }
+    }
+
+    public BookmarkFolderTree getBookmarkFolders(ContentResolver cr) {
+        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;
+    }
 }