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