Bug 862796: Add a bookmarks panel to about:home. [r=bnicholson]
authorSriram Ramasubramanian <sriram@mozilla.com>
Thu, 30 May 2013 17:53:13 -0700
changeset 151290 e4c63bf2c0e869ed5e99f02a0f1c6f1c65a754ef
parent 151289 5ac643b8155bad79be1dc80d7b845baab39681be
child 151291 af7d943f4976603a3f4bce75c6f98c2761b7bc49
push idunknown
push userunknown
push dateunknown
reviewersbnicholson
bugs862796
milestone24.0a1
Bug 862796: Add a bookmarks panel to about:home. [r=bnicholson]
mobile/android/base/GeckoViewsFactory.java
mobile/android/base/Makefile.in
mobile/android/base/home/BookmarkFolderView.java
mobile/android/base/home/BookmarksPage.java
mobile/android/base/home/HomePager.java
mobile/android/base/home/TwoLinePageRow.java
mobile/android/base/resources/drawable-hdpi/bookmark_folder_closed.png
mobile/android/base/resources/drawable-hdpi/bookmark_folder_opened.png
mobile/android/base/resources/drawable-mdpi/bookmark_folder_closed.png
mobile/android/base/resources/drawable-mdpi/bookmark_folder_opened.png
mobile/android/base/resources/drawable-xhdpi/bookmark_folder_closed.png
mobile/android/base/resources/drawable-xhdpi/bookmark_folder_opened.png
mobile/android/base/resources/drawable/bookmark_folder.xml
mobile/android/base/resources/layout/bookmark_folder_row.xml
mobile/android/base/resources/layout/bookmark_item_row.xml
mobile/android/base/resources/layout/two_line_page_row.xml
mobile/android/base/resources/values-v16/styles.xml
mobile/android/base/resources/values/attrs.xml
mobile/android/base/resources/values/colors.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/resources/values/styles.xml
--- a/mobile/android/base/GeckoViewsFactory.java
+++ b/mobile/android/base/GeckoViewsFactory.java
@@ -1,15 +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/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.home.BookmarkFolderView;
+import org.mozilla.gecko.home.TwoLinePageRow;
 import org.mozilla.gecko.menu.MenuItemDefault;
 import org.mozilla.gecko.widget.AboutHomeView;
 import org.mozilla.gecko.widget.AddonsSection;
 import org.mozilla.gecko.widget.FaviconView;
 import org.mozilla.gecko.widget.IconTabWidget;
 import org.mozilla.gecko.widget.LastTabsSection;
 import org.mozilla.gecko.widget.LinkTextView;
 import org.mozilla.gecko.widget.PromoBox;
