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();
+ }
+ }
+}