Bug 722020 - Bookmarks UI with folders. r=lucasr
authorMargaret Leibovic <margaret.leibovic@gmail.com>
Thu, 23 Feb 2012 10:48:47 -0800
changeset 87561 230c6a8153c6dbbb5d351314925dd5a534bcb70b
parent 87560 43a4ce8728ca704717b523cf4992c169b3c5b708
child 87562 fa2de31b5ac0153fca30628a83546879d6a4712d
push id22130
push userrnewman@mozilla.com
push dateFri, 24 Feb 2012 02:35:54 +0000
treeherdermozilla-central@d23600a1d4a7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs722020
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 722020 - Bookmarks UI with folders. r=lucasr
mobile/android/base/AwesomeBarTabs.java
mobile/android/base/Makefile.in
mobile/android/base/db/AndroidBrowserDB.java
mobile/android/base/db/BrowserDB.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/base/resources/drawable-hdpi/folder.png
mobile/android/base/resources/drawable-xhdpi-v11/folder.png
mobile/android/base/resources/drawable/folder.png
mobile/android/base/resources/layout/awesomebar_folder_header_row.xml
mobile/android/base/resources/layout/awesomebar_folder_row.xml
mobile/android/base/resources/layout/awesomebar_tabs.xml
--- a/mobile/android/base/AwesomeBarTabs.java
+++ b/mobile/android/base/AwesomeBarTabs.java
@@ -15,16 +15,17 @@
  * The Original Code is Mozilla Android code.
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2009-2010
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Lucas Rocha <lucasr@mozilla.com>
+ *   Margaret Leibovic <margaret.leibovic@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -36,17 +37,16 @@
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.MatrixCursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
 import android.graphics.LightingColorFilter;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.SystemClock;
 import android.text.TextUtils;
@@ -59,36 +59,35 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AdapterView;
 import android.widget.ExpandableListView;
 import android.widget.FilterQueryProvider;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.SimpleCursorAdapter;
-import android.widget.SimpleCursorTreeAdapter;
 import android.widget.SimpleExpandableListAdapter;
 import android.widget.TabHost;
 import android.widget.TextView;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
-import org.mozilla.gecko.sync.repositories.android.BrowserContract.Bookmarks;
 
 public class AwesomeBarTabs extends TabHost {
     private static final String LOGTAG = "GeckoAwesomeBarTabs";
 
     private static final String ALL_PAGES_TAB = "all";
     private static final String BOOKMARKS_TAB = "bookmarks";
     private static final String HISTORY_TAB = "history";
 
@@ -144,18 +143,17 @@ public class AwesomeBarTabs extends TabH
                 Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
                 favicon.setImageBitmap(bitmap);
             }
 
             return childView;
         }
     }
 
-    private class AwesomeCursorViewBinder implements SimpleCursorAdapter.ViewBinder,
-                                                     SimpleCursorTreeAdapter.ViewBinder {
+    private class AwesomeCursorViewBinder implements SimpleCursorAdapter.ViewBinder {
         private boolean updateFavicon(View view, Cursor cursor, int faviconIndex) {
             byte[] b = cursor.getBlob(faviconIndex);
             ImageView favicon = (ImageView) view;
 
             if (b == null) {
                 favicon.setImageDrawable(null);
             } else {
                 Bitmap bitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
@@ -175,136 +173,187 @@ public class AwesomeBarTabs extends TabH
                 title = cursor.getString(urlIndex);
             }
 
             titleView.setText(title);
             return true;
         }
 
         public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
-            // If we're updating a folder header in the bookmarks UI,
-            // the apadter updates the title for us automatically.
-            int isFolderIndex = cursor.getColumnIndex(Bookmarks.IS_FOLDER);
-            if (isFolderIndex >= 0 && cursor.getInt(isFolderIndex) == 1)
-                return false;
-
             int faviconIndex = cursor.getColumnIndexOrThrow(URLColumns.FAVICON);
             if (columnIndex == faviconIndex) {
                 return updateFavicon(view, cursor, faviconIndex);
             }
 
             int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE);
             if (columnIndex == titleIndex) {
                 return updateTitle(view, cursor, titleIndex);
             }
 
             // Other columns are handled automatically
             return false;
         }
     }
 