@@ -82,16 +84,18 @@ public final class GeckoViewsFactory imp
             mFactoryMap.put("FrameLayout", GeckoFrameLayout.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("ImageButton", GeckoImageButton.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("ImageView", GeckoImageView.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("LinearLayout", GeckoLinearLayout.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("RelativeLayout", GeckoRelativeLayout.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("TextSwitcher", GeckoTextSwitcher.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("TextView", GeckoTextView.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("FaviconView", FaviconView.class.getConstructor(arg1Class, arg2Class));
+            mFactoryMap.put("home.BookmarkFolderView", BookmarkFolderView.class.getConstructor(arg1Class, arg2Class));
+            mFactoryMap.put("home.TwoLinePageRow", TwoLinePageRow.class.getConstructor(arg1Class, arg2Class));
         } catch (NoSuchMethodException nsme) {
             Log.e(LOGTAG, "Unable to initialize views factory", nsme);
         }
     }
 
     // Making this a singleton class.
     private static final GeckoViewsFactory INSTANCE = new GeckoViewsFactory();
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -209,18 +209,21 @@ FENNEC_JAVA_FILES = \
   gfx/SubdocumentScrollHelper.java \
   gfx/TextLayer.java \
   gfx/TextureGenerator.java \
   gfx/TextureReaper.java \
   gfx/TileLayer.java \
   gfx/TouchEventHandler.java \
   gfx/ViewTransform.java \
   gfx/VirtualLayer.java \
+  home/BookmarksPage.java \
+  home/BookmarkFolderView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
+  home/TwoLinePageRow.java \
   menu/GeckoMenu.java \
   menu/GeckoMenuInflater.java \
   menu/GeckoMenuItem.java \
   menu/GeckoSubMenu.java \
   menu/MenuItemActionBar.java \
   menu/MenuItemDefault.java \
   menu/MenuPanel.java \
   menu/MenuPopup.java \
@@ -425,16 +428,18 @@ RES_LAYOUT = \
   res/layout/awesomebar_row.xml \
   res/layout/awesomebar_search.xml \
   res/layout/awesomebar_suggestion_row.xml \
   res/layout/awesomebar_suggestion_item.xml \
   res/layout/awesomebar_suggestion_prompt.xml \
   res/layout/awesomebar_tab_indicator.xml \
   res/layout/awesomebar_tabs.xml \
   res/layout/bookmark_edit.xml \
+  res/layout/bookmark_folder_row.xml \
+  res/layout/bookmark_item_row.xml \
   res/layout/browser_toolbar.xml \
   res/layout/datetime_picker.xml \
   res/layout/doorhangerpopup.xml \
   res/layout/doorhanger.xml \
   res/layout/doorhanger_button.xml \
   res/layout/find_in_page_content.xml \
   res/layout/font_size_preference.xml \
   res/layout/gecko_app.xml \
@@ -455,16 +460,17 @@ RES_LAYOUT = \
   res/layout/remote_tabs_group.xml \
   res/layout/tabs_panel.xml \
   res/layout/tabs_counter.xml \
   res/layout/tabs_panel_header.xml \
   res/layout/tabs_panel_indicator.xml \
   res/layout/tabs_item_cell.xml \
   res/layout/tabs_item_row.xml \
   res/layout/text_selection_handles.xml \
+  res/layout/two_line_page_row.xml \
   res/layout/list_item_header.xml \
   res/layout/select_dialog_list.xml \
   res/layout/select_dialog_multichoice.xml \
   res/layout/select_dialog_singlechoice.xml \
   res/layout/simple_dropdown_item_1line.xml \
   res/layout/abouthome_addon_row.xml \
   res/layout/abouthome_last_tabs_row.xml \
   res/layout/abouthome_section.xml \
@@ -539,16 +545,20 @@ RES_VALUES_XLARGE_V11 = \
   res/values-xlarge-v11/integers.xml \
   res/values-xlarge-v11/styles.xml \
   $(NULL)
 
 RES_VALUES_V14 = \
   res/values-v14/styles.xml \
   $(NULL)
 
+RES_VALUES_V16 = \
+  res/values-v16/styles.xml \
+  $(NULL)
+
 RES_XML = \
   res/xml/preferences_datareporting.xml \
   $(SYNC_RES_XML) \
   $(NULL)
 
 RES_XML_V11 = \
   res/xml-v11/preference_headers.xml \
   res/xml-v11/preferences_general.xml \
@@ -586,16 +596,18 @@ RES_DRAWABLE_MDPI = \
   res/drawable-mdpi/alert_app.png \
   res/drawable-mdpi/alert_download.png \
   res/drawable-mdpi/autocomplete_list_bg.9.png \
   res/drawable-mdpi/awesomebar_tab_center.9.png \
   res/drawable-mdpi/awesomebar_tab_left.9.png \
   res/drawable-mdpi/awesomebar_tab_right.9.png \
   res/drawable-mdpi/awesomebar_sep_left.9.png \
   res/drawable-mdpi/awesomebar_sep_right.9.png \
+  res/drawable-mdpi/bookmark_folder_closed.png \
+  res/drawable-mdpi/bookmark_folder_opened.png \
   res/drawable-mdpi/desktop_notification.png \
   res/drawable-mdpi/ic_addons_empty.png \
   res/drawable-mdpi/ic_menu_addons_filler.png \
   res/drawable-mdpi/ic_menu_bookmark_add.png \
   res/drawable-mdpi/ic_menu_bookmark_remove.png \
   res/drawable-mdpi/ic_menu_character_encoding.png \
   res/drawable-mdpi/ic_menu_close_all_tabs.png \
   res/drawable-mdpi/ic_menu_forward.png \
@@ -700,16 +712,18 @@ RES_DRAWABLE_HDPI = \
   res/drawable-hdpi/alert_addon.png \
   res/drawable-hdpi/alert_app.png \
   res/drawable-hdpi/alert_download.png \
   res/drawable-hdpi/awesomebar_tab_center.9.png \
   res/drawable-hdpi/awesomebar_tab_left.9.png \
   res/drawable-hdpi/awesomebar_tab_right.9.png \
   res/drawable-hdpi/awesomebar_sep_left.9.png \
   res/drawable-hdpi/awesomebar_sep_right.9.png \
+  res/drawable-hdpi/bookmark_folder_closed.png \
+  res/drawable-hdpi/bookmark_folder_opened.png \
   res/drawable-hdpi/ic_addons_empty.png \
   res/drawable-hdpi/ic_menu_addons_filler.png \
   res/drawable-hdpi/ic_menu_bookmark_add.png \
   res/drawable-hdpi/ic_menu_bookmark_remove.png \
   res/drawable-hdpi/ic_menu_character_encoding.png \
   res/drawable-hdpi/ic_menu_close_all_tabs.png \
   res/drawable-hdpi/ic_menu_forward.png \
   res/drawable-hdpi/ic_menu_new_private_tab.png \
@@ -794,16 +808,18 @@ RES_DRAWABLE_XHDPI = \
   res/drawable-xhdpi/alert_addon.png \
   res/drawable-xhdpi/alert_app.png \
   res/drawable-xhdpi/alert_download.png \
   res/drawable-xhdpi/awesomebar_tab_center.9.png \
   res/drawable-xhdpi/awesomebar_tab_left.9.png \
   res/drawable-xhdpi/awesomebar_tab_right.9.png \
   res/drawable-xhdpi/awesomebar_sep_left.9.png \
   res/drawable-xhdpi/awesomebar_sep_right.9.png \
+  res/drawable-xhdpi/bookmark_folder_closed.png \
+  res/drawable-xhdpi/bookmark_folder_opened.png \
   res/drawable-xhdpi/ic_addons_empty.png \
   res/drawable-xhdpi/ic_menu_addons_filler.png \
   res/drawable-xhdpi/ic_menu_bookmark_add.png \
   res/drawable-xhdpi/ic_menu_bookmark_remove.png \
   res/drawable-xhdpi/ic_menu_close_all_tabs.png \
   res/drawable-xhdpi/ic_menu_character_encoding.png \
   res/drawable-xhdpi/ic_menu_forward.png \
   res/drawable-xhdpi/ic_menu_new_private_tab.png \
@@ -1035,16 +1051,17 @@ MOZ_ANDROID_DRAWABLES += \
   mobile/android/base/resources/drawable/url_bar_entry.xml                      \
   mobile/android/base/resources/drawable/url_bar_nav_button.xml                 \
   mobile/android/base/resources/drawable/url_bar_right_edge.xml                 \
   mobile/android/base/resources/drawable/awesomebar_listview_divider.xml        \
   mobile/android/base/resources/drawable/awesomebar_header_row.xml              \
   mobile/android/base/resources/drawable/awesomebar_tab_indicator.xml           \
   mobile/android/base/resources/drawable/awesomebar_tab_selected.xml            \
   mobile/android/base/resources/drawable/awesomebar_tab_unselected.xml          \
+  mobile/android/base/resources/drawable/bookmark_folder.xml                    \
   mobile/android/base/resources/drawable/favicon_bg.xml                         \
   mobile/android/base/resources/drawable/handle_end_level.xml                   \
   mobile/android/base/resources/drawable/handle_start_level.xml                 \
   mobile/android/base/resources/drawable/ic_menu_back.xml                       \
   mobile/android/base/resources/drawable/ic_menu_desktop_mode_off.xml           \
   mobile/android/base/resources/drawable/ic_menu_desktop_mode_on.xml            \
   mobile/android/base/resources/drawable/ic_menu_quit.xml                       \
   mobile/android/base/resources/drawable/menu_item_state.xml                    \
@@ -1060,29 +1077,30 @@ MOZ_ANDROID_DRAWABLES += \
   mobile/android/base/resources/drawable/tab_thumbnail.xml                      \
   mobile/android/base/resources/drawable/tabs_panel_indicator.xml               \
   mobile/android/base/resources/drawable/textbox_bg.xml                         \
   mobile/android/base/resources/drawable/webapp_titlebar_bg.xml                 \
   $(NULL)
 
 MOZ_BRANDING_DRAWABLE_MDPI = $(shell if test -e $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/android-resources.mn; then cat $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/android-resources.mn | tr '\n' ' ';  fi)
 
-RESOURCES=$(RES_LAYOUT) $(RES_LAYOUT_LARGE_LAND_V11) $(RES_LAYOUT_LARGE_V11) $(RES_LAYOUT_XLARGE_V11) $(RES_LAYOUT_XLARGE_LAND_V11) $(RES_VALUES) $(RES_VALUES_LAND) $(RES_VALUES_V11) $(RES_VALUES_LARGE_V11) $(RES_VALUES_LARGE_LAND_V11) $(RES_VALUES_XLARGE_V11) $(RES_VALUES_LAND_V14) $(RES_VALUES_V14) $(RES_XML) $(RES_XML_V11) $(RES_ANIM) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_HDPI) $(RES_DRAWABLE_XHDPI) $(RES_DRAWABLE_MDPI_V11) $(RES_DRAWABLE_HDPI_V11) $(RES_DRAWABLE_XHDPI_V11) $(RES_DRAWABLE_LARGE_MDPI_V11) $(RES_DRAWABLE_LARGE_HDPI_V11) $(RES_DRAWABLE_LARGE_XHDPI_V11) $(RES_DRAWABLE_XLARGE_MDPI_V11) $(RES_DRAWABLE_XLARGE_HDPI_V11) $(RES_DRAWABLE_XLARGE_XHDPI_V11) $(RES_COLOR) $(RES_MENU)
+RESOURCES=$(RES_LAYOUT) $(RES_LAYOUT_LARGE_LAND_V11) $(RES_LAYOUT_LARGE_V11) $(RES_LAYOUT_XLARGE_V11) $(RES_LAYOUT_XLARGE_LAND_V11) $(RES_VALUES) $(RES_VALUES_LAND) $(RES_VALUES_V11) $(RES_VALUES_LARGE_V11) $(RES_VALUES_LARGE_LAND_V11) $(RES_VALUES_XLARGE_V11) $(RES_VALUES_LAND_V14) $(RES_VALUES_V14) $(RES_VALUES_V16) $(RES_XML) $(RES_XML_V11) $(RES_ANIM) $(RES_DRAWABLE_MDPI) $(RES_DRAWABLE_LDPI) $(RES_DRAWABLE_HDPI) $(RES_DRAWABLE_XHDPI) $(RES_DRAWABLE_MDPI_V11) $(RES_DRAWABLE_HDPI_V11) $(RES_DRAWABLE_XHDPI_V11) $(RES_DRAWABLE_LARGE_MDPI_V11) $(RES_DRAWABLE_LARGE_HDPI_V11) $(RES_DRAWABLE_LARGE_XHDPI_V11) $(RES_DRAWABLE_XLARGE_MDPI_V11) $(RES_DRAWABLE_XLARGE_HDPI_V11) $(RES_DRAWABLE_XLARGE_XHDPI_V11) $(RES_COLOR) $(RES_MENU)
 
 RES_DIRS= \
   res/layout                    \
   res/layout-large-v11          \
   res/layout-large-land-v11     \
   res/layout-xlarge-v11         \
   res/layout-xlarge-land-v11    \
   res/values                    \
   res/values-v11                \
   res/values-large-v11          \
   res/values-xlarge-v11         \
-  res/values-land-v14           \
+  res/values-v14                \
+  res/values-v16                \
   res/xml                       \
   res/xml-v11                   \
   res/anim                      \
   res/drawable-ldpi             \
   res/drawable-mdpi             \
   res/drawable-hdpi             \
   res/drawable-xhdpi            \
   res/drawable                  \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/BookmarkFolderView.java
@@ -0,0 +1,55 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+public class BookmarkFolderView extends TextView {
+    private static final int[] STATE_OPEN = { R.attr.state_open };
+
+    private boolean mIsOpen = false;
+
+    public BookmarkFolderView(Context context) {
+        super(context);
+    }
+
+    public BookmarkFolderView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public BookmarkFolderView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+        if (mIsOpen) {
+            mergeDrawableStates(drawableState, STATE_OPEN);
+        }
+
+        return drawableState;
+    }
+
+    public void open() {
+        if (!mIsOpen) {
+            mIsOpen = true;
+            refreshDrawableState();
+        }
+    }
+
+    public void close() {
+        if (mIsOpen) {
+            mIsOpen = false;
+            refreshDrawableState();
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/BookmarksPage.java
@@ -0,0 +1,443 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.db.BrowserContract.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserDB.URLColumns;
+import org.mozilla.gecko.util.GamepadUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.util.Pair;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.LinkedList;
+
+/**
+ * A page in about:home that displays a ListView of bookmarks.
+ */
+public class BookmarksPage extends Fragment {
+    public static final String LOGTAG = "GeckoBookmarksPage";
+
+    private int mFolderId = Bookmarks.FIXED_ROOT_ID;
+    private String mFolderTitle = "";
+
+    private BookmarksListAdapter mCursorAdapter = null;
+    private BookmarksQueryTask mQueryTask = null;
+
+    // The view shown by the fragment.
+    private ListView mList;
+
+    // Folder title for the currently shown list of bookmarks.
+    private BookmarkFolderView mFolderView;
+
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+
+        // Intialize the adapter.
+        mCursorAdapter = new BookmarksListAdapter(getActivity());
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+
+        // Can't use getters for adapter. It will create one if null.
+        if (mCursorAdapter != null) {
+            final Cursor cursor = mCursorAdapter.getCursor();
+            mCursorAdapter = null;
+
+            // Gingerbread locks the DB when closing a cursor, so do it in the background.
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                    if (cursor != null && !cursor.isClosed())
+                        cursor.close();
+                }
+            });
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        super.onCreateView(inflater, container, savedInstanceState);
+
+        // All list views are styled to look the same with a global activity theme.
+        // If the style of the list changes, inflate it from an XML.
+        mList = new ListView(container.getContext());
+        return mList;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        // Folder title view, is always in open state.
+        mFolderView = (BookmarkFolderView) LayoutInflater.from(getActivity()).inflate(R.layout.bookmark_folder_row, null);
+        mFolderView.open();
+
+        // We need to add the header before we set the adapter, hence make it null
+        refreshListWithCursor(null);
+
+        EventHandlers eventHandlers = new EventHandlers();
+        mList.setOnTouchListener(eventHandlers);
+        mList.setOnItemClickListener(eventHandlers);
+        mList.setOnCreateContextMenuListener(eventHandlers);
+        mList.setOnKeyListener(GamepadUtils.getListItemClickDispatcher());
+
+        mQueryTask = new BookmarksQueryTask();
+        mQueryTask.execute();
+    }
+
+    @Override
+    public void onDestroyView() {
+        mList = null;
+        mFolderView = null;
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        // Reattach the fragment, forcing a reinflation of its view.
+        // We use commitAllowingStateLoss() instead of commit() here to avoid
+        // an IllegalStateException. If the phone is rotated while Fennec
+        // is in the background, onConfigurationChanged() is fired.
+        // onConfigurationChanged() is called before onResume(), so
+        // using commit() would throw an IllegalStateException since it can't
+        // be used between the Activity's onSaveInstanceState() and
+        // onResume().
+        if (isVisible()) {
+            getFragmentManager().beginTransaction()
+                                .detach(this)
+                                .attach(this)
+                                .commitAllowingStateLoss();
+        }
+    }
+
+    private void refreshListWithCursor(Cursor cursor) {
+        // We need to add the header before we set the adapter, hence making it null.
+        mList.setAdapter(null);
+
+        // Add a header view based on the root folder.
+        if (mFolderId == Bookmarks.FIXED_ROOT_ID) {
+            if (mList.getHeaderViewsCount() == 1) {
+                mList.removeHeaderView(mFolderView);
+            }
+        } else {
+            if (mList.getHeaderViewsCount() == 0) {
+                mList.addHeaderView(mFolderView, null, true);
+            }
+
+            mFolderView.setText(mFolderTitle);
+        }
+
+        // This will update the cursorAdapter to use the new one if it already exists.
+        mCursorAdapter.changeCursor(cursor);
+        mList.setAdapter(mCursorAdapter);
+
+        // Reset the task.
+        mQueryTask = null;
+    }
+
+    /**
+     * Internal class to handle different event listeners on the ListView.
+     */
+    private class EventHandlers implements AdapterView.OnItemClickListener,
+                                           View.OnCreateContextMenuListener,
+                                           View.OnTouchListener {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            final ListView list = (ListView) parent;
+            final int headerCount = list.getHeaderViewsCount();
+
+            // If we tap on the header view, move back to parent folder.
+            if (headerCount == 1 && position == 0) {
+                mCursorAdapter.moveToParentFolder();
+                return;
+            }
+
+            Cursor cursor = mCursorAdapter.getCursor();
+            if (cursor == null) {
+                return;
+            }
+
+            // The header view takes up a spot in the list
+            if (headerCount == 1) {
+                position--;
+            }
+
+            cursor.moveToPosition(position);
+
+            int type = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE));
+            if (type == Bookmarks.TYPE_FOLDER) {
+                // If we're clicking on a folder, update adapter to move to that folder
+                int folderId = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
+                String folderTitle = mCursorAdapter.getFolderTitle(position);
+                mCursorAdapter.moveToChildFolder(folderId, folderTitle);
+             } else {
+                // Otherwise, just open the URL
+                String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
+                Tabs.getInstance().loadUrl(url);
+             }
+         }
+
+        @Override
+        public boolean onTouch(View view, MotionEvent event) {
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                // take focus away from awesome bar to hide the keyboard
+                view.requestFocus();
+            }
+            return false;
+        }
+
+        @Override
+        public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+            ListView list = (ListView) view;
+            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+            Object selectedItem = list.getItemAtPosition(info.position);
+            Cursor cursor = (Cursor) selectedItem;
+
+            // Don't show the context menu for folders
+            if (cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) {
+                return;
+            }
+
+            String keyword = null;
+            int keywordCol = cursor.getColumnIndex(URLColumns.KEYWORD);
+            if (keywordCol != -1) {
+                keyword = cursor.getString(keywordCol);
+            }
+
+            int id = cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID));
+            String url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
+            String title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
+            byte[] favicon = cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON));
+            int display = Combined.DISPLAY_NORMAL;
+
+            MenuInflater inflater = new MenuInflater(list.getContext());
+            inflater.inflate(R.menu.awesomebar_contextmenu, menu);
+
+            // Show Open Private Tab if we're in private mode, Open New Tab otherwise
+            boolean isPrivate = false;
+            Tab tab = Tabs.getInstance().getSelectedTab();
+            if (tab != null) {
+                isPrivate = tab.isPrivate();
+            }
+            menu.findItem(R.id.open_new_tab).setVisible(!isPrivate);
+            menu.findItem(R.id.open_private_tab).setVisible(isPrivate);
+
+            // Hide "Remove" item if there isn't a valid history ID
+            if (id < 0) {
+                menu.findItem(R.id.remove_history).setVisible(false);
+            }
+            menu.setHeaderTitle(title);
+ 
+            menu.findItem(R.id.remove_history).setVisible(false);
+            menu.findItem(R.id.open_in_reader).setVisible(false);
+            return;
+        }
+    }
+
+    /**
+     * Adapter to back the ListView with a list of bookmarks.
+     */
+    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;
+
+        // mParentStack holds folder id/title pairs that allow us to navigate
+        // back up the folder heirarchy.
+        private LinkedList<Pair<Integer, String>> mParentStack;
+
+        public BookmarksListAdapter(Context context) {
+            // Initializing with a null cursor.
+            super(context, -1, null, new String[] {}, new int[] {});
+
+            mParentStack = new LinkedList<Pair<Integer, String>>();
+
+            // Add the root folder to the stack
+            Pair<Integer, String> rootFolder = new Pair<Integer, String>(mFolderId, mFolderTitle);
+            mParentStack.addFirst(rootFolder);
+        }
+
+        // Refresh the current folder by executing a new task.
+        private void refreshCurrentFolder() {
+            // Cancel any pre-existing async refresh tasks
+            if (mQueryTask != null) {
+                mQueryTask.cancel(false);
+            }
+
+            Pair<Integer, String> folderPair = mParentStack.getFirst();
+            mFolderId = folderPair.first;
+            mFolderTitle = folderPair.second;
+
+            mQueryTask = new BookmarksQueryTask();
+            mQueryTask.execute(new Integer(mFolderId));
+        }
+
+        /**
+         * Moves to parent folder, if one exists.
+         */
+        public void moveToParentFolder() {
+            // If we're already at the root, we can't move to a parent folder
+            if (mParentStack.size() != 1) {
+                mParentStack.removeFirst();
+                refreshCurrentFolder();
+            }
+        }
+
+        /**
+         * Moves to child folder, given a folderId.
+         *
+         * @param folderId The id of the folder to show.
+         * @param folderTitle The title of the folder to show.
+         */
+        public void moveToChildFolder(int folderId, String folderTitle) {
+            Pair<Integer, String> folderPair = new Pair<Integer, String>(folderId, folderTitle);
+            mParentStack.addFirst(folderPair);
+            refreshCurrentFolder();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int getItemViewType(int position) {
+            Cursor c = getCursor();
+
+            if (c.moveToPosition(position) &&
+                c.getInt(c.getColumnIndexOrThrow(Bookmarks.TYPE)) == Bookmarks.TYPE_FOLDER) {
+                return VIEW_TYPE_FOLDER;
+            }
+
+            // Default to retuning normal item type
+            return VIEW_TYPE_ITEM;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int getViewTypeCount() {
+            return VIEW_TYPE_COUNT;
+        }
+
+        /**
+         * Get the title of the folder for a given position.
+         *
+         * @param position The position of the view.
+         * @return The title of the folder at the position.
+         */
+        public String getFolderTitle(int position) {
+            Cursor c = getCursor();
+            if (!c.moveToPosition(position)) {
+                return "";
+            }
+
+            String guid = c.getString(c.getColumnIndexOrThrow(Bookmarks.GUID));
+
+            // If we don't have a special GUID, just return the folder title from the DB.
+            if (guid == null || guid.length() == 12) {
+                return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE));
+            }
+
+            // Use localized strings for special folder names.
+            if (guid.equals(Bookmarks.FAKE_DESKTOP_FOLDER_GUID)) {
+                return getResources().getString(R.string.bookmarks_folder_desktop);
+            } else if (guid.equals(Bookmarks.MENU_FOLDER_GUID)) {
+                return getResources().getString(R.string.bookmarks_folder_menu);
+            } else if (guid.equals(Bookmarks.TOOLBAR_FOLDER_GUID)) {
+                return getResources().getString(R.string.bookmarks_folder_toolbar);
+            } else if (guid.equals(Bookmarks.UNFILED_FOLDER_GUID)) {
+                return getResources().getString(R.string.bookmarks_folder_unfiled);
+            }
+
+            // If for some reason we have a folder with a special GUID, but it's not one of
+            // the special folders we expect in the UI, just return the title from the DB.
+            return c.getString(c.getColumnIndexOrThrow(Bookmarks.TITLE));
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final int viewType = getItemViewType(position);
+
+            if (convertView == null) {
+                if (viewType == VIEW_TYPE_ITEM) {
+                    convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, null);
+                } else {
+                    convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_folder_row, null);
+                }
+            }
+
+            Cursor cursor = getCursor();
+            if (!cursor.moveToPosition(position)) {
+                throw new IllegalStateException("Couldn't move cursor to position " + position);
+            }
+
+            if (viewType == VIEW_TYPE_ITEM) {
+                TwoLinePageRow row = (TwoLinePageRow) convertView;
+                row.updateFromCursor(cursor);
+            } else {
+                BookmarkFolderView row = (BookmarkFolderView) convertView;
+                row.setText(getFolderTitle(position));
+            }
+
+            return convertView;
+        }
+    }
+
+    /**
+     * AsyncTask to query the DB for bookmarks.
+     */
+    private class BookmarksQueryTask extends AsyncTask<Integer, Void, Cursor> {
+        @Override
+        protected Cursor doInBackground(Integer... folderIds) {
+            int folderId = Bookmarks.FIXED_ROOT_ID;
+
+            if (folderIds.length != 0) {
+                folderId = folderIds[0].intValue();
+            }
+
+            return BrowserDB.getBookmarksInFolder(getActivity().getContentResolver(), folderId);
+        }
+
+        @Override
+        protected void onPostExecute(final Cursor cursor) {
+            refreshListWithCursor(cursor);
+        }
+    }
+}
--- a/mobile/android/base/home/HomePager.java
+++ b/mobile/android/base/home/HomePager.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.widget.AboutHome;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentStatePagerAdapter;
 import android.support.v4.view.ViewPager;
