Initial Contribution
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
new file mode 100644
index 0000000..3b76e75
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Browser;
+import android.provider.Browser.BookmarkColumns;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebIconDatabase;
+import android.webkit.WebIconDatabase.IconListener;
+import android.widget.BaseAdapter;
+
+import java.io.ByteArrayOutputStream;
+
+class BrowserBookmarksAdapter extends BaseAdapter {
+
+    private final String            LOGTAG = "Bookmarks";
+
+    private String                  mCurrentPage;
+    private Cursor                  mCursor;
+    private int                     mCount;
+    private String                  mLastWhereClause;
+    private String[]                mLastSelectionArgs;
+    private String                  mLastOrderBy;
+    private BrowserBookmarksPage    mBookmarksPage;
+    private ContentResolver         mContentResolver;
+    private ChangeObserver          mChangeObserver;
+    private DataSetObserver         mDataSetObserver;
+    private boolean                 mDataValid;
+
+    // When true, this adapter is used to pick a bookmark to create a shortcut
+    private boolean mCreateShortcut;
+    private int mExtraOffset;
+
+    // Implementation of WebIconDatabase.IconListener
+    private class IconReceiver implements IconListener {
+        public void onReceivedIcon(String url, Bitmap icon) {
+            updateBookmarkFavicon(mContentResolver, url, icon);
+        }
+    }
+
+    // Instance of IconReceiver
+    private final IconReceiver mIconReceiver = new IconReceiver();
+
+    /**
+     *  Create a new BrowserBookmarksAdapter.
+     *  @param b        BrowserBookmarksPage that instantiated this.  
+     *                  Necessary so it will adjust its focus
+     *                  appropriately after a search.
+     */
+    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage) {
+        this(b, curPage, false);
+    }
+
+    /**
+     *  Create a new BrowserBookmarksAdapter.
+     *  @param b        BrowserBookmarksPage that instantiated this.
+     *                  Necessary so it will adjust its focus
+     *                  appropriately after a search.
+     */
+    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage,
+            boolean createShortcut) {
+        mDataValid = false;
+        mCreateShortcut = createShortcut;
+        mExtraOffset = createShortcut ? 0 : 1;
+        mBookmarksPage = b;
+        mCurrentPage = b.getResources().getString(R.string.current_page) + 
+                curPage;
+        mContentResolver = b.getContentResolver();
+        mLastOrderBy = Browser.BookmarkColumns.CREATED + " DESC";
+        mChangeObserver = new ChangeObserver();
+        mDataSetObserver = new MyDataSetObserver();
+        // FIXME: Should have a default sort order that the user selects.
+        search(null);
+        // FIXME: This requires another query of the database after the
+        // initial search(null). Can we optimize this?
+        Browser.requestAllIcons(mContentResolver,
+                Browser.BookmarkColumns.FAVICON + " is NULL AND " +
+                Browser.BookmarkColumns.BOOKMARK + " == 1", mIconReceiver);
+    }
+    
+    /**
+     *  Return a hashmap with one row's Title, Url, and favicon.
+     *  @param position  Position in the list.
+     *  @return Bundle  Stores title, url of row position, favicon, and id
+     *                   for the url.  Return a blank map if position is out of
+     *                   range.
+     */
+    public Bundle getRow(int position) {
+        Bundle map = new Bundle();
+        if (position < mExtraOffset || position >= mCount) {
+            return map;
+        }
+        mCursor.moveToPosition(position- mExtraOffset);
+        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
+        map.putString(Browser.BookmarkColumns.TITLE, 
+                mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
+        map.putString(Browser.BookmarkColumns.URL, url);
+        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
+        if (data != null) {
+            map.putParcelable(Browser.BookmarkColumns.FAVICON,
+                    BitmapFactory.decodeByteArray(data, 0, data.length));
+        }
+        map.putInt("id", mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
+        return map;
+    }
+
+    /**
+     *  Update a row in the database with new information. 
+     *  Requeries the database if the information has changed.
+     *  @param map  Bundle storing id, title and url of new information
+     */
+    public void updateRow(Bundle map) {
+
+        // Find the record
+        int id = map.getInt("id");
+        int position = -1;
+        for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
+            if (mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX) == id) {
+                position = mCursor.getPosition();
+                break;
+            }
+        }
+        if (position < 0) {
+            return;
+        }
+
+        mCursor.moveToPosition(position);
+        ContentValues values = new ContentValues();
+        String title = map.getString(Browser.BookmarkColumns.TITLE);
+        if (!title.equals(mCursor
+                .getString(Browser.HISTORY_PROJECTION_TITLE_INDEX))) {
+            values.put(Browser.BookmarkColumns.TITLE, title);
+        }
+        String url = map.getString(Browser.BookmarkColumns.URL);
+        if (!url.equals(mCursor.
+                getString(Browser.HISTORY_PROJECTION_URL_INDEX))) {
+            values.put(Browser.BookmarkColumns.URL, url);
+        }
+        if (values.size() > 0
+                && mContentResolver.update(Browser.BOOKMARKS_URI, values,
+                        "_id = " + id, null) != -1) {
+            refreshList();
+        }
+    }
+
+    /**
+     *  Delete a row from the database.  Requeries the database.  
+     *  Does nothing if the provided position is out of range.
+     *  @param position Position in the list.
+     */
+    public void deleteRow(int position) {
+        if (position < mExtraOffset || position >= getCount()) {
+            return;
+        }
+        mCursor.moveToPosition(position- mExtraOffset);
+        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
+        WebIconDatabase.getInstance().releaseIconForPageUrl(url);
+        Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, mCursor
+                .getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
+        int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
+        if (0 == numVisits) {
+            mContentResolver.delete(uri, null, null);
+        } else {
+            // It is no longer a bookmark, but it is still a visited site.
+            ContentValues values = new ContentValues();
+            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
+            mContentResolver.update(uri, values, null, null);
+        }
+        refreshList();
+    }
+    
+    /**
+     *  Delete all bookmarks from the db. Requeries the database.  
+     *  All bookmarks with become visited URLs or if never visited 
+     *  are removed
+     */
+    public void deleteAllRows() {
+        StringBuilder deleteIds = null;
+        StringBuilder convertIds = null;
+        
+        for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) {
+            String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
+            WebIconDatabase.getInstance().releaseIconForPageUrl(url);
+            int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX);
+            int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
+            if (0 == numVisits) {
+                if (deleteIds == null) {
+                    deleteIds = new StringBuilder();
+                    deleteIds.append("( ");
+                } else {
+                    deleteIds.append(" OR ( ");
+                }
+                deleteIds.append(BookmarkColumns._ID);
+                deleteIds.append(" = ");
+                deleteIds.append(id);
+                deleteIds.append(" )");
+            } else {
+                // It is no longer a bookmark, but it is still a visited site.
+                if (convertIds == null) {
+                    convertIds = new StringBuilder();
+                    convertIds.append("( ");
+                } else {
+                    convertIds.append(" OR ( ");
+                }
+                convertIds.append(BookmarkColumns._ID);
+                convertIds.append(" = ");
+                convertIds.append(id);
+                convertIds.append(" )");
+            }
+        }
+        
+        if (deleteIds != null) {
+            mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(), 
+                null);
+        }
+        if (convertIds != null) {
+            ContentValues values = new ContentValues();
+            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
+            mContentResolver.update(Browser.BOOKMARKS_URI, values, 
+                    convertIds.toString(), null);
+        }
+        refreshList();
+    }
+
+    /**
+     *  Refresh list to recognize a change in the database.
+     */
+    public void refreshList() {
+        // FIXME: consider using requery().
+        // Need to do more work to get it to function though.
+        searchInternal(mLastWhereClause, mLastSelectionArgs, mLastOrderBy);
+    }
+
+    /**
+     *  Search the database for bookmarks that match the input string.
+     *  @param like String to use to search the database.  Strings with spaces 
+     *              are treated as having multiple search terms using the
+     *              OR operator.  Search both the title and url.
+     */
+    public void search(String like) {
+        String whereClause = Browser.BookmarkColumns.BOOKMARK + " == 1";
+        String[] selectionArgs = null;
+        if (like != null) {
+            String[] likes = like.split(" ");
+            int count = 0;
+            boolean firstTerm = true;
+            StringBuilder andClause = new StringBuilder(256);
+            for (int j = 0; j < likes.length; j++) {
+                if (likes[j].length() > 0) {
+                    if (firstTerm) {
+                        firstTerm = false;
+                    } else {
+                        andClause.append(" OR ");
+                    }
+                    andClause.append(Browser.BookmarkColumns.TITLE
+                            + " LIKE ? OR " + Browser.BookmarkColumns.URL
+                            + " LIKE ? ");
+                    count += 2;
+                }
+            }
+            if (count > 0) {
+                selectionArgs = new String[count];
+                count = 0;
+                for (int j = 0; j < likes.length; j++) {
+                    if (likes[j].length() > 0) {
+                        like = "%" + likes[j] + "%";
+                        selectionArgs[count++] = like;
+                        selectionArgs[count++] = like;
+                    }
+                }
+                whereClause += " AND (" + andClause + ")";
+            }
+        }
+        searchInternal(whereClause, selectionArgs, mLastOrderBy);
+    }
+
+    /**
+     * Update the bookmark's favicon.
+     * @param cr The ContentResolver to use.
+     * @param url The url of the bookmark to update.
+     * @param favicon The favicon bitmap to write to the db.
+     */
+    /* package */ static void updateBookmarkFavicon(ContentResolver cr,
+            String url, Bitmap favicon) {
+        if (url == null || favicon == null) {
+            return;
+        }
+        final String[] selArgs = new String[] { url };
+        final String where = Browser.BookmarkColumns.URL + " == ? AND "
+                + Browser.BookmarkColumns.BOOKMARK + " == 1";
+        final String[] projection = new String[] { Browser.BookmarkColumns._ID };
+        final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, where,
+                selArgs, null);
+        boolean succeed = c.moveToFirst();
+        ContentValues values = null;
+        while (succeed) {
+            if (values == null) {
+                final ByteArrayOutputStream os = new ByteArrayOutputStream();
+                favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
+                values = new ContentValues();
+                values.put(Browser.BookmarkColumns.FAVICON, os.toByteArray());
+            }
+            cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, c
+                    .getInt(0)), values, null, null);
+            succeed = c.moveToNext();
+        }
+        c.close();
+    }
+
+    /**
+     *  This sorts alphabetically, with non-capitalized titles before 
+     *  capitalized.
+     */
+    public void sortAlphabetical() {
+        searchInternal(mLastWhereClause, mLastSelectionArgs,
+                Browser.BookmarkColumns.TITLE + " COLLATE UNICODE ASC");
+    }
+
+    /**
+     *  Internal function used in search, sort, and refreshList.
+     */
+    private void searchInternal(String whereClause, String[] selectionArgs,
+            String orderBy) {
+        if (mCursor != null) {
+            mCursor.unregisterContentObserver(mChangeObserver);
+            mCursor.unregisterDataSetObserver(mDataSetObserver);
+            mBookmarksPage.stopManagingCursor(mCursor);
+            mCursor.deactivate();
+        }
+
+        mLastWhereClause = whereClause;
+        mLastSelectionArgs = selectionArgs;
+        mLastOrderBy = orderBy;
+        mCursor = mContentResolver.query(
+            Browser.BOOKMARKS_URI,
+            Browser.HISTORY_PROJECTION,
+            whereClause,
+            selectionArgs, 
+            orderBy);
+        mCursor.registerContentObserver(mChangeObserver);
+        mCursor.registerDataSetObserver(mDataSetObserver);
+        mBookmarksPage.startManagingCursor(mCursor);
+
+        mDataValid = true;
+        notifyDataSetChanged();
+
+        mCount = mCursor.getCount() + mExtraOffset;
+    }
+
+    /**
+     * How many items should be displayed in the list.
+     * @return Count of items.
+     */
+    public int getCount() {
+        if (mDataValid) {
+            return mCount;
+        } else {
+            return 0;
+        }
+    }
+
+    public boolean areAllItemsEnabled() {
+        return true;
+    }
+
+    public boolean isEnabled(int position) {
+        return true;
+    }
+
+    /**
+     * Get the data associated with the specified position in the list.
+     * @param position Index of the item whose data we want.
+     * @return The data at the specified position.
+     */
+    public Object getItem(int position) {
+        return null;
+    }
+
+    /**
+     * Get the row id associated with the specified position in the list.
+     * @param position Index of the item whose row id we want.
+     * @return The id of the item at the specified position.
+     */
+    public long getItemId(int position) {
+        return position;
+    }
+
+    /**
+     * Get a View that displays the data at the specified position
+     * in the list.
+     * @param position Index of the item whose view we want.
+     * @return A View corresponding to the data at the specified position.
+     */
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (!mDataValid) {
+            throw new IllegalStateException(
+                    "this should only be called when the cursor is valid");
+        }
+        if (position < 0 || position > mCount) {
+            throw new AssertionError(
+                    "BrowserBookmarksAdapter tried to get a view out of range");
+        }
+        if (position == 0 && !mCreateShortcut) {
+            AddNewBookmark b;
+            if (convertView instanceof AddNewBookmark) {
+                b = (AddNewBookmark) convertView;
+            } else {
+                b = new AddNewBookmark(mBookmarksPage);
+            }
+            b.setUrl(mCurrentPage);
+            return b;
+        }
+        if (convertView == null || convertView instanceof AddNewBookmark) {
+            convertView = new BookmarkItem(mBookmarksPage);
+        }
+        bind((BookmarkItem)convertView, position);
+        return convertView;
+    }
+
+    /**
+     *  Return the title for this item in the list.
+     */
+    public String getTitle(int position) {
+        return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position);
+    }
+
+    /**
+     *  Return the Url for this item in the list.
+     */
+    public String getUrl(int position) {
+        return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position);
+    }
+
+    /**
+     * Private helper function to return the title or url.
+     */
+    private String getString(int cursorIndex, int position) {
+        if (position < mExtraOffset || position > mCount) {
+            return "";
+        }
+        mCursor.moveToPosition(position- mExtraOffset);
+        return mCursor.getString(cursorIndex);
+    }
+
+    private void bind(BookmarkItem b, int position) {
+        mCursor.moveToPosition(position- mExtraOffset);
+
+        String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX);
+        if (title.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
+            title = title.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
+        }
+        b.setName(title);
+        String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
+        if (url.length() > BrowserSettings.MAX_TEXTVIEW_LEN) {
+            url = url.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN);
+        }
+        b.setUrl(url);
+        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
+        if (data != null) {
+            b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length));
+        } else {
+            b.setFavicon(null);
+        }
+    }
+
+    private class ChangeObserver extends ContentObserver {
+        public ChangeObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            refreshList();
+        }
+    }
+    
+    private class MyDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            mDataValid = true;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            mDataValid = false;
+            notifyDataSetInvalidated();
+        }
+    }
+}