Bug 882365: Add a top bookmarks section grid view. [r=lucasr]
authorSriram Ramasubramanian <sriram@mozilla.com>
Thu, 20 Jun 2013 11:45:58 -0700
changeset 151330 f154de5f5ef71070b4052a93bf22e3f43f5897d8
parent 151329 5b02134ccd2d6742ea0e8923c12435e151e98bd4
child 151331 e2a28f0025f10afb9e5adf1c249b8855f8868200
push idunknown
push userunknown
push dateunknown
reviewerslucasr
bugs882365
milestone24.0a1
Bug 882365: Add a top bookmarks section grid view. [r=lucasr]
mobile/android/base/GeckoViewsFactory.java
mobile/android/base/Makefile.in
mobile/android/base/home/BookmarkThumbnailView.java
mobile/android/base/home/TopBookmarkItemView.java
mobile/android/base/home/TopBookmarksView.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/color/top_bookmark_item_title.xml
mobile/android/base/resources/drawable-hdpi/abouthome_thumbnail_add.png
mobile/android/base/resources/drawable-mdpi/abouthome_thumbnail_add.png
mobile/android/base/resources/drawable-xhdpi/abouthome_thumbnail_add.png
mobile/android/base/resources/layout/top_bookmark_item_view.xml
mobile/android/base/resources/values-v11/styles.xml
mobile/android/base/resources/values-v11/themes.xml
mobile/android/base/resources/values/attrs.xml
mobile/android/base/resources/values/styles.xml
mobile/android/base/resources/values/themes.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/GeckoViewsFactory.java
+++ b/mobile/android/base/GeckoViewsFactory.java
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.home.BookmarkFolderView;
+import org.mozilla.gecko.home.BookmarkThumbnailView;
 import org.mozilla.gecko.home.BookmarksListView;
 import org.mozilla.gecko.home.FadedTextView;
 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;
@@ -85,16 +86,17 @@ public final class GeckoViewsFactory imp
             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.BookmarkThumbnailView", BookmarkThumbnailView.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("home.BookmarksListView", BookmarksListView.class.getConstructor(arg1Class, arg2Class));
             mFactoryMap.put("home.FadedTextView", FadedTextView.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);
         }
     }
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -211,21 +211,24 @@ FENNEC_JAVA_FILES = \
   gfx/TextureReaper.java \
   gfx/TileLayer.java \
   gfx/TouchEventHandler.java \
   gfx/ViewTransform.java \
   gfx/VirtualLayer.java \
   home/BookmarksListView.java \
   home/BookmarksPage.java \
   home/BookmarkFolderView.java \
+  home/BookmarkThumbnailView.java \
   home/HomeFragment.java \
   home/HomeListView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
   home/FadedTextView.java \
+  home/TopBookmarkItemView.java \
+  home/TopBookmarksView.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 \
@@ -459,16 +462,17 @@ RES_LAYOUT = \
   res/layout/search_engine_row.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/top_bookmark_item_view.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/suggestion_item.xml \
   res/layout/abouthome_addon_row.xml \
@@ -977,16 +981,17 @@ RES_COLOR = \
   res/color/menu_item_title.xml \
   res/color/primary_text.xml \
   res/color/primary_text_inverse.xml \
   res/color/secondary_text.xml \
   res/color/secondary_text_inverse.xml \
   res/color/select_item_multichoice.xml \
   res/color/tertiary_text.xml \
   res/color/tertiary_text_inverse.xml \
+  res/color/top_bookmark_item_title.xml \
   res/color/url_bar_title.xml \
   res/color/url_bar_title_hint.xml \
   $(NULL)
 
 RES_MENU = \
   res/menu/abouthome_topsites_contextmenu.xml \
   res/menu/browser_app_menu.xml \
   res/menu/gecko_app_menu.xml \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/BookmarkThumbnailView.java