@@ -21,16 +22,17 @@ import java.util.EnumMap;
 import java.util.EnumSet;
 
 public class HomePager extends ViewPager {
     private final Context mContext;
     private volatile boolean mLoaded;
 
     // List of pages in order.
     private enum Page {
+        BOOKMARKS
     }
 
     private EnumMap<Page, Fragment> mPages = new EnumMap<Page, Fragment>(Page.class);
 
     public HomePager(Context context) {
         super(context);
         mContext = context;
     }
@@ -45,16 +47,17 @@ public class HomePager extends ViewPager
      *
      * @param fm FragmentManager for the adapter
      */
     public void show(FragmentManager fm) {
         mLoaded = true;
         TabsAdapter adapter = new TabsAdapter(fm);
 
         // Add the pages to the adapter in order.
+        adapter.addTab(Page.BOOKMARKS, BookmarksPage.class, null, getContext().getString(R.string.bookmarks_title));
 
         setAdapter(adapter);
         setVisibility(VISIBLE);
     }
 
     /**
      * Hides the pager and removes all child fragments.
      */
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/TwoLinePageRow.java
@@ -0,0 +1,92 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.home;
+
+import org.mozilla.gecko.Favicons;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB.URLColumns;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.widget.FaviconView;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class TwoLinePageRow extends LinearLayout {
+
+    private final TextView mTitle;
+    private final TextView mUrl;
+    private final FaviconView mFavicon;
+
+    public TwoLinePageRow(Context context) {
+        this(context, null);
+    }
+
+    public TwoLinePageRow(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TwoLinePageRow(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        setGravity(Gravity.CENTER_VERTICAL);
+
+        LayoutInflater.from(context).inflate(R.layout.two_line_page_row, this);
+        mTitle = (TextView) findViewById(R.id.title);
+        mUrl = (TextView) findViewById(R.id.url);
+        mFavicon = (FaviconView) findViewById(R.id.favicon);
+    }
+
+    public void updateFromCursor(Cursor cursor) {
+        if (cursor == null) {
+            return;
+        }
+
+        int titleIndex = cursor.getColumnIndexOrThrow(URLColumns.TITLE);
+        final String title = cursor.getString(titleIndex);
+
+        int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
+        final String url = cursor.getString(urlIndex);
+
+        // Use the URL instead of an empty title for consistency with the normal URL
+        // bar view - this is the equivalent of getDisplayTitle() in Tab.java
+        if (TextUtils.isEmpty(title)) {
+            mTitle.setText(url);
+        } else {
+            mTitle.setText(title);
+        }
+
+        // Update the url with "Switch to tab" if needed.
+        // FIXME: Bug 877469: Add back switch to tab functionality.
+        Integer tabId = null;
+        if (tabId != null) {
+            mUrl.setText(R.string.switch_to_tab);
+            mUrl.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_url_bar_tab, 0, 0, 0);
+        } else {
+            mUrl.setText(url);
+            mUrl.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+        }
+
+        byte[] b = cursor.getBlob(cursor.getColumnIndexOrThrow(URLColumns.FAVICON));
+        Bitmap favicon = null;
+        if (b != null) {
+            Bitmap bitmap = BitmapUtils.decodeByteArray(b);
+            if (bitmap != null) {
+                favicon = Favicons.getInstance().scaleImage(bitmap);
+            }
+        }
+
+        mFavicon.updateImage(favicon, url);
+    }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fa29670fbd82e43defc7477e24b87e48a5b9e7dc
GIT binary patch
literal 264
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt^PVn_Ar-gY-ZbQ63>0a7s9B<Q
zO+m*YNpPCO+KrN}K_MJv3oiv_h;SX<5^N(hvE%U`ULU2;_h#RDB2r(!f6k{g1|RhX
zhET@kulKHzi{3b=R`TDAv*&WoZ1R}O#P;T(z=A;824&|3dMg=Y+YWpUW=a!e_$|`F
zHhTlVbIY_f!psIK?B9|+E=(2`-L=s&``6>;JZ{ky4tWv*6%0B+gPAxKFo-n9a|Q=0
x58N_Hc+8l)&2(E!vh}q~^CK7-SypV9zpgJF&$WNsW}w#?JYD@<);T3K0RWm?UjhIC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ab96b22db613d3a39f46c7ee903e11d1c5d93b96
GIT binary patch
literal 506
zc$@+H0R{evP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0005ONkl<Zc-rlm
zy-vbV6vr9dob(xtK7ff2;Unnm;>$QV8YeXZeqj_pP@q99m6lS#mMZe$z{-Fo#>99$
zXEEM3Au-UBqj&i2|KGj$e58#40BfXx6c|>Z{O~-8Ib;jzBA*ESf_KD1mXSz63=~*E
zUJw`oOXCnw5f~BoVGGP6Fk<Gz6j%#DAQpze1hRsBAP@kX$V?Cdi->}}ArJzuNN)(5
z$T~7f;a3r0$_5E^84L7C066nM1azkj@vA$CUKYMNy(#k%IKQp@439Dz=yr>TfLd)(
zXn~Aw^LauE#Sp?DWeqT0CN+>X9N*~|stq1RY}DJZ=PBF^waTUoQCK1T{&f*bD8)iR
zt3COiPR?}s%z<O2B!oZ7nS2JBQmPB#V<`=ac8ia|VMg~9ZiUw>k<rE0Nh^fz@VNmR
zQZ%dtiDcg5walp%|N4-(9(iq}XL(ByI7s6ha64=S=-t!SDM+NW8Xo~7t=qnqs@0l&
z1WdQ%ZL2io*a#3*yuO0Y8g)JbMAB2xw51wOn~wmk67iH!(A1^mR|zTbR|NWu1^Ohg
w%~)WE1g4Q)W;a1uqsWx}Pb>wbfD{-nU+l*2%#NSp`v3p{07*qoM6N<$g534lp#T5?
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..2ca9e218209689740d7bdd1405d2b203fa472a85
GIT binary patch
literal 205
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJDV{ElAr-gYURUHh<RH-cP;G_W
zLiYy@F>Ik1qrdRXVLV*R!IzS%E!OIA;Bh=>SKj2l%16SJ->B@LvsR_G=)m=YeY~cA
z6%O?h7Z?5tl)2z+ymv#a%z;a6;VTX#1~PE-_600d|F$BX{X<1N_q_{ULbqfL92|rA
z0#w*74lwc+G%(9NXpkvrZ}PER;M$~==Tv`I<^yB7e^K~XeT8p8_c3_7`njxgN@xNA
DCGJYf
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9edb231d95ef579cb6ac97ec07526852fb4576cd
GIT binary patch
literal 375
zc$@)u0f_#IP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80003#Nkl<Zc-rmP
zJx&5a7{+lDD~{k1Jb;(6qTm*u!86!cC<Y4AL|h<oCBiN+iwd|DfLlVbRu&fe`n*#N
zABhPp$vX+z;#ZKcng1{!K!%8{N&{A21fwVm7w`qNff?i>vnFxCDbSXF;ArOnJahmd
zPy%N=1>k{0I0s7T8%`j*bpYOc3@`8tBq)X)2Sh-Y#2i32{TtBejp(w{)XHuk8=%67
zO7*rL{OX}aUVkhbP;U>->>aN!y9Ay0DP23B9=urdsT+RC22`4_X7;WMWj7(#n%7(p
ze)Fj0Pr>a|u)Oi)1In&40sdejAJBRmFQ0w+0CxXuHX&a|@&TRUlnRdCi25>8(#Me1
z{DJuts?rw`@T~a~(g6jtAq4}N8!Wzz?ixMufOY#F@M$q%&H*`yfo%LHO#hmt0e=@!
VV%@?E6-NL7002ovPDHLkV1oOfn@#`#
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7582d2f072752f08356663a88ae5e1195b08730b
GIT binary patch
literal 347
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU{vySaSW-r_4c-+AG4!K!$Y=4
zMGqN1A4_&!rq%~C98E_OEEy&xFPP4)AmA(P#B@T+=>}Iu#UF1A2G7lxIp2%d8ztRS
z`}}fybBD)V1||-L28Q~VdB3)}hnp;&mUJ?$#5q~&%bg;Ing=a*npH2l{Z_Lk{ODNp
z+%98zSOcePquy%fgaVJi73oo{c^@<_N@%~zV6eqIzm-ugvwA5*%)KSaRSP2=jja|d
zt`%o|aENX0W#*&y>zB42vuBXm_F|q=B)3#4!*vnQY^#RrM;LP11!f96Ffg(R5GFhr
zKg_FjeP;H1(v=Re%<E6;#8tnqTv*xEetzXb=K7g}5QPmMcg-ugr`<AjxiS+NXbhgN
KelF{r5}E*s>~|vo
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..03076f7bd6b99fa1474f296543173dde50d84fa9
GIT binary patch
literal 662
zc$@*20%`q;P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0007ANkl<Zc-rlo
zJ8u&~5XU158bnD)7aI5+BotIB_#7z^B@HbffRYySLV-N(%i|m;eq8Q+@!KJB43uf2
zC{9754F8eb?HVB>AIHuVzsg=q^V^--*}L@^5s5|xr~nn90#v}CQ0aVo0^WfR=mWYX
zr(hLKg0T^EI|2BCX>bK-1l)j^sTF|1Q-DUyPcWWV0WClY@HTY<Fz_*;g!m5bq)osZ
zKne0ZH3Bf`5jam+Ko>krR)87!eeeSO0Q3(y1+NC_cQ+9M_`*H#8XN&~oIdcuB*)Id
zKL9mA!ng(#EZ{vLq4+G|Ot64H3n0m;02S~TgLWz%`mkOwSlX$$FCc37Xkjxjg8!H)
zQn}G}U%+P2w9bCERChlG_FE@3n=Ki^&u^3|Z1>z3uwJNIXFnIU-7kWr`Z4m{4E{?l
z60r*)(2BM2L5%wX^6{Z{>1PTx8GXQ~EpywO^GX!9dhQF@+P80ee$;lq6XY45^~`N=
z(GO*`!kJ>tI{U@ik^2JB)^e3Mf?wOK$~X)y=VK#yqyu+@G6a0~qhT$t3<1dV-yM2n
z6v5TKgJH*B83Kal3C(1Swg(?F1TbE?VNHd6+>s#wPk^}i83OQBh(D8Y7QlJmx(d8<
zU4{T=aXT~w%nS;m2myuaSL-T3!z4ohv$7joCG5tBG6V#TE-(9G1<`#0fIC6!D%c1b
zG6e8hV9qnQMQ)garhpeC%y!tau7Z_(tS?7ZfC~8k1YAhJB9oJTNooRIi@hc{xG4D)
w_~24N;FrJ$&-5=dDnJFO02QDDQ~-(Q7p#g{`q3iZs{jB107*qoM6N<$g7NbjzW@LL
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/bookmark_folder.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <!-- state open -->
+    <item gecko:state_open="true"
+          android:drawable="@drawable/bookmark_folder_opened"/>
+
+    <!-- state close -->
+    <item android:drawable="@drawable/bookmark_folder_closed"/>
+
+</selector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_folder_row.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<org.mozilla.gecko.home.BookmarkFolderView xmlns:android="http://schemas.android.com/apk/res/android"
+                                           style="@style/Widget.BookmarkFolderView"
+                                           android:layout_width="fill_parent"
+                                           android:layout_height="@dimen/page_row_height"
+                                           android:minHeight="@dimen/page_row_height"
+                                           android:gravity="center_vertical"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/bookmark_item_row.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<org.mozilla.gecko.home.TwoLinePageRow xmlns:android="http://schemas.android.com/apk/res/android"
+                                       android:layout_width="fill_parent"
+                                       android:layout_height="@dimen/page_row_height"
+                                       android:minHeight="@dimen/page_row_height"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/two_line_page_row.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <org.mozilla.gecko.widget.FaviconView android:id="@+id/favicon"
+                                          android:layout_width="@dimen/favicon_bg"
+                                          android:layout_height="@dimen/favicon_bg"
+                                          android:layout_marginLeft="10dip"
+                                          android:layout_marginRight="10dip"/>
+
+    <LinearLayout android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"
+                  android:orientation="vertical">
+
+        <TextView android:id="@+id/title"
+                  style="@style/Widget.TwoLinePageRow.Title"
+                  android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"/>
+
+        <TextView android:id="@+id/url"
+                  style="@style/Widget.TwoLinePageRow.Url"
+                  android:layout_width="wrap_content"
+                  android:layout_height="wrap_content"/>
+
+    </LinearLayout>
+
+</merge>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/values-v16/styles.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <style name="Widget.TwoLinePageRow.Title">
+        <item name="android:textAppearance">@style/TextAppearance.Widget.TwoLinePageRow.Title</item>
+        <item name="android:fontFamily">sans-serif-light</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:ellipsize">middle</item>
+    </style>
+
+</resources>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -163,10 +163,14 @@
             <flag name="icon" value="0x01" />
         </attr>
     </declare-styleable>
 
     <declare-styleable name="HomePagerTabStrip">
         <attr name="tabIndicatorColor" format="color"/>
     </declare-styleable>
 
+    <declare-styleable name="BookmarkFolderView">
+        <attr name="state_open" format="boolean"/>
+    </declare-styleable>
+
 </resources>
 
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -32,17 +32,17 @@
   <!-- highlight-focused on private nav button: 10% white over background_private -->
   <color name="highlight_nav_focused_pb">#FF3F423F</color>
 
   <!--
       Application theme colors
   -->
   <!-- Default colors -->
   <color name="text_color_primary">#222222</color>
-  <color name="text_color_secondary">#666666</color>
+  <color name="text_color_secondary">#777777</color>
   <color name="text_color_tertiary">#9198A1</color>
 
   <!-- Default inverse colors -->
   <color name="text_color_primary_inverse">#FFFFFF</color>
   <color name="text_color_secondary_inverse">#DDDDDD</color>
   <color name="text_color_tertiary_inverse">#A4A7A9</color>
 
   <!-- Disabled colors -->
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -31,16 +31,19 @@
     <dimen name="browser_toolbar_favicon_size">29.33dip</dimen>
 
     <!-- Dimensions used by Favicons and FaviconView -->
     <dimen name="favicon_size_small">16dp</dimen>
     <dimen name="favicon_size_large">32dp</dimen>
     <dimen name="favicon_bg">32dp</dimen>
     <dimen name="favicon_bg_radius">1dp</dimen>
 
+    <!-- Page Row height -->
+    <dimen name="page_row_height">64dp</dimen>
+
     <!-- Max width of the doorhanger on tablets -->
     <dimen name="doorhanger_width">400dp</dimen>
 
     <dimen name="flow_layout_spacing">6dp</dimen>
     <dimen name="menu_item_icon">21dp</dimen>
     <dimen name="menu_item_state_icon">18dp</dimen>
     <dimen name="menu_item_row_height">44dp</dimen>
     <dimen name="menu_item_row_width">240dp</dimen>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -64,16 +64,37 @@
         <item name="android:textAppearance">?android:attr/textAppearanceLargeInverse</item>
         <item name="android:gravity">center_vertical</item>
         <item name="android:paddingLeft">12dip</item>
         <item name="android:paddingRight">7dip</item>
         <item name="android:checkMark">?android:attr/listChoiceIndicatorMultiple</item>
         <item name="android:ellipsize">marquee</item>
     </style>
 
+    <style name="Widget.TwoLinePageRow" />
+
+    <style name="Widget.TwoLinePageRow.Title">
+        <item name="android:textAppearance">@style/TextAppearance.Widget.TwoLinePageRow.Title</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:ellipsize">middle</item>
+    </style>
+
+    <style name="Widget.TwoLinePageRow.Url">
+        <item name="android:textAppearance">@style/TextAppearance.Widget.TwoLinePageRow.Url</item>
+        <item name="android:includeFontPadding">false</item>
+        <item name="android:singleLine">true</item>
+        <item name="android:ellipsize">middle</item>
+    </style>
+
+    <style name="Widget.BookmarkFolderView" parent="Widget.TwoLinePageRow.Title">
+        <item name="android:paddingLeft">10dip</item>
+        <item name="android:drawablePadding">10dip</item>
+        <item name="android:drawableLeft">@drawable/bookmark_folder</item>
+    </style>
+
     <!--
         TextAppearance
         Note: Gecko uses light theme as default, while Android uses dark.
         If Android convention has to be followd, the list of colors specified 
         in themes.xml would be inverse, and things would get confusing.
         Hence, Gecko's TextAppearance is based on text over light theme and
         TextAppearance.Inverse is based on text over dark theme.
     -->
@@ -156,16 +177,24 @@
     <style name="TextAppearance.Widget.TextView">
         <item name="android:textColor">@color/primary_text</item>
     </style>
 
     <style name="TextAppearance.Widget.HomePagerTabStrip" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorHint</item>
     </style>
 
+    <style name="TextAppearance.Widget.TwoLinePageRow" />
+
+    <style name="TextAppearance.Widget.TwoLinePageRow.Title" parent="TextAppearance.Medium"/>
+
+    <style name="TextAppearance.Widget.TwoLinePageRow.Url" parent="TextAppearance.Micro">
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+    </style>
+
     <!-- BrowserToolbar -->
     <style name="BrowserToolbar">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
     <style name="UrlBar.ImageButton.TabCount">