Bug 885884: Add back pin bookmark support to TopBookmarksView. [r=lucasr]
authorSriram Ramasubramanian <sriram@mozilla.com>
Mon, 22 Jul 2013 12:49:36 -0700
changeset 143424 8e852b2a313a362dbdc2aa14dd1912ee474a334c
parent 143423 2c8f72dc409c2a813f593aa1fd9fd0b1168d323b
child 143425 bd393061461ab1af9217eb8c0ddd320d1dcf075a
push id25130
push userlrocha@mozilla.com
push dateWed, 21 Aug 2013 09:41:27 +0000
treeherdermozilla-central@b2486721572e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs885884
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 885884: Add back pin bookmark support to TopBookmarksView. [r=lucasr]
mobile/android/base/Makefile.in
mobile/android/base/home/BookmarksPage.java
mobile/android/base/home/BrowserSearch.java
mobile/android/base/home/PinBookmarkDialog.java
mobile/android/base/home/SearchLoader.java
mobile/android/base/home/TopBookmarksView.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/resources/layout/pin_bookmark_dialog.xml
mobile/android/base/strings.xml.in
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -224,19 +224,21 @@ FENNEC_JAVA_FILES = \
   home/HistoryPage.java \
   home/HomeFragment.java \
   home/HomeListView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
   home/FadedTextView.java \
   home/FaviconsLoader.java \
   home/LastTabsPage.java \
+  home/PinBookmarkDialog.java \
   home/ReadingListPage.java \
   home/SearchEngine.java \
   home/SearchEngineRow.java \
+  home/SearchLoader.java \
   home/SimpleCursorLoader.java \
   home/SuggestClient.java \
   home/TopBookmarkItemView.java \
   home/TopBookmarksAdapter.java \
   home/TopBookmarksView.java \
   home/TwoLinePageRow.java \
   home/VisitedPage.java \
   menu/GeckoMenu.java \
@@ -468,16 +470,17 @@ RES_LAYOUT = \
   res/layout/launch_app_list.xml \
   res/layout/launch_app_listitem.xml \
   res/layout/menu_action_bar.xml \
   res/layout/menu_item_action_view.xml \
   res/layout/menu_popup.xml \
   res/layout/notification_icon_text.xml \
   res/layout/notification_progress.xml \
   res/layout/notification_progress_text.xml \
+  res/layout/pin_bookmark_dialog.xml \
   res/layout/preference_rightalign_icon.xml \
   res/layout/search_engine_row.xml \
   res/layout/site_setting_item.xml \
   res/layout/site_setting_title.xml \
   res/layout/shared_ui_components.xml \
   res/layout/site_identity.xml \
   res/layout/suggestion_item.xml \
   res/layout/remote_tabs_child.xml \
--- a/mobile/android/base/home/BookmarksPage.java
+++ b/mobile/android/base/home/BookmarksPage.java
@@ -9,24 +9,29 @@ import org.mozilla.gecko.Favicons;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+import org.mozilla.gecko.home.PinBookmarkDialog.OnBookmarkSelectedListener;
+import org.mozilla.gecko.home.TopBookmarksView.OnPinBookmarkListener;
 import org.mozilla.gecko.home.TopBookmarksView.Thumbnail;
+import org.mozilla.gecko.util.ThreadUtils;
 
+import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -71,16 +76,19 @@ public class BookmarksPage extends HomeF
     private TopBookmarksAdapter mTopBookmarksAdapter;
 
     // Callback for cursor loaders.
     private CursorLoaderCallbacks mLoaderCallbacks;
 
     // Callback for thumbnail loader.
     private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
 
+    // Listener for pinning bookmarks.
+    private PinBookmarkListener mPinBookmarkListener;
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         BookmarksListView list = (BookmarksListView) inflater.inflate(R.layout.home_bookmarks_page, container, false);
 
         mTopBookmarks = new TopBookmarksView(getActivity());
         list.addHeaderView(mTopBookmarks);
 
         return list;
@@ -93,22 +101,25 @@ public class BookmarksPage extends HomeF
         OnUrlOpenListener listener = null;
         try {
             listener = (OnUrlOpenListener) getActivity();
         } catch (ClassCastException e) {
             throw new ClassCastException(getActivity().toString()
                     + " must implement HomePager.OnUrlOpenListener");
         }
 
+        mPinBookmarkListener = new PinBookmarkListener();
+
         mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
         mList.setOnUrlOpenListener(listener);
 
         registerForContextMenu(mList);
 
         mTopBookmarks.setOnUrlOpenListener(listener);
