/* * 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.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.webkit.WebIconDatabase; import android.webkit.WebIconDatabase.IconListener; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import java.io.ByteArrayOutputStream; class BrowserBookmarksAdapter extends BaseAdapter { private String mCurrentPage; private String mCurrentTitle; private Cursor mCursor; private int mCount; private BrowserBookmarksPage mBookmarksPage; private ContentResolver mContentResolver; private boolean mDataValid; private boolean mGridMode; // 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, String curTitle, boolean createShortcut) { mDataValid = false; mCreateShortcut = createShortcut; mExtraOffset = createShortcut ? 0 : 1; mBookmarksPage = b; mCurrentPage = b.getResources().getString(R.string.current_page) + curPage; mCurrentTitle = curTitle; mContentResolver = b.getContentResolver(); mGridMode = false; // FIXME: Should have a default sort order that the user selects. String whereClause = Browser.BookmarkColumns.BOOKMARK + " != 0"; String orderBy = Browser.BookmarkColumns.VISITS + " DESC"; mCursor = b.managedQuery(Browser.BOOKMARKS_URI, Browser.HISTORY_PROJECTION, whereClause, null, orderBy); mCursor.registerContentObserver(new ChangeObserver()); mCursor.registerDataSetObserver(new MyDataSetObserver()); mDataValid = true; notifyDataSetChanged(); mCount = mCursor.getCount() + mExtraOffset; // FIXME: This requires another query of the database after the // managedQuery. 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); Bookmarks.removeFromBookmarks(null, mContentResolver, url); 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() { mCursor.requery(); mCount = mCursor.getCount() + mExtraOffset; notifyDataSetChanged(); } /** * 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(); } /** * 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; } /* package */ void switchViewMode(boolean toGrid) { mGridMode = toGrid; } /* package */ void populateBookmarkItem(BookmarkItem b, int position) { mCursor.moveToPosition(position - mExtraOffset); b.setUrl(mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX)); b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); Bitmap bitmap = (null == data) ? null : BitmapFactory.decodeByteArray(data, 0, data.length); b.setFavicon(bitmap); } /** * 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 (mGridMode) { if (convertView == null || convertView instanceof AddNewBookmark || convertView instanceof BookmarkItem) { LayoutInflater factory = LayoutInflater.from(mBookmarksPage); convertView = factory.inflate(R.layout.bookmark_thumbnail, null); } View holder = convertView.findViewById(R.id.holder); ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb); ImageView fav = (ImageView) convertView.findViewById(R.id.fav); TextView tv = (TextView) convertView.findViewById(R.id.label); if (0 == position && !mCreateShortcut) { // This is to create a bookmark for the current page. holder.setVisibility(View.VISIBLE); fav.setVisibility(View.GONE); tv.setText(mCurrentTitle); // FIXME: Want to show the screenshot of the current page thumb.setImageResource(R.drawable.blank); return convertView; } holder.setVisibility(View.GONE); mCursor.moveToPosition(position - mExtraOffset); tv.setText(mCursor.getString( Browser.HISTORY_PROJECTION_TITLE_INDEX)); byte[] data = mCursor.getBlob( Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX); if (data == null) { // Backup is to just show white thumb.setImageResource(R.drawable.blank); } else { thumb.setImageBitmap( BitmapFactory.decodeByteArray(data, 0, data.length)); } // Now show the favicon data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); if (data == null) { fav.setVisibility(View.GONE); } else { fav.setVisibility(View.VISIBLE); fav.setImageBitmap( BitmapFactory.decodeByteArray(data, 0, data.length)); } return convertView; } 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 BookmarkItem)) { 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(); } } }