-    private class RefreshChildrenCursorTask extends AsyncTask<String, Void, Cursor> {
-        private int mGroupPosition;
+    private class BookmarksListAdapter extends SimpleCursorAdapter {
+        private static final int VIEW_TYPE_ITEM = 0;
+        private static final int VIEW_TYPE_FOLDER = 1;
+        private static final int VIEW_TYPE_COUNT = 2;
+
+        private LayoutInflater mInflater;
+        private LinkedList<Pair<Integer, String>> mParentStack;
+        private RefreshBookmarkCursorTask mRefreshTask = null;
+        private TextView mBookmarksTitleView;
+
+        public BookmarksListAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
+            super(context, layout, c, from, to);
+
+            mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
 
-        public RefreshChildrenCursorTask(int groupPosition) {
-            mGroupPosition = groupPosition;
+            // mParentStack holds folder id/title pairs that allow us to navigate
+            // back up the folder heirarchy
+            mParentStack = new LinkedList<Pair<Integer, String>>();
+
+            // Add the root folder to the stack
+            Pair<Integer, String> rootFolder = new Pair<Integer, String>(Bookmarks.FIXED_ROOT_ID, "");
+            mParentStack.push(rootFolder);
+        }
+
+        public void refreshCurrentFolder() {
+            // Cancel any pre-existing async refresh tasks
+            if (mRefreshTask != null)
+                mRefreshTask.cancel(false);
+
+            Pair<Integer, String> folderPair = mParentStack.getFirst();
+            mRefreshTask = new RefreshBookmarkCursorTask(folderPair.first, folderPair.second);
+            mRefreshTask.execute();
         }
 
+        public void moveToParentFolder() {
+            mParentStack.pop();
+            refreshCurrentFolder();
+        }
+
+        public void moveToChildFolder(int folderId, String folderTitle) {
+            Pair<Integer, String> folderPair = new Pair<Integer, String>(folderId, folderTitle);
+            mParentStack.push(folderPair);
+            refreshCurrentFolder();
+        }
+
+        public int getItemViewType(int position) {
+            Cursor c = getCursor();
+ 
+            if (c.moveToPosition(position) &&
+                c.getInt(c.getColumnIndexOrThrow(Bookmarks.IS_FOLDER)) == 1)
+                return VIEW_TYPE_FOLDER;
+
+            // Default to retuning normal item type
+            return VIEW_TYPE_ITEM;
+        }
+ 
         @Override
-        protected Cursor doInBackground(String... params) {
-            String guid = params[0];
-            if (guid != null && guid.equals(Bookmarks.MOBILE_FOLDER_GUID))
-                return BrowserDB.getMobileBookmarks(mContentResolver);
+        public int getViewTypeCount() {
+            return VIEW_TYPE_COUNT;
+        }
 
-            // If we don't have the mobile bookmarks folder, we must have
-            // the desktop bookmarks folder
-            return BrowserDB.getDesktopBookmarks(mContentResolver);
+        public String getFolderTitle(int position) {
+            Cursor c = getCursor();
+            if (!c.moveToPosition(position))
+                return "";
+
+            return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE));
         }
 
         @Override
-        protected void onPostExecute(Cursor childrenCursor) {
-            mBookmarksAdapter.setChildrenCursor(mGroupPosition, childrenCursor);
-        }
-    }
+        public View getView(int position, View convertView, ViewGroup parent) {
+            int viewType = getItemViewType(position);
+
+            if (viewType == VIEW_TYPE_ITEM)
+                return super.getView(position, convertView, parent);
+
+            if (convertView == null)
+                convertView = mInflater.inflate(R.layout.awesomebar_folder_row, null);
 
-    public class BookmarksListAdapter extends SimpleCursorTreeAdapter {
-        public BookmarksListAdapter(Context context, Cursor cursor,
-                int groupLayout, String[] groupFrom, int[] groupTo,
-                int childLayout, String[] childFrom, int[] childTo) {
-            super(context, cursor, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
+            TextView titleView = (TextView) convertView.findViewById(R.id.title);
+            titleView.setText(getFolderTitle(position));
+
+            return convertView;
+        }
+
+        public void setBookmarksTitleView(TextView titleView) {
+            mBookmarksTitleView = titleView;
         }
 
-        @Override
-        protected Cursor getChildrenCursor(Cursor groupCursor) {
-            String guid = groupCursor.getString(groupCursor.getColumnIndexOrThrow(Bookmarks.GUID));
+        private class RefreshBookmarkCursorTask extends AsyncTask<Void, Void, Cursor> {
+            private int mFolderId;
+            private String mFolderTitle;
+
+            public RefreshBookmarkCursorTask(int folderId, String folderTitle) {
+                mFolderId = folderId;
+                mFolderTitle = folderTitle;
+            }
+
+            protected Cursor doInBackground(Void... params) {
+                return BrowserDB.getBookmarksInFolder(mContentResolver, mFolderId);
+            }
 
-            // We need to do this in a AsyncTask because we're on the main thread
-            new RefreshChildrenCursorTask(groupCursor.getPosition()).execute(guid);
+            protected void onPostExecute(Cursor cursor) {
+                mRefreshTask = null;
+                mBookmarksAdapter.changeCursor(cursor);
 
-            // Return an empty Cursor to avoid possible NPE
-            return new MatrixCursor(new String [] { Bookmarks._ID,
-                                                    Bookmarks.URL,
-                                                    Bookmarks.TITLE,
-                                                    Bookmarks.FAVICON });
+                // Hide the header text if we're at the root folder
+                if (mFolderId == Bookmarks.FIXED_ROOT_ID) {
+                    mBookmarksTitleView.setVisibility(View.GONE);
+                } else {
+                    mBookmarksTitleView.setText(mFolderTitle);
+                    mBookmarksTitleView.setVisibility(View.VISIBLE);
+                }
+            }
         }
     }
 
     private class BookmarksQueryTask extends AsyncTask<Void, Void, Cursor> {
         protected Cursor doInBackground(Void... arg0) {
-            // Make our own cursor to group mobile bookmarks and desktop bookmarks.
-            // This data is used in BookmarksListAdapter and AwesomeCursorViewBinder.
-            MatrixCursor c = new MatrixCursor(new String[] { Bookmarks._ID,
-                                                             Bookmarks.IS_FOLDER,
-                                                             Bookmarks.GUID,
-                                                             URLColumns.TITLE }, 2);
-
-            Resources resources = mContext.getResources();
-            c.addRow(new Object[] { 0, 1, Bookmarks.MOBILE_FOLDER_GUID,
-                                    resources.getString(R.string.bookmarks_folder_mobile)} );
-            c.addRow(new Object[] { 1, 1, null,
-                                    resources.getString(R.string.bookmarks_folder_desktop)} );
-            return c;
+            return BrowserDB.getBookmarksInFolder(mContentResolver, Bookmarks.FIXED_ROOT_ID);
         }
 
         protected void onPostExecute(Cursor cursor) {
             // Load the list using a custom adapter so we can create the bitmaps
             mBookmarksAdapter = new BookmarksListAdapter(
                 mContext,
+                R.layout.awesomebar_row,
                 cursor,
-                R.layout.awesomebar_header_row,
-                new String[] { URLColumns.TITLE },
-                new int[] { R.id.title },
-                R.layout.awesomebar_row,
                 new String[] { URLColumns.TITLE,
                                URLColumns.URL,
                                URLColumns.FAVICON },
                 new int[] { R.id.title, R.id.url, R.id.favicon }
             );
 
             mBookmarksAdapter.setViewBinder(new AwesomeCursorViewBinder());
 
-            final ExpandableListView bookmarksList = (ExpandableListView) findViewById(R.id.bookmarks_list);
-
-            // This listener only applies to child items, not group header items.
-            bookmarksList.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
-                public boolean onChildClick(ExpandableListView parent, View view,
-                        int groupPosition, int childPosition, long id) {
-                    handleBookmarkItemClick(groupPosition, childPosition);
-                    return true;
+            ListView bookmarksList = (ListView) findViewById(R.id.bookmarks_list);
+            bookmarksList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                    handleBookmarkItemClick(position);
                 }
             });
 
-            bookmarksList.setAdapter(mBookmarksAdapter);
+            // We need to add the header before we set the adapter
+            LayoutInflater inflater =
+                    (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            View headerView = inflater.inflate(R.layout.awesomebar_folder_header_row, null);
 
-            // Expand the "Mobile Bookmarks" section
-            bookmarksList.expandGroup(0);
-            // Expand the "Desktop Bookmarks" section
-            // TODO: Once we update the UI to include a better "expand" affordance,
-            // we can collapse this at first if we want.
-            bookmarksList.expandGroup(1);
+            // Hide the header title view to begin with
+            TextView titleView = (TextView) headerView.findViewById(R.id.title);
+            titleView.setVisibility(View.GONE);
+            mBookmarksAdapter.setBookmarksTitleView(titleView);
+
+            bookmarksList.addHeaderView(headerView, null, true);
+
+            bookmarksList.setAdapter(mBookmarksAdapter);
         }
     }
 
     private static class GroupList extends LinkedList<Map<String,String>> {
         private static final long serialVersionUID = 0L;
     }
 
     private static class ChildrenList extends LinkedList<Map<String,Object>> {
@@ -761,20 +810,39 @@ public class AwesomeBarTabs extends TabH
 
     private void hideSoftInput(View view) {
         InputMethodManager imm =
                 (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
 
         imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
     }
 
-    private void handleBookmarkItemClick(int groupPosition, int childPosition) {
-        Cursor cursor = mBookmarksAdapter.getChild(groupPosition, childPosition);
+    private void handleBookmarkItemClick(int position) {
+        // If we tap on the header view, go up a level
+        if (position == 0) {
+            mBookmarksAdapter.moveToParentFolder();
+            return;
+        }
+
+        Cursor cursor = mBookmarksAdapter.getCursor();
+        // The header view takes up a spot in the list
+        cursor.moveToPosition(position - 1);
+
+        int isFolder = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.IS_FOLDER));
+        if (isFolder == 1) {
+            // If we're clicking on a folder, update mBookmarksAdapter to move to that folder
+            int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
+            String folderTitle = mBookmarksAdapter.getFolderTitle(position - 1);
+
+            mBookmarksAdapter.moveToChildFolder(folderId, folderTitle);
+            return;
+        }
+
+        // Otherwise, just open the URL
         String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
-
         if (mUrlOpenListener != null)
             mUrlOpenListener.onUrlOpen(url);
     }
 
     private void handleHistoryItemClick(int groupPosition, int childPosition) {
         @SuppressWarnings("unchecked")
         Map<String,Object> historyItem =
                 (Map<String,Object>) mHistoryAdapter.getChild(groupPosition, childPosition);
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -216,16 +216,18 @@ ICON_PATH = $(topsrcdir)/$(MOZ_BRANDING_
 ICON_PATH_HDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon64.png
 DEFINES += -DMOZ_ANDROID_SHARED_ID="$(ANDROID_PACKAGE_NAME).sharedID"
 endif
 
 RES_LAYOUT = \
   $(SYNC_RES_LAYOUT) \
   res/layout/autocomplete_list_item.xml \
   res/layout/awesomebar.xml \
+  res/layout/awesomebar_folder_row.xml \
+  res/layout/awesomebar_folder_header_row.xml \
   res/layout/awesomebar_header_row.xml \
   res/layout/awesomebar_row.xml \
   res/layout/awesomebar_search.xml \
   res/layout/awesomebar_tab_indicator.xml \
   res/layout/awesomebar_tabs.xml \
   res/layout/browser_toolbar.xml \
   res/layout/doorhangerpopup.xml \
   res/layout/doorhanger.xml \
@@ -284,16 +286,17 @@ RES_DRAWABLE_NODPI = \
   res/drawable-nodpi/abouthome_bg.png \
   res/drawable-nodpi/abouthome_topsites_bg.png \
   res/drawable-nodpi/tabs_tray_bg.png \
   res/drawable-nodpi/tabs_tray_pressed_bg.png \
   $(NULL)
 
 RES_DRAWABLE_BASE = \
   res/drawable/favicon.png \
+  res/drawable/folder.png \
   res/drawable/abouthome_icon.png \
   res/drawable/abouthome_logo.png \
   res/drawable/abouthome_separator.9.png \
   res/drawable/abouthome_sync_logo.png \
   res/drawable/abouthome_sync_bg.9.png \
   res/drawable/abouthome_sync_pressed_bg.9.png \
   res/drawable/awesomebar_tab.9.png \
   res/drawable/awesomebar_tab_pressed.9.png \
@@ -330,16 +333,17 @@ RES_DRAWABLE_BASE = \
   $(NULL)
 
 RES_DRAWABLE_LDPI = \
   $(addprefix res/drawable-ldpi/,$(notdir $(SYNC_RES_DRAWABLE_LDPI))) \
   $(NULL)
 
 RES_DRAWABLE_HDPI = \
   res/drawable-hdpi/favicon.png \
+  res/drawable-hdpi/folder.png \
   res/drawable-hdpi/home_bg.png \
   res/drawable-hdpi/home_star.png \
   res/drawable-hdpi/abouthome_icon.png \
   res/drawable-hdpi/abouthome_logo.png \
   res/drawable-hdpi/abouthome_separator.9.png \
   res/drawable-hdpi/abouthome_sync_logo.png \
   res/drawable-hdpi/abouthome_sync_bg.9.png \
   res/drawable-hdpi/abouthome_sync_pressed_bg.9.png \
@@ -394,16 +398,17 @@ RES_DRAWABLE_HDPI_V11 = \
   res/drawable-hdpi-v11/ic_menu_reload.png \
   res/drawable-hdpi-v11/ic_menu_save_as_pdf.png \
   res/drawable-hdpi-v11/ic_menu_share.png \
   res/drawable-hdpi-v11/ic_menu_forward.png \
   $(NULL)
 
 RES_DRAWABLE_XHDPI_V11 = \
   res/drawable-xhdpi-v11/favicon.png \
+  res/drawable-xhdpi-v11/folder.png \
   res/drawable-xhdpi-v11/abouthome_icon.png \
   res/drawable-xhdpi-v11/abouthome_logo.png \
   res/drawable-xhdpi-v11/abouthome_separator.9.png \
   res/drawable-xhdpi-v11/abouthome_sync_logo.png \
   res/drawable-xhdpi-v11/abouthome_sync_bg.9.png \
   res/drawable-xhdpi-v11/abouthome_sync_pressed_bg.9.png \
   res/drawable-xhdpi-v11/awesomebar_tab.9.png \
   res/drawable-xhdpi-v11/awesomebar_tab_pressed.9.png \
--- a/mobile/android/base/db/AndroidBrowserDB.java
+++ b/mobile/android/base/db/AndroidBrowserDB.java
@@ -183,22 +183,17 @@ public class AndroidBrowserDB implements
         // Valid for Android versions up to 4.0.
         return 250;
     }
 
     public void clearHistory(ContentResolver cr) {
         Browser.clearHistory(cr);
     }
 
-    public Cursor getMobileBookmarks(ContentResolver cr) {
-        Cursor c = cr.query(null, null, null, null, null);
-        return new AndroidDBCursor(c);
-    }
-
-    public Cursor getDesktopBookmarks(ContentResolver cr) {
+    public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
         Cursor c = cr.query(null, null, null, null, null);
         return new AndroidDBCursor(c);
     }
 
     public Cursor isBookmarkQueryPre11(ContentResolver cr, String uri) {
         return cr.query(Browser.BOOKMARKS_URI,
                         new String[] { BookmarkColumns.URL },
                         Browser.BookmarkColumns.URL + " = ? and " + Browser.BookmarkColumns.BOOKMARK + " = ?",
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -72,19 +72,17 @@ public class BrowserDB {
         public Cursor getAllVisitedHistory(ContentResolver cr);
 
         public Cursor getRecentHistory(ContentResolver cr, int limit);
 
         public int getMaxHistoryCount();
 
         public void clearHistory(ContentResolver cr);
 
-        public Cursor getMobileBookmarks(ContentResolver cr);
-
-        public Cursor getDesktopBookmarks(ContentResolver cr);
+        public Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
 
         public boolean isBookmark(ContentResolver cr, String uri);
 
         public String getUrlForKeyword(ContentResolver cr, String keyword);
 
         public void addBookmark(ContentResolver cr, String title, String uri);
 
         public void removeBookmark(ContentResolver cr, int id);
@@ -141,22 +139,18 @@ public class BrowserDB {
     public static int getMaxHistoryCount() {
         return sDb.getMaxHistoryCount();
     }
 
     public static void clearHistory(ContentResolver cr) {
         sDb.clearHistory(cr);
     }
 
-    public static Cursor getMobileBookmarks(ContentResolver cr) {
-        return sDb.getMobileBookmarks(cr);
-    }
-
-    public static Cursor getDesktopBookmarks(ContentResolver cr) {
-        return sDb.getDesktopBookmarks(cr);
+    public static Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
+        return sDb.getBookmarksInFolder(cr, folderId);
     }
 
     public static String getUrlForKeyword(ContentResolver cr, String keyword) {
         return sDb.getUrlForKeyword(cr, keyword);
     }
     
     public static boolean isBookmark(ContentResolver cr, String uri) {
         return sDb.isBookmark(cr, uri);
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -16,16 +16,17 @@
  *
  * The Initial Developer of the Original Code is Mozilla Foundation.
  * Portions created by the Initial Developer are Copyright (C) 2011
  * the Initial Developer. All Rights Reserved.
  *
  * Contributor(s):
  *   Lucas Rocha <lucasr@mozilla.com>
  *   Richard Newman <rnewman@mozilla.com>
+ *   Margaret Leibovic <margaret.leibovic@gmail.com>
  *
  * Alternatively, the contents of this file may be used under the terms of
  * either the GNU General Public License Version 2 or later (the "GPL"), or
  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  * in which case the provisions of the GPL or the LGPL are applicable instead
  * of those above. If you wish to allow use of your version of this file only
  * under the terms of either the GPL or the LGPL, and not to allow others to
  * use your version of this file under the terms of the MPL, indicate your
@@ -47,17 +48,16 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.CursorWrapper;
-import android.database.MatrixCursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
 import android.provider.Browser;
 import android.util.Log;
 
 public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
@@ -74,26 +74,36 @@ public class LocalBrowserDB implements B
     protected static void debug(String message) {
         if (logDebug) {
             Log.d(LOGTAG, message);
         }
     }
 
     private final String mProfile;
     private long mMobileFolderId;
+    private long mTagsFolderId;
 
     private final Uri mBookmarksUriWithProfile;
     private final Uri mParentsUriWithProfile;
     private final Uri mHistoryUriWithProfile;
     private final Uri mImagesUriWithProfile;
     private final Uri mDeletedHistoryUriWithProfile;
 
+    private static final String[] DEFAULT_BOOKMARK_COLUMNS =
+            new String[] { Bookmarks._ID,
+                           Bookmarks.URL,
+                           Bookmarks.TITLE,
+                           Bookmarks.IS_FOLDER,
+                           Bookmarks.PARENT,
+                           Bookmarks.FAVICON }; 
+
     public LocalBrowserDB(String profile) {
         mProfile = profile;
         mMobileFolderId = -1;
+        mTagsFolderId = -1;
 
         mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI);
         mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI);
         mHistoryUriWithProfile = appendProfile(History.CONTENT_URI);
         mImagesUriWithProfile = appendProfile(Images.CONTENT_URI);
 
         mDeletedHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon().
             appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1").build();
@@ -291,36 +301,28 @@ public class LocalBrowserDB implements B
     public int getMaxHistoryCount() {
         return MAX_HISTORY_COUNT;
     }
 
     public void clearHistory(ContentResolver cr) {
         cr.delete(mHistoryUriWithProfile, null, null);
     }
 
-    public Cursor getMobileBookmarks(ContentResolver cr) {
-        return getBookmarks(cr, true);
-    }
-
-    public Cursor getDesktopBookmarks(ContentResolver cr) {
-        return getBookmarks(cr, false);
-    }
-
-    private Cursor getBookmarks(ContentResolver cr, boolean mobileBookmarks) {
-        String parentSelection = mobileBookmarks ? " = ?" : " != ?";
-        long mobileFolderId = getMobileBookmarksFolderId(cr);
+    // This method filters out the root folder and the tags folder, since we
+    // don't want to see those in the UI
+    public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
         Cursor c = cr.query(mBookmarksUriWithProfile,
-                            new String[] { Bookmarks._ID,
-                                           Bookmarks.URL,
-                                           Bookmarks.TITLE,
-                                           Bookmarks.FAVICON },
-                            Bookmarks.IS_FOLDER + " = 0 AND " +
-                            Bookmarks.PARENT + parentSelection,
-                            new String[] { String.valueOf(mobileFolderId) },
-                            Bookmarks.DATE_MODIFIED + " DESC");
+                            DEFAULT_BOOKMARK_COLUMNS,
+                            Bookmarks.PARENT + " = ? AND " +
+                            Bookmarks._ID + " <> ? AND " +
+                            Bookmarks._ID + " <> ?",
+                            new String[] { String.valueOf(folderId),
+                                           String.valueOf(Bookmarks.FIXED_ROOT_ID),
+                                           String.valueOf(getTagsBookmarksFolderId(cr))},
+                            null);
 
         return new LocalDBCursor(c);
     }
 
     public boolean isBookmark(ContentResolver cr, String uri) {
         Cursor cursor = cr.query(mBookmarksUriWithProfile,
                                  new String[] { Bookmarks._ID },
                                  Bookmarks.URL + " = ?",
@@ -350,33 +352,47 @@ public class LocalBrowserDB implements B
 
         return url;
     }
 
     private long getMobileBookmarksFolderId(ContentResolver cr) {
         if (mMobileFolderId >= 0)
             return mMobileFolderId;
 
+        mMobileFolderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
+        return mMobileFolderId;
+    }
+
+    private long getTagsBookmarksFolderId(ContentResolver cr) {
+        if (mTagsFolderId >= 0)
+            return mTagsFolderId;
+
+        mTagsFolderId = getFolderIdFromGuid(cr, Bookmarks.TAGS_FOLDER_GUID);
+        return mTagsFolderId;
+    }
+
+    private long getFolderIdFromGuid(ContentResolver cr, String guid) {
+        long folderId = -1;
         Cursor c = null;
 
         try {
             c = cr.query(mBookmarksUriWithProfile,
                          new String[] { Bookmarks._ID },
                          Bookmarks.GUID + " = ?",
-                         new String[] { Bookmarks.MOBILE_FOLDER_GUID },
+                         new String[] { guid },
                          null);
 
             if (c.moveToFirst())
-                mMobileFolderId = c.getLong(c.getColumnIndexOrThrow(Bookmarks._ID));
+                folderId = c.getLong(c.getColumnIndexOrThrow(Bookmarks._ID));
         } finally {
             if (c != null)
                 c.close();
         }
 
-        return mMobileFolderId;
+        return folderId;
     }
 
     /**
      * Find parents of records that match the provided criteria, and bump their
      * modified timestamp.
      */
     protected void bumpParents(ContentResolver cr, String param, String value) {
         ContentValues values = new ContentValues();
new file mode 100644
index 0000000000000000000000000000000000000000..3b1b63032fbd7e662ca89e8afe7e27fce3669abb
GIT binary patch
literal 1610
zc$}S7drT8|9Pf(MSGCQ6r5&!^4s}Lp@2>Q1YwhcxN|jKIsGGFCUTH&nSK9*$W<cG*
zVTdd!!v~2*ShlFyoR64IBSmFCf)hy;btL#&++Yq#=0t7nu8z7#jDPHx-2Hyv?|nYs
z@Avn8TvOqSyje5ep21)+W|{MiMf46Qo@p%ly_)%CGrjRCQwe1yT~w7lfHQPX(t!hJ
zpS=t(!tKuL4Hs}VgOTF)6qisXmI4(<`b72wMilb-8HuIVhWvJ{9H#&WUgjY*;Mj?7
z5b!uP;A*J_w)hRW+ml}tz^yeaim{q<Oz8x*O8|99MH}$plpP59yhKnH(tt1Qs_1h<
z4S~Q52vx2D{|c(aQV1Bx01ik+un>b~2q2S)5DBc5D02WYjKB~ghG4l6fmKqu3Pym*
z1=8LEPM504h)(*VB@O7ND8CAVs;a6)Rk<QEPzE7Nr4oY0kXS6F5yD_KLD@q>BAA`D
zV8nx1z~iSpBmpF3yMwHtG$5F~g3oWUyfjP%Clf`d3<}x(5F&yhpASl+gH#djdObQ=
zT<yo9B0NY|1TeZDuIzufdObA(q@z)-40!0G*u6#)tMK6jWj1O+`iscvandPLzz8Cj
zz;ZDxQy_>!j-YyRu3nBxP=ib=P1<-BS1FU4jC#35g3?69M01s}0g)PY3b{-#g^?uJ
zOav)Af#FH6=WlNAYq=^z0Jl?QpqM1R$qp!VlN1?rlYYQpm5LC+V#hp0LX#-aWUP3=
zQ;9p#0O<o>WLV{SMFglpj{fWB3WH1s%VZK%sWiwCIXKC6{-3NtbTH6=+9j!?doXc)
z+5EKda(r-to|ynW9KRLBjxiX_klCm!4&8J_<8keoLRNF9fcGep|9I*9YegMr&Yim)
z={wDIt^Cz(o7=lLx`pM>k3M~}?e^#(WE@>5DHodrws3C!?7HZ~Q@gs37RVQ6?oRF3
zIl6eiKOP&t-(4FU+BkaiComO1HW+^%>s-!;k@{9H_v<yP(t>3u@y_DKo3qlgxA6)(
zO8WZF&b@G<hu?W{|F^cD<|egq^z)&Vh`U46aOLdjYwc}qmfglG%ti>-bFg6f2euI>
zW2c2{9SO&|){ygFMu*P3aOwPct17xpN8USv961a}r#?%OccmYyZ$2KZ987iDN?8lr
z^7rk#bLGv}0c&+Aw5R`MI@8hAw`q$tGLZM#E>=AH<nGY@maK9<pU)AMDn>Kgnqw`O
zQog&<d$qk$zmt=5V|{F5{KMY1nPmr7W@)3TtxbI1bzR$y!bs$L`rUi?E?TelvsMYb
zFb8I5a3V02)vaA{pl8wHIJKYpa>s_$@b+n3ZR*Bb!~I@Pc<6`o%;kDR20M+z{zhGY
zJ6vbbPLE}dnB5W<OTmd|3emi6+<AhDI4MbMd8>2#wX+5dtFaa%jtY3f2DFCFe&k5M
zo{c%3bHrQG+5SUQYvWIQBNxAyIn@uJJ%)-o(TVX`tgbP>DJSRPN2>Wt-;LkDOg8S(
z)}GA!BeG{~_!Kc^V7iuH%WmRLJb&<`>(djuS_iB8&ZPtG#~Vz&$~7e=U1205{RF%9
zpk-`$uvER#adWu4ZauSMNqAu9g7MMWJ<OkHaN{4(3snLv%gA(kW}>t7jAgR1`iYG5
u<G>qUWhLDH#g4Q&ow<)X1$)_yDLlrytquBLdeOCszq{GA!uXZmw&_oKBvzFG
new file mode 100644
index 0000000000000000000000000000000000000000..a54a0f388922ccb43db4d03839bdb785301ec086
GIT binary patch
literal 1816
zc$}S8c~BEq7~e#tNC5>81fi}Q5ihd4AtY={xDq0<1Tlc%(UFjBBto(wStLXP6x4#&
z3oVxwDp(PmL0eEdBbOsninjGWIn)7+qX?~7ECORUme^w)|LB|9_ulv3@ArM*`;OWC
z$cU8=c5Ze6062)lgi>k^vV68S)Lf%V>7f=^GBl2iB9vsBOos!(3L*gq#cJ7RT#CyS
z=_wt!KLAWgR7J;;aguPJoKQ1m77Wv%)&iE{A863Z<ViRQCg7V@8a_01t_1>B3O*F;
zD?ub$0iLJ|GwSdtV??yvm?Xy(P+$P)Z{Sf1)Ho>v4eDf#o@d}gZ}jr0z2z8&z&8*w
zi4XndRGcId6c9Qb^kpL6a)g6|95xeWBN!W70kRMjfl(HWaJ^B4=gZ|GC^$JF%9>80
z<Vl61Nn4b}hZ0Fr%Y)&xv@~X#50lVshEWW|V1xy;Sl$%ETc55WWd?7Jo?+D>#PxEW
zN=vE;4QP>N2}CN%hal?{)LMz;tzwOSGEh{=V1rBxqf7)=t6?izPfBs+`_cO7bS(}`
zaXpc$lT-OnGX7!e{ZtE(@`krsr=pS~OBNFHR5h+4#X>$r-7pm@1r;Jc1Vy=Qgv&xW
zekkh4MMWVjpAfEyEfR2ieXTm)#l<)r1VMu-Bt=9+DK3TxP+wuNAD0v2i=bAnSfeLp
z8aZy|s{ZDNzL(1r=x`ZH=%NWC*;)aSi3CaL6A3LSi1KBkphPBDX)Gs}1Wo#i>r`8D
zg-A!J!8Z}+sovp%NWc~S>*am|PB6mZutgXq;GkS+lB@VXQG+RG;Qy41^@yrL%l2*Y
zQ^MQ!!8KIR=&0s+T-z?AdQK!321gt24IIu2n<bmqe!p|DvGdXKge^zWUz9l$jA`Qi
zp@H`GwhV}IB8yd#2cj2k$Lj*0%<y<2n11b*m%ZJT9ZR<7ED6tR4RgMfFoBF$sx|7g
zbH6v5j}OPrY^Z)_ve~40+4IaalF?&Mzi;00G1G02b77!6JqWM`XdqBXbH_4`5tZG8
zU?UiQq+z4x(HHYDee=31_k3aA<+0t1W*Ijh?z}QIde@<1Nx{opV%ps^XT`I>nW8K!
zI!`<4#HgvT-}&XQyRRO2_~G-?g5Tni?~|cc;p6y4C%R3Z?J?^|x|DU?;l|T@8DFK2
z@Lw;xx9?`cNqDLE{Kwn6qLS-dTv_(uQLZDsX)tjD6&EZElfLdhW++S3bVaAv)^&TY
zaG}qnT@i<Ss0TN$Tsx9kT3Ib!yEVe$&d;CJCxwY<ZJ18!)3H1*rg-71{*DD>suDwH
zYX8WmbF>>BU6CVn5AFs@XU!Hz!Q4FY{3FvAxfI>#X}+4^<e6Vg8<#>4de_v~8HXOt
z8J*@ex00z<nBDKy4LvZ4zw?I*aB(cn<l4CH>H4eHJ9js^x36>QsC}?vYwD<}XKhT(
zIj`~Nq8WqT`N!@k$<J)BbPfmI3azyZ-Eh3gY408cb)J`f$n82?9X|c@u@++h_l5s}
zTWQTy`TDHq$=8$SuG$1V^!UgaJ9ez$93k@j@nRWpt!qu+A^7}l+WsO8IM^TH{o+M^
zxlPbKI)JVA+SdNl?0CW}xnf0e_-E-27pMdxGlDMIQmM?w0XGp~P3qKaK<l&JDI18J
z8ide`KcJ>NHbGj)));_6AxlN&v-*m)yp+>bfDl?{+J9g=aN{-xgtX*l9=ustGPova
z<$?@{w#$+YC%0ZVTnRZEnFS)jvtvBhvsXz+PxndlUI|T=0Xe5mCC+MUYgcBSGGD5C
z8gci`!rAtI*Zk?^rs~T^ySByK%PScRL?G*lB36|PDh>&j^&Kzi?cL>^Ih<m6{A3?d
zhCcC8o6ioMxJ4L7>KkS{E_Yow(VXkg%U<U)@O4Jy;u15y>-D&xDl;%|Z!_HUN5j&<
p?!w&k<@^klo#mme?Ce0gJph}3DF60(Z@lGSB@T@c)`rCA{0Y5hrrH1i
new file mode 100644
index 0000000000000000000000000000000000000000..43b7a0f2b4617c5ff29c8c09b9c98489fbc27044
GIT binary patch
literal 1399
zc$}S7Z%i9y7(Xc1$zY;nggIHx1B{FG{`AkaJt>s7lr>uc3k#Smlk45v_6FBG@2+cU
zoKVdSWNv)W=@PRzb;J)E<~Ebf7%h{rgb#}nomn;$(8zFrGt7OEl~S)riJFBEzRBJD
zKF|Gr&-1*`pBrp#ZYnN(q7Z_hV$Uv@Pu=0{vq`JI<#_y_x@}V$JC!yephRg2Kn<+m
z2e5~ygTM!9w)dq8Pzynt5ZB(Rbb9wtjKHIG7K6rk5z4OG`j|*F-9Ul;AjpMn$nRey
z5tw6bNQcRbdqpP*al7IYXp1+uGx2W5$|Ci3aBYlI4e&sr;TYc&mZ_KxS+z^4=d2n-
z;8lpyZA1PJs?*yFI|T{ACKT5(I7z^y5haYc)o87O^*DiJgdW4qIs&ImW(p_ZwS}nO
zBsM_#T<$ers$@e#iXu`N7L7*Hr~wtEAVyfNRt(o;dc96X=;Yq8LdSGrxiV+L1!P9z
zM1>Q=a8{=MLPW74NbU-}==H7}4$EtaQd5S-Xb~e&9OHQ`hn5u|2yBd&+j~WT`G71$
zBu1@Apz<HCHl}8QYBW@f#HmH0dt3q&;XzpOxNL~}1!Xx_O_2pB2(uA4>v7UT5Ee7x
zcIXWbv)kx)k|tBm#)G(4($wg3m~o?9B@&G)*NQs{ldHjECLJc6$Z<VkS)s!W$Z@%K
zF6rKoOF1P#D}vN62tBzDXblO9Acq7IcD9*N0`}4j7tU(31zL+0NZdidx+Q^!S2Ij;
z4~W3+G`s(GbBmK~z)8~RwpyJeVMf-t?ElFcrUrxkr(JR?wFk4u_2yTF_3;5=b!H@W
zIGoS?QUyVI!yZ>dd+dgPIg_a$v*$nkdY)Eu2fA|cvwiq$?+gtk4j(>F`<pbCb;V0t
zi>EEA`!j~IGcP6w&aso9Oju?*?FEL?(y=4KmWryX;g2BQw^xGY_|=zFi|Lzl{PvF7
zi?REC@7EUg4~i>q3_3pWKc23ssks5y)fE-(K%1LKVusB(_nrEteA`naUk@!pWvwWF
z{+xjYjN#8F$FY*KH$}*2SSl!(dj2h~Jup0T{5OVSzN_5(M=C08@5`)QOCDJKv8C!j
zdT0SUy72Sd{JlL($-c~2%eTi;b6bxD1Z}nBj*6Q!%@YR0{N2rkS0C0#E`2^-f3gB9
zj?-|p_TXqT@$9h|;4S6-CH9>IE9um8UDb>5BZ;JD$3q`(DUXa!B_gAl#G{FfW}!c$
z*<X?dT`YV0<MB^dHeJt~N*-dmW~WcTTWNngv%FaG6V!En?%I3x$?;#G%s(@Fv=lCz
zxpk}e^29i=W#!3<yBCE0Uxr>u&y62yJ+V9g?Dx>{rP)&@{Snl1`}*FL<}zkK@z`#|
jN%X8em=Z4xK>0<GzGHmlhv3e^?61(%*z6i{biMi)095uE
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/awesomebar_folder_header_row.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:orientation="vertical"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content">
+
+    <TextView android:id="@+id/title"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:textAppearance="?android:attr/textAppearanceMedium"
+              android:textColor="?android:attr/textColorPrimary"
+              android:singleLine="true"
+              android:padding="6dip"
+              android:ellipsize="middle"/>
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/awesomebar_folder_row.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:padding="6dip">
+
+    <ImageView android:id="@+id/favicon"
+               android:src="@drawable/folder"
+               android:layout_width="32dip"
+               android:layout_height="32dip"
+               android:layout_marginRight="10dip"
+               android:layout_centerVertical="true"
+               android:minWidth="32dip"
+               android:minHeight="32dip"
+               android:scaleType="fitCenter"/>
+
+    <TextView android:id="@+id/title"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:layout_centerVertical="true"
+              android:textAppearance="?android:attr/textAppearanceMedium"
+              android:textColor="?android:attr/textColorPrimary"
+              android:layout_toRightOf="@id/favicon"
+              android:singleLine="true"
+              android:ellipsize="middle"/>
+
+</RelativeLayout>
--- a/mobile/android/base/resources/layout/awesomebar_tabs.xml
+++ b/mobile/android/base/resources/layout/awesomebar_tabs.xml
@@ -13,19 +13,18 @@
 
         <FrameLayout android:id="@android:id/tabcontent"
                      android:layout_width="fill_parent"
                      android:layout_height="fill_parent">
 
             <ListView android:id="@+id/all_pages_list"
                       style="@style/AwesomeBarList"/>
 
-            <ExpandableListView android:id="@+id/bookmarks_list"
-                                style="@style/AwesomeBarList"
-                                android:groupIndicator="@android:color/transparent"/>
+            <ListView android:id="@+id/bookmarks_list"
+                      style="@style/AwesomeBarList"/>
 
             <ExpandableListView android:id="@+id/history_list"
                                 style="@style/AwesomeBarList"
                                 android:groupIndicator="@android:color/transparent"/>
 
         </FrameLayout>
 
     </LinearLayout>