+        mTopBookmarks.setOnPinBookmarkListener(mPinBookmarkListener);
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         // Setup the top bookmarks adapter.
         mTopBookmarksAdapter = new TopBookmarksAdapter(getActivity(), null);
@@ -138,16 +149,17 @@ public class BookmarksPage extends HomeF
     }
 
     @Override
     public void onDestroyView() {
         mList = null;
         mListAdapter = null;
         mTopBookmarks = null;
         mTopBookmarksAdapter = null;
+        mPinBookmarkListener = null;
         super.onDestroyView();
     }
 
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
 
         // Reattach the fragment, forcing a reinflation of its view.
@@ -162,16 +174,54 @@ public class BookmarksPage extends HomeF
             getFragmentManager().beginTransaction()
                                 .detach(this)
                                 .attach(this)
                                 .commitAllowingStateLoss();
         }
     }
 
     /**
+     * Listener for pinning bookmarks.
+     */
+    private class PinBookmarkListener implements OnPinBookmarkListener,
+                                                 OnBookmarkSelectedListener {
+        // Tag for the PinBookmarkDialog fragment.
+        private static final String TAG_PIN_BOOKMARK = "pin_bookmark";
+
+        // Position of the pin.
+        private int mPosition;
+
+        @Override
+        public void onPinBookmark(int position) {
+            mPosition = position;
+
+            final FragmentManager manager = getActivity().getSupportFragmentManager();
+            PinBookmarkDialog dialog = (PinBookmarkDialog) manager.findFragmentByTag(TAG_PIN_BOOKMARK);
+            if (dialog == null) {
+                dialog = PinBookmarkDialog.newInstance();
+            }
+
+            dialog.setOnBookmarkSelectedListener(this);
+            dialog.show(manager, TAG_PIN_BOOKMARK);
+        }
+
+        @Override
+        public void onBookmarkSelected(final String url, final String title) {
+            final int position = mPosition;
+            final Context context = getActivity().getApplicationContext();
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                    BrowserDB.pinSite(context.getContentResolver(), url, title, position);
+                }
+            });
+        }
+    }
+
+    /**
      * Loader for the list for bookmarks.
      */
     private static class BookmarksLoader extends SimpleCursorLoader {
         private final int mFolderId;
 
         public BookmarksLoader(Context context) {
             this(context, Bookmarks.FIXED_ROOT_ID);
         }
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -6,53 +6,45 @@
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.AutocompleteHandler;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
-import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
+import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.widget.FaviconView;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.SimpleCursorAdapter;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.LayoutInflater;
 import android.widget.AdapterView;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
 import android.widget.ListView;
-import android.widget.TextView;
 
 import java.util.ArrayList;
 import java.util.List;
 
 /**
  * Fragment that displays frecency search results in a ListView.
  */
 public class BrowserSearch extends HomeFragment
@@ -395,48 +387,21 @@ public class BrowserSearch extends HomeF
         mAutocompleteHandler = handler;
 
         if (isVisible()) {
             // The adapter depends on the search term to determine its number
             // of items. Make it we notify the view about it.
             mAdapter.notifyDataSetChanged();
 
             // Restart loaders with the new search term
-            getLoaderManager().restartLoader(SEARCH_LOADER_ID, null, mCursorLoaderCallbacks);
+            SearchLoader.restart(getLoaderManager(), SEARCH_LOADER_ID, mCursorLoaderCallbacks, mSearchTerm, false);
             filterSuggestions();
         }
     }
 
-    private static class SearchCursorLoader extends SimpleCursorLoader {
-        // Max number of search results
-        private static final int SEARCH_LIMIT = 100;
-
-        // The target search term associated with the loader
-        private final String mSearchTerm;
-
-        public SearchCursorLoader(Context context, String searchTerm) {
-            super(context);
-            mSearchTerm = searchTerm;
-        }
-
-        @Override
-        public Cursor loadCursor() {
-            if (TextUtils.isEmpty(mSearchTerm)) {
-                return null;
-            }
-
-            final ContentResolver cr = getContext().getContentResolver();
-            return BrowserDB.filter(cr, mSearchTerm, SEARCH_LIMIT);
-        }
-
-        public String getSearchTerm() {
-            return mSearchTerm;
-        }
-    }
-
     private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> {
         private final SuggestClient mSuggestClient;
         private final String mSearchTerm;
         private ArrayList<String> mSuggestions;
 
         public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
             super(context);
             mSuggestClient = suggestClient;
@@ -611,17 +576,17 @@ public class BrowserSearch extends HomeF
         }
     }
 
     private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
             switch(id) {
             case SEARCH_LOADER_ID:
-                return new SearchCursorLoader(getActivity(), mSearchTerm);
+                return SearchLoader.createInstance(getActivity(), args);
 
             case FAVICONS_LOADER_ID:
                 return FaviconsLoader.createInstance(getActivity(), args);
             }
 
             return null;
         }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/PinBookmarkDialog.java
