BrowserBookmarksAdapter.java revision 3918d4443ff38ef1870e02aa51a8b29f8352bb1a
1/* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.browser; 18 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.ContentValues; 22import android.database.ContentObserver; 23import android.database.Cursor; 24import android.database.DataSetObserver; 25import android.graphics.Bitmap; 26import android.graphics.BitmapFactory; 27import android.net.Uri; 28import android.os.Bundle; 29import android.os.Handler; 30import android.provider.Browser; 31import android.provider.Browser.BookmarkColumns; 32import android.view.KeyEvent; 33import android.view.LayoutInflater; 34import android.view.View; 35import android.view.ViewGroup; 36import android.webkit.WebIconDatabase; 37import android.webkit.WebIconDatabase.IconListener; 38import android.webkit.WebView; 39import android.widget.BaseAdapter; 40import android.widget.ImageView; 41import android.widget.TextView; 42 43import java.io.ByteArrayOutputStream; 44 45class BrowserBookmarksAdapter extends BaseAdapter { 46 47 private String mCurrentPage; 48 private String mCurrentTitle; 49 private Cursor mCursor; 50 private int mCount; 51 private BrowserBookmarksPage mBookmarksPage; 52 private ContentResolver mContentResolver; 53 private boolean mDataValid; 54 private boolean mGridMode; 55 56 // When true, this adapter is used to pick a bookmark to create a shortcut 57 private boolean mCreateShortcut; 58 private int mExtraOffset; 59 60 // Implementation of WebIconDatabase.IconListener 61 private class IconReceiver implements IconListener { 62 public void onReceivedIcon(String url, Bitmap icon) { 63 updateBookmarkFavicon(mContentResolver, null, url, icon); 64 } 65 } 66 67 // Instance of IconReceiver 68 private final IconReceiver mIconReceiver = new IconReceiver(); 69 70 /** 71 * Create a new BrowserBookmarksAdapter. 72 * @param b BrowserBookmarksPage that instantiated this. 73 * Necessary so it will adjust its focus 74 * appropriately after a search. 75 */ 76 public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage, 77 String curTitle, boolean createShortcut) { 78 mDataValid = false; 79 mCreateShortcut = createShortcut; 80 mExtraOffset = createShortcut ? 0 : 1; 81 mBookmarksPage = b; 82 mCurrentPage = b.getResources().getString(R.string.current_page) 83 + curPage; 84 mCurrentTitle = curTitle; 85 mContentResolver = b.getContentResolver(); 86 mGridMode = false; 87 88 // FIXME: Should have a default sort order that the user selects. 89 String whereClause = Browser.BookmarkColumns.BOOKMARK + " != 0"; 90 String orderBy = Browser.BookmarkColumns.VISITS + " DESC"; 91 mCursor = b.managedQuery(Browser.BOOKMARKS_URI, 92 Browser.HISTORY_PROJECTION, whereClause, null, orderBy); 93 mCursor.registerContentObserver(new ChangeObserver()); 94 mCursor.registerDataSetObserver(new MyDataSetObserver()); 95 96 mDataValid = true; 97 notifyDataSetChanged(); 98 99 mCount = mCursor.getCount() + mExtraOffset; 100 101 // FIXME: This requires another query of the database after the 102 // managedQuery. Can we optimize this? 103 Browser.requestAllIcons(mContentResolver, 104 Browser.BookmarkColumns.FAVICON + " is NULL AND " + 105 Browser.BookmarkColumns.BOOKMARK + " == 1", mIconReceiver); 106 } 107 108 /** 109 * Return a hashmap with one row's Title, Url, and favicon. 110 * @param position Position in the list. 111 * @return Bundle Stores title, url of row position, favicon, and id 112 * for the url. Return a blank map if position is out of 113 * range. 114 */ 115 public Bundle getRow(int position) { 116 Bundle map = new Bundle(); 117 if (position < mExtraOffset || position >= mCount) { 118 return map; 119 } 120 mCursor.moveToPosition(position- mExtraOffset); 121 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 122 map.putString(Browser.BookmarkColumns.TITLE, 123 mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); 124 map.putString(Browser.BookmarkColumns.URL, url); 125 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 126 if (data != null) { 127 map.putParcelable(Browser.BookmarkColumns.FAVICON, 128 BitmapFactory.decodeByteArray(data, 0, data.length)); 129 } 130 map.putInt("id", mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); 131 return map; 132 } 133 134 /** 135 * Update a row in the database with new information. 136 * Requeries the database if the information has changed. 137 * @param map Bundle storing id, title and url of new information 138 */ 139 public void updateRow(Bundle map) { 140 141 // Find the record 142 int id = map.getInt("id"); 143 int position = -1; 144 for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { 145 if (mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX) == id) { 146 position = mCursor.getPosition(); 147 break; 148 } 149 } 150 if (position < 0) { 151 return; 152 } 153 154 mCursor.moveToPosition(position); 155 ContentValues values = new ContentValues(); 156 String title = map.getString(Browser.BookmarkColumns.TITLE); 157 if (!title.equals(mCursor 158 .getString(Browser.HISTORY_PROJECTION_TITLE_INDEX))) { 159 values.put(Browser.BookmarkColumns.TITLE, title); 160 } 161 String url = map.getString(Browser.BookmarkColumns.URL); 162 if (!url.equals(mCursor. 163 getString(Browser.HISTORY_PROJECTION_URL_INDEX))) { 164 values.put(Browser.BookmarkColumns.URL, url); 165 } 166 if (values.size() > 0 167 && mContentResolver.update(Browser.BOOKMARKS_URI, values, 168 "_id = " + id, null) != -1) { 169 refreshList(); 170 } 171 } 172 173 /** 174 * Delete a row from the database. Requeries the database. 175 * Does nothing if the provided position is out of range. 176 * @param position Position in the list. 177 */ 178 public void deleteRow(int position) { 179 if (position < mExtraOffset || position >= getCount()) { 180 return; 181 } 182 mCursor.moveToPosition(position- mExtraOffset); 183 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 184 Bookmarks.removeFromBookmarks(null, mContentResolver, url); 185 refreshList(); 186 } 187 188 /** 189 * Delete all bookmarks from the db. Requeries the database. 190 * All bookmarks with become visited URLs or if never visited 191 * are removed 192 */ 193 public void deleteAllRows() { 194 StringBuilder deleteIds = null; 195 StringBuilder convertIds = null; 196 197 for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { 198 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 199 WebIconDatabase.getInstance().releaseIconForPageUrl(url); 200 int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX); 201 int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); 202 if (0 == numVisits) { 203 if (deleteIds == null) { 204 deleteIds = new StringBuilder(); 205 deleteIds.append("( "); 206 } else { 207 deleteIds.append(" OR ( "); 208 } 209 deleteIds.append(BookmarkColumns._ID); 210 deleteIds.append(" = "); 211 deleteIds.append(id); 212 deleteIds.append(" )"); 213 } else { 214 // It is no longer a bookmark, but it is still a visited site. 215 if (convertIds == null) { 216 convertIds = new StringBuilder(); 217 convertIds.append("( "); 218 } else { 219 convertIds.append(" OR ( "); 220 } 221 convertIds.append(BookmarkColumns._ID); 222 convertIds.append(" = "); 223 convertIds.append(id); 224 convertIds.append(" )"); 225 } 226 } 227 228 if (deleteIds != null) { 229 mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(), 230 null); 231 } 232 if (convertIds != null) { 233 ContentValues values = new ContentValues(); 234 values.put(Browser.BookmarkColumns.BOOKMARK, 0); 235 mContentResolver.update(Browser.BOOKMARKS_URI, values, 236 convertIds.toString(), null); 237 } 238 refreshList(); 239 } 240 241 /** 242 * Refresh list to recognize a change in the database. 243 */ 244 public void refreshList() { 245 mCursor.requery(); 246 mCount = mCursor.getCount() + mExtraOffset; 247 notifyDataSetChanged(); 248 } 249 250 /** 251 * Update the bookmark's favicon. This is a convenience method for updating 252 * a bookmark favicon for the originalUrl and url of the passed in WebView. 253 * @param cr The ContentResolver to use. 254 * @param WebView The WebView containing the url to update. 255 * @param favicon The favicon bitmap to write to the db. 256 */ 257 /* package */ static void updateBookmarkFavicon(ContentResolver cr, 258 WebView view, Bitmap favicon) { 259 if (view != null) { 260 updateBookmarkFavicon(cr, view.getOriginalUrl(), view.getUrl(), 261 favicon); 262 } 263 } 264 265 private static void updateBookmarkFavicon(ContentResolver cr, 266 String originalUrl, String url, Bitmap favicon) { 267 final Cursor c = queryBookmarksForUrl(cr, originalUrl, url); 268 if (c == null) { 269 return; 270 } 271 boolean succeed = c.moveToFirst(); 272 ContentValues values = null; 273 while (succeed) { 274 if (values == null) { 275 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 276 favicon.compress(Bitmap.CompressFormat.PNG, 100, os); 277 values = new ContentValues(); 278 values.put(Browser.BookmarkColumns.FAVICON, os.toByteArray()); 279 } 280 cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, c 281 .getInt(0)), values, null, null); 282 succeed = c.moveToNext(); 283 } 284 c.close(); 285 } 286 287 /* package */ static Cursor queryBookmarksForUrl(ContentResolver cr, 288 String originalUrl, String url) { 289 if (cr == null || url == null) { 290 return null; 291 } 292 293 // If originalUrl is null, just set it to url. 294 if (originalUrl == null) { 295 originalUrl = url; 296 } 297 298 // Look for both the original url and the actual url. This takes in to 299 // account redirects. 300 String originalUrlNoQuery = removeQuery(originalUrl); 301 String urlNoQuery = removeQuery(url); 302 originalUrl = originalUrlNoQuery + '?'; 303 url = urlNoQuery + '?'; 304 305 // Use NoQuery to search for the base url (i.e. if the url is 306 // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) 307 // Use url to match the base url with other queries (i.e. if the url is 308 // http://www.google.com/m, search for 309 // http://www.google.com/m?some_query) 310 final String[] selArgs = new String[] { 311 originalUrlNoQuery, urlNoQuery, originalUrl, url }; 312 final String where = "(" + BookmarkColumns.URL + " == ? OR " 313 + BookmarkColumns.URL + " == ? OR " 314 + BookmarkColumns.URL + " GLOB ? || '*' OR " 315 + BookmarkColumns.URL + " GLOB ? || '*') AND " 316 + BookmarkColumns.BOOKMARK + " == 1"; 317 final String[] projection = 318 new String[] { Browser.BookmarkColumns._ID }; 319 return cr.query(Browser.BOOKMARKS_URI, projection, where, selArgs, 320 null); 321 } 322 323 // Strip the query from the given url. 324 private static String removeQuery(String url) { 325 if (url == null) { 326 return null; 327 } 328 int query = url.indexOf('?'); 329 String noQuery = url; 330 if (query != -1) { 331 noQuery = url.substring(0, query); 332 } 333 return noQuery; 334 } 335 336 /** 337 * How many items should be displayed in the list. 338 * @return Count of items. 339 */ 340 public int getCount() { 341 if (mDataValid) { 342 return mCount; 343 } else { 344 return 0; 345 } 346 } 347 348 public boolean areAllItemsEnabled() { 349 return true; 350 } 351 352 public boolean isEnabled(int position) { 353 return true; 354 } 355 356 /** 357 * Get the data associated with the specified position in the list. 358 * @param position Index of the item whose data we want. 359 * @return The data at the specified position. 360 */ 361 public Object getItem(int position) { 362 return null; 363 } 364 365 /** 366 * Get the row id associated with the specified position in the list. 367 * @param position Index of the item whose row id we want. 368 * @return The id of the item at the specified position. 369 */ 370 public long getItemId(int position) { 371 return position; 372 } 373 374 /* package */ void switchViewMode(boolean toGrid) { 375 mGridMode = toGrid; 376 } 377 378 /* package */ void populateBookmarkItem(BookmarkItem b, int position) { 379 mCursor.moveToPosition(position - mExtraOffset); 380 b.setUrl(mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX)); 381 b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)); 382 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 383 Bitmap bitmap = (null == data) ? null : 384 BitmapFactory.decodeByteArray(data, 0, data.length); 385 b.setFavicon(bitmap); 386 } 387 388 /** 389 * Get a View that displays the data at the specified position 390 * in the list. 391 * @param position Index of the item whose view we want. 392 * @return A View corresponding to the data at the specified position. 393 */ 394 public View getView(int position, View convertView, ViewGroup parent) { 395 if (!mDataValid) { 396 throw new IllegalStateException( 397 "this should only be called when the cursor is valid"); 398 } 399 if (position < 0 || position > mCount) { 400 throw new AssertionError( 401 "BrowserBookmarksAdapter tried to get a view out of range"); 402 } 403 if (mGridMode) { 404 if (convertView == null || convertView instanceof AddNewBookmark 405 || convertView instanceof BookmarkItem) { 406 LayoutInflater factory = LayoutInflater.from(mBookmarksPage); 407 convertView 408 = factory.inflate(R.layout.bookmark_thumbnail, null); 409 } 410 View holder = convertView.findViewById(R.id.holder); 411 ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb); 412 TextView tv = (TextView) convertView.findViewById(R.id.label); 413 414 if (0 == position && !mCreateShortcut) { 415 // This is to create a bookmark for the current page. 416 holder.setVisibility(View.VISIBLE); 417 tv.setText(mCurrentTitle); 418 // FIXME: Want to show the screenshot of the current page 419 thumb.setImageResource(R.drawable.blank); 420 return convertView; 421 } 422 holder.setVisibility(View.GONE); 423 mCursor.moveToPosition(position - mExtraOffset); 424 tv.setText(mCursor.getString( 425 Browser.HISTORY_PROJECTION_TITLE_INDEX)); 426 byte[] data = mCursor.getBlob( 427 Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX); 428 if (data == null) { 429 // Backup is to just show white 430 thumb.setImageResource(R.drawable.blank); 431 } else { 432 thumb.setImageBitmap( 433 BitmapFactory.decodeByteArray(data, 0, data.length)); 434 } 435 436 return convertView; 437 438 } 439 if (position == 0 && !mCreateShortcut) { 440 AddNewBookmark b; 441 if (convertView instanceof AddNewBookmark) { 442 b = (AddNewBookmark) convertView; 443 } else { 444 b = new AddNewBookmark(mBookmarksPage); 445 } 446 b.setUrl(mCurrentPage); 447 return b; 448 } 449 if (convertView == null || !(convertView instanceof BookmarkItem)) { 450 convertView = new BookmarkItem(mBookmarksPage); 451 } 452 bind((BookmarkItem)convertView, position); 453 return convertView; 454 } 455 456 /** 457 * Return the title for this item in the list. 458 */ 459 public String getTitle(int position) { 460 return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position); 461 } 462 463 /** 464 * Return the Url for this item in the list. 465 */ 466 public String getUrl(int position) { 467 return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position); 468 } 469 470 /** 471 * Return the favicon for this item in the list. 472 */ 473 public Bitmap getFavicon(int position) { 474 return getBitmap(Browser.HISTORY_PROJECTION_FAVICON_INDEX, position); 475 } 476 477 public Bitmap getTouchIcon(int position) { 478 return getBitmap(Browser.HISTORY_PROJECTION_TOUCH_ICON_INDEX, position); 479 } 480 481 private Bitmap getBitmap(int cursorIndex, int position) { 482 if (position < mExtraOffset || position > mCount) { 483 return null; 484 } 485 mCursor.moveToPosition(position - mExtraOffset); 486 byte[] data = mCursor.getBlob(cursorIndex); 487 if (data == null) { 488 return null; 489 } 490 return BitmapFactory.decodeByteArray(data, 0, data.length); 491 } 492 493 /** 494 * Private helper function to return the title or url. 495 */ 496 private String getString(int cursorIndex, int position) { 497 if (position < mExtraOffset || position > mCount) { 498 return ""; 499 } 500 mCursor.moveToPosition(position- mExtraOffset); 501 return mCursor.getString(cursorIndex); 502 } 503 504 private void bind(BookmarkItem b, int position) { 505 mCursor.moveToPosition(position- mExtraOffset); 506 507 String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX); 508 if (title.length() > BrowserSettings.MAX_TEXTVIEW_LEN) { 509 title = title.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN); 510 } 511 b.setName(title); 512 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 513 if (url.length() > BrowserSettings.MAX_TEXTVIEW_LEN) { 514 url = url.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN); 515 } 516 b.setUrl(url); 517 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 518 if (data != null) { 519 b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); 520 } else { 521 b.setFavicon(null); 522 } 523 } 524 525 private class ChangeObserver extends ContentObserver { 526 public ChangeObserver() { 527 super(new Handler()); 528 } 529 530 @Override 531 public boolean deliverSelfNotifications() { 532 return true; 533 } 534 535 @Override 536 public void onChange(boolean selfChange) { 537 refreshList(); 538 } 539 } 540 541 private class MyDataSetObserver extends DataSetObserver { 542 @Override 543 public void onChanged() { 544 mDataValid = true; 545 notifyDataSetChanged(); 546 } 547 548 @Override 549 public void onInvalidated() { 550 mDataValid = false; 551 notifyDataSetInvalidated(); 552 } 553 } 554} 555