FileDocCategorySizeDatePackage
BrowserBookmarksAdapter.javaAPI DocAndroid 1.5 API20379Wed May 06 22:42:42 BST 2009com.android.browser

BrowserBookmarksAdapter.java

/*
 * 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;
        }
        // Strip the query.
        int query = url.indexOf('?');
        String noQuery = url;
        if (query != -1) {
            noQuery = url.substring(0, query);
        }
        url = noQuery + '?';
        // Use noQuery to search for the base url (i.e. if the url is
        // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com)
        // Use url to match the base url with other queries (i.e. if the url is
        // http://www.google.com/m, search for
        // http://www.google.com/m?some_query)
        final String[] selArgs = new String[] { noQuery, url };
        final String where = "(" + Browser.BookmarkColumns.URL + " == ? OR "
                + Browser.BookmarkColumns.URL + " GLOB ? || '*') 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);
    }

    /**
     * Return the favicon for this item in the list.
     */
    public Bitmap getFavicon(int position) {
        if (position < mExtraOffset || position > mCount) {
            return null;
        }
        mCursor.moveToPosition(position - mExtraOffset);
        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
        if (data == null) {
            return null;
        }
        return BitmapFactory.decodeByteArray(data, 0, data.length);
    }

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