@@ -0,0 +1,223 @@
+/* -*- 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.db.BrowserDB.URLColumns;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.EditText;
+import android.widget.ListView;
+
+/**
+ * Dialog fragment that displays frecency search results, for pinning as a bookmark, in a ListView.
+ */
+class PinBookmarkDialog extends DialogFragment {
+    // Listener for url selection
+    public static interface OnBookmarkSelectedListener {
+        public void onBookmarkSelected(String url, String title);
+    }
+
+    // Cursor loader ID for search query
+    private static final int SEARCH_LOADER_ID = 0;
+
+    // Cursor loader ID for favicons query
+    private static final int FAVICONS_LOADER_ID = 1;
+
+    // Holds the current search term to use in the query
+    private String mSearchTerm;
+
+    // Adapter for the list of search results
+    private SearchAdapter mAdapter;
+
+    // Search entry
+    private EditText mSearch;
+
+    // Search results
+    private ListView mList;
+
+    // Callbacks used for the search and favicon cursor loaders
+    private CursorLoaderCallbacks mLoaderCallbacks;
+
+    // Bookmark selected listener
+    private OnBookmarkSelectedListener mOnBookmarkSelectedListener;
+
+    public static PinBookmarkDialog newInstance() {
+        return new PinBookmarkDialog();
+    }
+
+    private PinBookmarkDialog() {
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Holo_Light_Dialog);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle 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.
+        return inflater.inflate(R.layout.pin_bookmark_dialog, container, false);
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        mSearch = (EditText) view.findViewById(R.id.search);
+        mSearch.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void afterTextChanged(Editable s) {
+            }
+
+            @Override
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            @Override
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+                filter(mSearch.getText().toString());
+            }
+        });
+
+        mList = (HomeListView) view.findViewById(R.id.list);
+        mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                if (mOnBookmarkSelectedListener != null) {
+                    final Cursor c = mAdapter.getCursor();
+                    if (c == null || !c.moveToPosition(position)) {
+                        return;
+                    }
+
+                    final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));
+                    final String title = c.getString(c.getColumnIndexOrThrow(URLColumns.TITLE));
+                    mOnBookmarkSelectedListener.onBookmarkSelected(url, title);
+                }
+
+                // Dismiss the fragment and the dialog.
+                dismiss();
+            }
+        });
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+
+        // Initialize the search adapter
+        mAdapter = new SearchAdapter(getActivity());
+        mList.setAdapter(mAdapter);
+
+        // Create callbacks before the initial loader is started
+        mLoaderCallbacks = new CursorLoaderCallbacks();
+
+        // Reconnect to the loader only if present
+        getLoaderManager().initLoader(SEARCH_LOADER_ID, null, mLoaderCallbacks);
+
+        // Default filter.
+        filter("");
+    }
+
+    private void filter(String searchTerm) {
+        if (!TextUtils.isEmpty(searchTerm) &&
+            TextUtils.equals(mSearchTerm, searchTerm)) {
+            return;
+        }
+
+        mSearchTerm = searchTerm;
+
+        // Restart loaders with the new search term
+        SearchLoader.restart(getLoaderManager(), SEARCH_LOADER_ID, mLoaderCallbacks, mSearchTerm);
+    }
+
+    public void setOnBookmarkSelectedListener(OnBookmarkSelectedListener listener) {
+        mOnBookmarkSelectedListener = listener;
+    }
+
+    private static class SearchAdapter extends CursorAdapter {
+        private LayoutInflater mInflater;
+
+        public SearchAdapter(Context context) {
+            super(context, null);
+            mInflater = LayoutInflater.from(context);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            TwoLinePageRow row = (TwoLinePageRow) view;
+            row.updateFromCursor(cursor);
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            return (TwoLinePageRow) mInflater.inflate(R.layout.home_item_row, parent, false);
+        }
+    }
+
+    private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            switch(id) {
+            case SEARCH_LOADER_ID:
+                return SearchLoader.createInstance(getActivity(), args);
+
+            case FAVICONS_LOADER_ID:
+                return FaviconsLoader.createInstance(getActivity(), args);
+            }
+
+            return null;
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
+            final int loaderId = loader.getId();
+            switch(loaderId) {
+            case SEARCH_LOADER_ID:
+                mAdapter.swapCursor(c);
+
+                FaviconsLoader.restartFromCursor(getLoaderManager(), FAVICONS_LOADER_ID,
+                        mLoaderCallbacks, c);
+                break;
+
+            case FAVICONS_LOADER_ID:
+                // Force the list to use the in-memory favicons.
+                mAdapter.notifyDataSetChanged();
+                break;
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {
+            final int loaderId = loader.getId();
+            switch(loaderId) {
+            case SEARCH_LOADER_ID:
+                mAdapter.swapCursor(null);
+                break;
+
+            case FAVICONS_LOADER_ID:
+                // Do nothing
+                break;
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/SearchLoader.java
@@ -0,0 +1,84 @@
+/* -*- 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.db.BrowserDB;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.Loader;
+import android.text.TextUtils;
+
+/**
+ * Encapsulates the implementation of the search cursor loader.
+ */
+class SearchLoader {
+    // Key for search terms
+    private static final String KEY_SEARCH_TERM = "search_term";
+
+    // Key for performing empty search
+    private static final String KEY_PERFORM_EMPTY_SEARCH = "perform_empty_search";
+
+    private SearchLoader() {
+    }
+
+    public static Loader<Cursor> createInstance(Context context, Bundle args) {
+        if (args != null) {
+            final String searchTerm = args.getString(KEY_SEARCH_TERM, "");
+            final boolean performEmptySearch = args.getBoolean(KEY_PERFORM_EMPTY_SEARCH, false);
+            return new SearchCursorLoader(context, searchTerm, performEmptySearch);
+        } else {
+            return new SearchCursorLoader(context, "", false);
+        }
+    }
+
+    public static void restart(LoaderManager manager, int loaderId,
+                               LoaderCallbacks<Cursor> callbacks, String searchTerm) {
+        restart(manager, loaderId, callbacks, searchTerm, true);
+    }
+
+    public static void restart(LoaderManager manager, int loaderId,
+                               LoaderCallbacks<Cursor> callbacks, String searchTerm, boolean performEmptySearch) {
+        Bundle bundle = new Bundle();
+        bundle.putString(SearchLoader.KEY_SEARCH_TERM, searchTerm);
+        bundle.putBoolean(SearchLoader.KEY_PERFORM_EMPTY_SEARCH, performEmptySearch);
+        manager.restartLoader(loaderId, bundle, callbacks);
+    }
+
+    public static class SearchCursorLoader extends SimpleCursorLoader {
+        // Max number of search results
+        private static final int SEARCH_LIMIT = 100;
+
+        // The target search term associated with the loader
+        private final String mSearchTerm;
+
+        // An empty search on the DB
+        private final boolean mPerformEmptySearch;
+
+        public SearchCursorLoader(Context context, String searchTerm, boolean performEmptySearch) {
+            super(context);
+            mSearchTerm = searchTerm;
+            mPerformEmptySearch = performEmptySearch;
+        }
+
+        @Override
+        public Cursor loadCursor() {
+            if (!mPerformEmptySearch && TextUtils.isEmpty(mSearchTerm)) {
+                return null;
+            }
+
+            return BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT);
+        }
+
+        public String getSearchTerm() {
+            return mSearchTerm;
+        }
+    }
+
+}
--- a/mobile/android/base/home/TopBookmarksView.java
+++ b/mobile/android/base/home/TopBookmarksView.java
@@ -22,25 +22,33 @@ 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";
 
+    // Listener for pinning bookmarks.
+    public static interface OnPinBookmarkListener {
+        public void onPinBookmark(int position);
+    }
+
     // Max number of bookmarks that needs to be shown.
     private final int mMaxBookmarks;
 
     // Number of columns to show.
     private final int mNumColumns;
 
     // On URL open listener.
     private OnUrlOpenListener mUrlOpenListener;
 
+    // Pin bookmark listener.
+    private OnPinBookmarkListener mPinBookmarkListener;
+
     // Temporary cache to store the thumbnails until the next layout pass.
     private Map<String, Thumbnail> mThumbnailsCache;
 
     /**
      *  Class to hold the bitmap of cached thumbnails/favicons.
      */
     public static class Thumbnail {
         // Thumbnail or favicon.
@@ -78,18 +86,26 @@ public class TopBookmarksView extends Gr
         super.onAttachedToWindow();
 
         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);
+                // If the url is empty, the user can pin a bookmark.
+                // If not, navigate to the page given by the url.
+                if (!TextUtils.isEmpty(url)) {
+                    if (mUrlOpenListener != null) {
+                        mUrlOpenListener.onUrlOpen(url);
+                    }
+                } else {
+                    if (mPinBookmarkListener != null) {
+                        mPinBookmarkListener.onPinBookmark(position);
+                    }
                 }
             }
         });
     }
 
     /**
      * {@inheritDoc}
      */