@@ -0,0 +1,71 @@
+/* -*- 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.ThumbnailHelper;
+
+import android.content.Context;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * A height constrained ImageView to show thumbnails of top bookmarks.
+ */
+public class BookmarkThumbnailView extends ImageView {
+    private static final String LOGTAG = "GeckoBookmarkThumbnailView";
+
+    // 27.34% opacity filter for the dominant color.
+    private static final int COLOR_FILTER = 0x46FFFFFF;
+
+    // Default filter color for "Add a bookmark" views.
+    private static final int DEFAULT_COLOR = 0x46ECF0F3;
+
+    public BookmarkThumbnailView(Context context) {
+        this(context, null);
+    }
+
+    public BookmarkThumbnailView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.bookmarkThumbnailViewStyle);
+    }
+
+    public BookmarkThumbnailView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    /**
+     * Measure the view to determine the measured width and height.
+     * The height is constrained by the measured width.
+     *
+     * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
+     * @param heightMeasureSpec vertical space requirements as imposed by the parent, but ignored.
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Default measuring.
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        // Force the height based on the aspect ratio.
+        final int width = getMeasuredWidth();
+        final int height = (int) (width * ThumbnailHelper.THUMBNAIL_ASPECT_RATIO);
+        setMeasuredDimension(width, height);
+    }
+
+    /**
+     * Sets the background to a Drawable by applying the specified color as a filter.
+     *
+     * @param color the color filter to apply over the drawable.
+     */
+    @Override
+    public void setBackgroundColor(int color) {
+        int colorFilter = color == 0 ? DEFAULT_COLOR : color & COLOR_FILTER;
+        Drawable drawable = getResources().getDrawable(R.drawable.favicon_bg);
+        drawable.setColorFilter(colorFilter, Mode.SRC_ATOP);
+        setBackgroundDrawable(drawable);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/TopBookmarkItemView.java
@@ -0,0 +1,221 @@
+/* -*- 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 android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+/**
+ * A view that displays the thumbnail and the title/url for a bookmark.
+ * If the title/url is longer than the width of the view, they are faded out.
+ * If there is no valid url, a default string is shown at 50% opacity.
+ * This is denoted by the empty state.
+ */
+public class TopBookmarkItemView extends RelativeLayout {
+    private static final String LOGTAG = "GeckoTopBookmarkItemView";
+
+    // Empty state, to denote there is no valid url.
+    private static final int[] STATE_EMPTY = { android.R.attr.state_empty };
+
+    // A Pin Drawable to denote pinned sites.
+    private static Drawable sPinDrawable = null;
+
+    // Child views.
+    private final TextView mTitleView;
+    private final ImageView mThumbnailView;
+    private final ImageView mPinView;
+
+    // Data backing this view.
+    private String mTitle;
+    private String mUrl;
+
+    // Pinned state.
+    private boolean mIsPinned = false;
+
+    // Empty state.
+    private boolean mIsEmpty = true;
+
+    public TopBookmarkItemView(Context context) {
+        this(context, null);
+    }
+
+    public TopBookmarkItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.topBookmarkItemViewStyle);
+    }
+
+    public TopBookmarkItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        LayoutInflater.from(context).inflate(R.layout.top_bookmark_item_view, this);
+
+        mTitleView = (TextView) findViewById(R.id.title);
+        mThumbnailView = (ImageView) findViewById(R.id.thumbnail);
+        mPinView = (ImageView) findViewById(R.id.pin);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+        if (mIsEmpty) {
+            mergeDrawableStates(drawableState, STATE_EMPTY);
+        }
+
+        return drawableState;
+    }
+
+    /**
+     * @return The title shown by this view.
+     */
+    public String getTitle() {
+        return (!TextUtils.isEmpty(mTitle) ? mTitle : mUrl);
+    }
+
+    /**
+     * @return The url shown by this view.
+     */
+    public String getUrl() {
+        return mUrl;
+    }
+
+    /**
+     * @return true, if this view is pinned, false otherwise.
+     */
+    public boolean isPinned() {
+        return mIsPinned;
+    }
+
+    /**
+     * @param title The title for this view.
+     */
+    public void setTitle(String title) {
+        if (mTitle != null && mTitle.equals(title)) {
+            return;
+        }
+
+        mTitle = title;
+        updateTitleView();
+    }
+
+    /**
+     * @param url The url for this view.
+     */
+    public void setUrl(String url) {
+        if (mUrl != null && mUrl.equals(url)) {
+            return;
+        }
+
+        mUrl = url;
+        updateTitleView();
+    }
+
+    /**
+     * @param pinned The pinned state of this view.
+     */
+    public void setPinned(boolean pinned) {
+        mIsPinned = pinned;
+        mPinView.setBackgroundDrawable(pinned ? getPinDrawable() : null);
+    }
+
+    /**
+     * Display the thumbnail from a resource.
+     *
+     * @param resId Resource ID of the drawable to show.
+     */
+    public void displayThumbnail(int resId) {
+        mThumbnailView.setImageResource(resId);
+        mThumbnailView.setBackgroundColor(0x0);
+    }
+
+    /**
+     * Display the thumbnail from a bitmap.
+     *
+     * @param thumbnail The bitmap to show as thumbnail.
+     */
+    public void displayThumbnail(Bitmap thumbnail) {
+        if (thumbnail == null) {
+            // Show a favicon based view instead.
+            displayThumbnail(R.drawable.favicon);
+            return;
+        }
+
+        mThumbnailView.setImageBitmap(thumbnail);
+        mThumbnailView.setBackgroundDrawable(null);
+    }
+
+    /**
+     * Display the thumbnail from a favicon.
+     *
+     * @param favicon The favicon to show as thumbnail.
+     */
+    public void displayFavicon(Bitmap favicon) {
+        if (favicon == null) {
+            // Should show default favicon.
+            displayThumbnail(R.drawable.favicon);
+            return;
+        }
+
+        mThumbnailView.setImageBitmap(favicon);
+        mThumbnailView.setBackgroundColor(Favicons.getInstance().getFaviconColor(favicon, mUrl));
+    }
+
+    /**
+     * Update the title shown by this view. If both title and url
+     * are empty, mark the state as STATE_EMPTY and show a default text.
+     */
+    private void updateTitleView() {
+        String title = getTitle();
+        if (!TextUtils.isEmpty(title)) {
+            mTitleView.setText(title);
+            mIsEmpty = false;
+        } else {
+            mTitleView.setText(R.string.bookmark_add);
+            mIsEmpty = true;
+        }
+
+        // Refresh for state change.
+        refreshDrawableState();
+    }
+
+    /**
+     * @return Drawable to be used as a pin.
+     */
+    private Drawable getPinDrawable() {
+        if (sPinDrawable == null) {
+            int size = getResources().getDimensionPixelSize(R.dimen.abouthome_topsite_pinsize);
+
+            // Draw a little triangle in the upper right corner.
+            Path path = new Path();
+            path.moveTo(0, 0);
+            path.lineTo(size, 0);
+            path.lineTo(size, size);
+            path.close();
+
+            sPinDrawable = new ShapeDrawable(new PathShape(path, size, size));
+            Paint p = ((ShapeDrawable) sPinDrawable).getPaint();
+            p.setColor(getResources().getColor(R.color.abouthome_topsite_pin));
+        }
+
+        return sPinDrawable;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/TopBookmarksView.java
@@ -0,0 +1,384 @@
+/* -*- 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.ThumbnailHelper;
+import org.mozilla.gecko.db.BrowserContract.Thumbnails;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
+import org.mozilla.gecko.db.BrowserDB.URLColumns;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.UiAsyncTask;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.CursorAdapter;
+import android.widget.GridView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A grid view of top bookmarks and pinned tabs.
+ * Each cell in the grid is a TopBookmarkItemView.
+ */
+public class TopBookmarksView extends GridView {
+    private static final String LOGTAG = "GeckoTopBookmarksView";
+
+    // Max number of bookmarks that needs to be shown.
+    private int mMaxBookmarks;
+
+    // Number of columns to show.
+    private int mNumColumns;
+
+    // On URL open listener.
+    private OnUrlOpenListener mUrlOpenListener;
+
+    // A cursor based adapter backing this view.
+    protected TopBookmarksAdapter mAdapter;
+
+    // Temporary cache to store the thumbnails until the next layout pass.
+    private Map<String, Bitmap> mThumbnailsCache;
+
+    public TopBookmarksView(Context context) {
+        this(context, null);
+    }
+
+    public TopBookmarksView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.topBookmarksViewStyle);
+    }
+
+    public TopBookmarksView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mMaxBookmarks = getResources().getInteger(R.integer.number_of_top_sites);
+        mNumColumns = getResources().getInteger(R.integer.number_of_top_sites_cols);
+        setNumColumns(mNumColumns);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Initialize the adapter.
+        mAdapter = new TopBookmarksAdapter(getContext(), null);
+        setAdapter(mAdapter);
+
+        setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                TopBookmarkItemView row = (TopBookmarkItemView) view;
+                String url = row.getUrl();
+
+                if (mUrlOpenListener != null && !TextUtils.isEmpty(url)) {
+                    mUrlOpenListener.onUrlOpen(url);
+                }
+            }
+        });
+
+        // Load the bookmarks.
+        new LoadBookmarksTask().execute();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mAdapter != null) {
+            setAdapter(null);
+            final Cursor cursor = mAdapter.getCursor();
+
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                if (cursor != null && !cursor.isClosed())
+                    cursor.close();
+                }
+            });
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getColumnWidth() {
+        // This method will be called from onMeasure() too.
+        // It's better to use getMeasuredWidth(), as it is safe in this case.
+        return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / mNumColumns;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Sets the padding for this view.
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        final int measuredWidth = getMeasuredWidth();
+        final int childWidth = getColumnWidth();
+        int childHeight = 0;
+
+        // Set the column width as the thumbnail width.
+        ThumbnailHelper.getInstance().setThumbnailWidth(childWidth);
+
+        // If there's an adapter, use it to calculate the height of this view.
+        final TopBookmarksAdapter adapter = (TopBookmarksAdapter) getAdapter();
+        final int count = (adapter == null ? 0 : adapter.getCount());
+
+        if (adapter != null && count > 0) {
+            // Get the first child from the adapter.
+            final View child = adapter.getView(0, null, this);
+            if (child != null) {
+                // Set a default LayoutParams on the child, if it doesn't have one on its own.
+                AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams();
+                if (params == null) {
+                    params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT,
+                                                          AbsListView.LayoutParams.WRAP_CONTENT);
+                    child.setLayoutParams(params);
+                }
+
+                // Measure the exact width of the child, and the height based on the width.
+                // Note: the child (and BookmarkThumbnailView) takes care of calculating its height.
+                int childWidthSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
+                int childHeightSpec = MeasureSpec.makeMeasureSpec(0,  MeasureSpec.UNSPECIFIED);
+                child.measure(childWidthSpec, childHeightSpec);
+                childHeight = child.getMeasuredHeight();
+            }
+        }
+
+        // Find the minimum of bookmarks we need to show, and the one given by the cursor.
+        final int total = Math.min(count > 0 ? count : Integer.MAX_VALUE, mMaxBookmarks);
+
+        // Number of rows required to show these bookmarks.
+        final int rows = (int) Math.ceil((double) total / mNumColumns);
+        final int childrenHeight = childHeight * rows;
+
+        // Total height of this view.
+        final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom();
+        setMeasuredDimension(measuredWidth, measuredHeight);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        // If there are thumnails in the cache, update them.
+        if (mThumbnailsCache != null) {
+            updateThumbnails(mThumbnailsCache);
+            mThumbnailsCache = null;
+        }
+    }
+
+    /**
+     * Set an url open listener to be used by this view.
+     *
+     * @param listener An url open listener for this view.
+     */
+    public void setOnUrlOpenListener(OnUrlOpenListener listener) {
+        mUrlOpenListener = listener;
+    }
+
+    /**
+     * Update the thumbnails returned by the db.
+     *
+     * @param thumbnails A map of urls and their thumbnail bitmaps.
+     */
+    private void updateThumbnails(Map<String, Bitmap> thumbnails) {
+        final int count = mAdapter.getCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+
+            // The grid view might get temporarily out of sync with the
+            // adapter refreshes (e.g. on device rotation).
+            if (child == null) {
+                continue;
+            }
+
+            TopBookmarkItemView view = (TopBookmarkItemView) child;
+            final String url = view.getUrl();
+
+            // If there is no url, then show "add bookmark".
+            if (TextUtils.isEmpty(url)) {
+                view.displayThumbnail(R.drawable.abouthome_thumbnail_add);
+            } else {
+                // Show the thumbnail.
+                Bitmap bitmap = (thumbnails != null ? thumbnails.get(url) : null);
+                view.displayThumbnail(bitmap);
+            }
+        }
+    }
+
+    /**
+     * A cursor adapter holding the pinned and top bookmarks.
+     */
+    public class TopBookmarksAdapter extends CursorAdapter {
+        public TopBookmarksAdapter(Context context, Cursor cursor) {
+            super(context, cursor);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public int getCount() {
+            return Math.min(super.getCount(), mMaxBookmarks);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected void onContentChanged () {
+            // Don't do anything. We don't want to regenerate every time
+            // our database is updated.
+            return;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void bindView(View bindView, Context context, Cursor cursor) {
+            String url = "";
+            String title = "";
+            boolean pinned = false;
+
+            // Cursor is already moved to required position.
+            if (!cursor.isAfterLast()) {
+                url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
+                title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
+                pinned = ((TopSitesCursorWrapper) cursor).isPinned();
+            }
+
+            TopBookmarkItemView view = (TopBookmarkItemView) bindView;
+            view.setTitle(title);
+            view.setUrl(url);
+            view.setPinned(pinned);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            return new TopBookmarkItemView(context);
+        }
+    }
+
+    /**
+     * An AsyncTask to load the top bookmarks from the db.
+     */
+    private class LoadBookmarksTask extends UiAsyncTask<Void, Void, Cursor> {
+        public LoadBookmarksTask() {
+            super(ThreadUtils.getBackgroundHandler());
+        }
+
+        @Override
+        protected Cursor doInBackground(Void... params) {
+            return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxBookmarks);
+        }
+
+        @Override
+        public void onPostExecute(Cursor cursor) {
+            // Change to new cursor.
+            mAdapter.changeCursor(cursor);
+
+            // Load the thumbnails.
+            if (mAdapter.getCount() > 0) {
+                new LoadThumbnailsTask().execute(cursor);
+            }
+        }
+    }
+
+    /**
+     * An AsyncTask to load the thumbnails from a cursor.
+     */
+    private class LoadThumbnailsTask extends UiAsyncTask<Cursor, Void, Map<String,Bitmap>> {
+        public LoadThumbnailsTask() {
+            super(ThreadUtils.getBackgroundHandler());
+        }
+
+        @Override
+        protected Map<String, Bitmap> doInBackground(Cursor... params) {
+            // TopBookmarksAdapter's cursor.
+            final Cursor adapterCursor = params[0];
+            if (adapterCursor == null || !adapterCursor.moveToFirst()) {
+                return null;
+            }
+
+            final List<String> urls = new ArrayList<String>();
+            do {
+                final String url = adapterCursor.getString(adapterCursor.getColumnIndexOrThrow(URLColumns.URL));
+                urls.add(url);
+            } while(adapterCursor.moveToNext());
+
+            if (urls.size() == 0) {
+                return null;
+            }
+
+            Map<String, Bitmap> thumbnails = new HashMap<String, Bitmap>();
+            Cursor cursor = BrowserDB.getThumbnailsForUrls(getContext().getContentResolver(), urls);
+            if (cursor == null || !cursor.moveToFirst()) {
+                return null;
+            }
+
+            try {
+                do {
+                    final String url = cursor.getString(cursor.getColumnIndexOrThrow(Thumbnails.URL));
+                    final byte[] b = cursor.getBlob(cursor.getColumnIndexOrThrow(Thumbnails.DATA));
+                    if (b == null) {
+                        continue;
+                    }
+
+                    Bitmap thumbnail = BitmapUtils.decodeByteArray(b);
+                    if (thumbnail == null) {
+                        continue;
+                    }
+
+                    thumbnails.put(url, thumbnail);
+                } while (cursor.moveToNext());
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            return thumbnails;
+        }
+
+        @Override
+        public void onPostExecute(Map<String, Bitmap> thumbnails) {
+            // If there's a layout scheduled on this view, wait for it to happen
+            // by storing the thumbnails in a cache. If not, update them right away.
+            if (isLayoutRequested()) {
+                mThumbnailsCache = thumbnails;
+            } else {
+                updateThumbnails(thumbnails);
+            }
+        }
+    }
+}
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -205,16 +205,17 @@ size. -->
 <!ENTITY contextmenu_site_settings "Edit Site Settings">
 
 <!ENTITY pref_titlebar_mode "Title bar">
 <!ENTITY pref_titlebar_mode_title "Show page title">
 <!ENTITY pref_titlebar_mode_url "Show page address">
 
 <!ENTITY history_removed "Page removed">
 
+<!ENTITY bookmark_add "Add a bookmark">
 <!ENTITY bookmark_edit_title "Edit Bookmark">
 <!ENTITY bookmark_edit_name "Name">
 <!ENTITY bookmark_edit_location "Location">
 <!ENTITY bookmark_edit_keyword "Keyword">
 
 <!-- Localization note (site_settings_*) : These strings are used in the "Site Settings"
      dialog that appears after selecting the "Edit Site Settings" context menu item. -->
 <!ENTITY site_settings_title3       "Site Settings">
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/top_bookmark_item_title.xml
@@ -0,0 +1,14 @@
+<?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">
+
+    <!-- empty with no url -->
+    <item android:state_empty="true" android:color="#80777777" />
+
+    <!-- default -->
+    <item android:color="#FF777777"/>
+
+</selector>
index afa1421f72ebe79956435c0853faa2a62e1a5574..59427ab670b73ebdc5950f6434fbb1c05803f1fd
GIT binary patch
literal 151
zc%17D@N?(olHy`uVBq!ia0vp^HXzK%3?vzhybXb*WQl7;NpOBzNqJ&XDuZK6ep0G}
zXKrG8YEWuoN@d~6RFDp~0G|+7ApPdUXVdAI1b{5Yk|4ie28U-i(tsQ(PZ!4!jq}L~
p2iTstvn~E-FCnAQWmwe2%Es{DS}W^$V?-RtLQhvemvv4FO#t0HDck@6
index c1b22593e1819566eb44dca2e774e645699d25a8..1f1a4ae5cfda5f76f23efeac89a1d82256dc1810
GIT binary patch
literal 137
zc%17D@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dE0iG_7Ar-gYURLB{P!Mo+d|w~d
z{D3jcpu_jzf<rCW?M_ee75rFo+EK4``wXvkMkW~GOR)bJ)v&x$$N>rt^f!M0&3=P{
X(QmthWYd)ZpxF$bu6{1-oD!M<dqOJW
index 14bf1ffa3b42faa6362778b60d985ce14548f46d..da9f1907a51913cece960eed1f62d07d5b68c49f
GIT binary patch
literal 156
zc%17D@N?(olHy`uVBq!ia0vp^0U*rC3?#SQPk0BUBuiW)N`mv#O3D+9QW+dm@{>{(
zJaZG%Q-e|yQz{EjrrH1%u?6^qxB}@nA3mE-za#);F_r}R1v5B2yO9Ru$a}gthG?8m
xPEcUmU@wt+X}acrd9Jz%r)79gHYPn{X4s=zyq%%Dd?!#TgQu&X%Q~loCIIymFAx9#
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/top_bookmark_item_view.xml
@@ -0,0 +1,31 @@
+<?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"
+       xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <org.mozilla.gecko.home.BookmarkThumbnailView
+            android:id="@+id/thumbnail"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentTop="true"/>
+
+    <org.mozilla.gecko.home.FadedTextView
+            android:id="@+id/title"
+            style="@style/Widget.TopBookmarkItemTitle"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/thumbnail"
+            android:duplicateParentState="true"
+            gecko:fadeWidth="20dip"/>
+
+    <ImageView android:id="@+id/pin"
+               style="@style/Widget.TopBookmarkItemPin"
+               android:layout_width="@dimen/abouthome_topsite_pinsize"
+               android:layout_height="@dimen/abouthome_topsite_pinsize"
+               android:layout_alignTop="@id/thumbnail"
+               android:layout_alignRight="@id/thumbnail"/>
+
+</merge>
--- a/mobile/android/base/resources/values-v11/styles.xml
+++ b/mobile/android/base/resources/values-v11/styles.xml
@@ -16,16 +16,18 @@
     <style name="Widget.BaseButton" parent="android:style/Widget.Holo.Light.Button"/>
 
     <style name="Widget.BaseDropDownItem" parent="android:style/Widget.Holo.Light.DropDownItem"/>
 
     <style name="Widget.BaseEditText" parent="android:style/Widget.Holo.Light.EditText"/>
 
     <style name="Widget.BaseListView" parent="android:style/Widget.Holo.ListView"/>
 
+    <style name="Widget.BaseGridView" parent="android:style/Widget.Holo.GridView"/>
+
     <style name="Widget.BaseTextView" parent="android:style/Widget.Holo.Light.TextView"/>
 
 
     <!--
         Application styles. All customizations that are not specific
         to a particular API level can go here.
     -->
     <style name="Widget.ListItem">
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -30,13 +30,17 @@
 
     <!--
         Activity based themes.
     -->
     <style name="Gecko.App">
         <item name="android:windowBackground">@android:color/white</item>
         <item name="android:panelBackground">@drawable/menu_panel_bg</item>
         <item name="android:listViewStyle">@style/Widget.ListView</item>
+        <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
+        <item name="bookmarkThumbnailViewStyle">@style/Widget.BookmarkThumbnailView</item>
+        <item name="topBookmarkItemViewStyle">@style/Widget.TopBookmarkItemView</item>
+        <item name="topBookmarksViewStyle">@style/Widget.TopBookmarksView</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -1,15 +1,29 @@
 <?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>
 
+    <!-- Theme level attributes -->
+    <declare-styleable name="Theme">
+
+        <!-- Default style for the BookmarkThumbnailView -->
+        <attr name="bookmarkThumbnailViewStyle" format="reference" />
+
+        <!-- Default style for the TopBookmarkItemView -->
+        <attr name="topBookmarkItemViewStyle" format="reference" />
+
+        <!-- Default style for the TopBookmarksView -->
+        <attr name="topBookmarksViewStyle" format="reference" />
+
+    </declare-styleable>
+
     <declare-styleable name="AboutHomeSection">
         <attr name="title" format="string"/>
         <attr name="subtitle" format="string"/>
         <attr name="more_text" format="string"/>
     </declare-styleable>
 
     <!-- DoorHangers -->
     <declare-styleable name="DoorHanger">
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -13,16 +13,18 @@
     <style name="Widget.BaseButton" parent="android:style/Widget.Button"/>
 
     <style name="Widget.BaseDropDownItem" parent="android:style/Widget.DropDownItem"/>
 
     <style name="Widget.BaseEditText" parent="android:style/Widget.EditText"/>
 
     <style name="Widget.BaseListLiew" parent="android:style/Widget.ListView"/>
 
+    <style name="Widget.BaseGridView" parent="android:style/Widget.GridView"/>
+
     <style name="Widget.BaseTextView" parent="android:style/Widget.TextView"/>
 
     <!--
         Application styles. All customizations that are not specific
         to a particular API level can go here.
     -->
     <style name="Widget.Button" parent="Widget.BaseButton">
         <item name="android:textAppearance">@style/TextAppearance.Widget.Button</item>
@@ -46,16 +48,23 @@
         <item name="android:cacheColorHint">@android:color/transparent</item>
         <item name="android:listSelector">@drawable/action_bar_button</item>
     </style>
 
     <style name="Widget.ExpandableListView" parent="Widget.ListView">
         <item name="android:groupIndicator">@android:color/transparent</item>
     </style>
 
+    <style name="Widget.GridView" parent="Widget.BaseGridView">
+        <item name="android:verticalSpacing">0dip</item>
+        <item name="android:horizontalSpacing">0dip</item>
+        <item name="android:cacheColorHint">@android:color/transparent</item>
+        <item name="android:listSelector">@drawable/action_bar_button</item>
+    </style>
+
     <style name="Widget.ListItem">
         <item name="android:minHeight">?android:attr/listPreferredItemHeight</item>
         <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>
@@ -83,16 +92,47 @@
     </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>
 
+    <style name="Widget.TopBookmarksView" parent="Widget.GridView">
+        <item name="android:padding">7dp</item>
+    </style>
+
+    <style name="Widget.TopBookmarkItemView">
+      <item name="android:layout_width">fill_parent</item>
+      <item name="android:layout_height">fill_parent</item>
+      <item name="android:padding">5dip</item>
+      <item name="android:orientation">vertical</item>
+    </style>
+
+    <style name="Widget.BookmarkThumbnailView">
+      <item name="android:padding">0dip</item>
+      <item name="android:scaleType">center</item>
+    </style>
+
+    <style name="Widget.TopBookmarkItemPin">
+      <item name="android:minWidth">30dip</item>
+      <item name="android:minHeight">30dip</item>
+      <item name="android:padding">0dip</item>
+    </style>
+
+    <style name="Widget.TopBookmarkItemTitle">
+      <item name="android:textColor">@color/top_bookmark_item_title</item>
+      <item name="android:textSize">12sp</item>
+      <item name="android:singleLine">true</item>
+      <item name="android:ellipsize">none</item>
+      <item name="android:paddingTop">5dip</item>
+      <item name="android:gravity">left</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.
     -->
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -69,16 +69,20 @@
     <!--
         Activity based themes.
     -->
     <style name="Gecko.App">
         <item name="android:windowBackground">@android:color/white</item>
         <item name="android:buttonStyle">@style/Widget.Button</item>
         <item name="android:dropDownItemStyle">@style/Widget.DropDownItem</item>
         <item name="android:editTextStyle">@style/Widget.EditText</item>
+        <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:textViewStyle">@style/Widget.TextView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="android:spinnerItemStyle">@style/Widget.TextView.SpinnerItem</item>
+        <item name="bookmarkThumbnailViewStyle">@style/Widget.BookmarkThumbnailView</item>
+        <item name="topBookmarkItemViewStyle">@style/Widget.TopBookmarkItemView</item>
+        <item name="topBookmarksViewStyle">@style/Widget.TopBookmarksView</item>
     </style>
 
     <style name="Gecko.Preferences" parent="GeckoPreferencesBase"/>
 
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -197,16 +197,17 @@
   <string name="contextmenu_site_settings">&contextmenu_site_settings;</string>
 
   <string name="pref_titlebar_mode">&pref_titlebar_mode;</string>
   <string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string>
   <string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string>
 
   <string name="history_removed">&history_removed;</string>
 
+  <string name="bookmark_add">&bookmark_add;</string>
   <string name="bookmark_edit_title">&bookmark_edit_title;</string>
   <string name="bookmark_edit_name">&bookmark_edit_name;</string>
   <string name="bookmark_edit_location">&bookmark_edit_location;</string>
   <string name="bookmark_edit_keyword">&bookmark_edit_keyword;</string>
 
   <string name="pref_use_master_password">&pref_use_master_password;</string>
   <string name="masterpassword_create_title">&masterpassword_create_title;</string>
   <string name="masterpassword_remove_title">&masterpassword_remove_title;</string>