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