@@ -171,16 +187,25 @@ public class TopBookmarksView extends Gr
      *
      * @param listener An url open listener for this view.
      */
     public void setOnUrlOpenListener(OnUrlOpenListener listener) {
         mUrlOpenListener = listener;
     }
 
     /**
+     * Set a pin bookmark listener to be used by this view.
+     *
+     * @param listener A pin bookmark listener for this view.
+     */
+    public void setOnPinBookmarkListener(OnPinBookmarkListener listener) {
+        mPinBookmarkListener = listener;
+    }
+
+    /**
      * Update the thumbnails returned by the db.
      *
      * @param thumbnails A map of urls and their thumbnail bitmaps.
      */
     public void updateThumbnails(Map<String, Thumbnail> thumbnails) {
         if (thumbnails == null) {
             return;
         }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -264,16 +264,17 @@ size. -->
 <!ENTITY button_no "No">
 <!ENTITY button_clear_data "Clear data">
 <!ENTITY button_set "Set">
 <!ENTITY button_clear "Clear">
 
 <!ENTITY home_last_tabs_title "Your tabs from last time">
 <!ENTITY home_last_tabs_open "Open all tabs from last time">
 <!ENTITY home_visited_empty "Websites you visited go here">
+<!ENTITY pin_bookmark_dialog_hint "Enter a search keyword">
 
 <!ENTITY filepicker_title "Choose File">
 <!ENTITY filepicker_audio_title "Choose or record a sound">
 <!ENTITY filepicker_image_title "Choose or take a picture">
 <!ENTITY filepicker_video_title "Choose or record a video">
 
 <!-- Site identity popup -->
 <!ENTITY identity_connected_to "You are connected to">
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/pin_bookmark_dialog.xml
@@ -0,0 +1,43 @@
+<?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/. -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="wrap_content"
+              android:orientation="vertical">
+
+    <LinearLayout android:layout_width="fill_parent"
+                  android:layout_height="@dimen/browser_toolbar_height"
+                  android:orientation="vertical"
+                  android:background="@color/background_normal"
+                  android:padding="4dip">
+
+        <EditText android:id="@+id/search"
+                  style="@style/UrlBar.Button"
+                  android:layout_width="fill_parent"
+                  android:layout_height="fill_parent"
+                  android:padding="6dip"
+                  android:hint="@string/pin_bookmark_dialog_hint"
+                  android:background="@drawable/url_bar_entry"
+                  android:textColor="@color/url_bar_title"
+                  android:textColorHint="@color/url_bar_title_hint"
+                  android:textColorHighlight="@color/url_bar_text_highlight"
+                  android:textSelectHandle="@drawable/handle_middle"
+                  android:textSelectHandleLeft="@drawable/handle_start"
+                  android:textSelectHandleRight="@drawable/handle_end"
+                  android:textCursorDrawable="@null"
+                  android:inputType="textUri|textNoSuggestions"
+                  android:imeOptions="actionSearch|flagNoExtractUi|flagNoFullscreen"
+                  android:singleLine="true"
+                  android:gravity="center_vertical|left"/>
+
+    </LinearLayout>
+
+    <org.mozilla.gecko.home.HomeListView android:id="@+id/list"
+                                         android:layout_width="fill_parent"
+                                         android:layout_height="0dip"
+                                         android:layout_weight="1.0"/>
+
+</LinearLayout>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -232,16 +232,17 @@
   <string name="button_set">&button_set;</string>
   <string name="button_clear">&button_clear;</string>
   <string name="button_yes">&button_yes;</string>
   <string name="button_no">&button_no;</string>
 
   <string name="home_last_tabs_title">&home_last_tabs_title;</string>
   <string name="home_last_tabs_open">&home_last_tabs_open;</string>
   <string name="home_visited_empty">&home_visited_empty;</string>
+  <string name="pin_bookmark_dialog_hint">&pin_bookmark_dialog_hint;</string>
 
   <string name="filepicker_title">&filepicker_title;</string>
   <string name="filepicker_audio_title">&filepicker_audio_title;</string>
   <string name="filepicker_image_title">&filepicker_image_title;</string>
   <string name="filepicker_video_title">&filepicker_video_title;</string>
 
   <!-- Default bookmarks. Use bookmarks titles shared with XUL from mobile's
        profile/bookmarks.inc. Don't expose the URLs to L10N. -->