BrowserBookmarksAdapter.java revision 0c90888c75eed12f6e2e14a9044faf50bd4af8ed
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.View; 34import android.view.ViewGroup; 35import android.webkit.WebIconDatabase; 36import android.webkit.WebIconDatabase.IconListener; 37import android.widget.BaseAdapter; 38 39import java.io.ByteArrayOutputStream; 40 41class BrowserBookmarksAdapter extends BaseAdapter { 42 43 private final String LOGTAG = "Bookmarks"; 44 45 private String mCurrentPage; 46 private Cursor mCursor; 47 private int mCount; 48 private String mLastWhereClause; 49 private String[] mLastSelectionArgs; 50 private String mLastOrderBy; 51 private BrowserBookmarksPage mBookmarksPage; 52 private ContentResolver mContentResolver; 53 private ChangeObserver mChangeObserver; 54 private DataSetObserver mDataSetObserver; 55 private boolean mDataValid; 56 57 // When true, this adapter is used to pick a bookmark to create a shortcut 58 private boolean mCreateShortcut; 59 private int mExtraOffset; 60 61 // Implementation of WebIconDatabase.IconListener 62 private class IconReceiver implements IconListener { 63 public void onReceivedIcon(String url, Bitmap icon) { 64 updateBookmarkFavicon(mContentResolver, url, icon); 65 } 66 } 67 68 // Instance of IconReceiver 69 private final IconReceiver mIconReceiver = new IconReceiver(); 70 71 /** 72 * Create a new BrowserBookmarksAdapter. 73 * @param b BrowserBookmarksPage that instantiated this. 74 * Necessary so it will adjust its focus 75 * appropriately after a search. 76 */ 77 public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage) { 78 this(b, curPage, false); 79 } 80 81 /** 82 * Create a new BrowserBookmarksAdapter. 83 * @param b BrowserBookmarksPage that instantiated this. 84 * Necessary so it will adjust its focus 85 * appropriately after a search. 86 */ 87 public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage, 88 boolean createShortcut) { 89 mDataValid = false; 90 mCreateShortcut = createShortcut; 91 mExtraOffset = createShortcut ? 0 : 1; 92 mBookmarksPage = b; 93 mCurrentPage = b.getResources().getString(R.string.current_page) + 94 curPage; 95 mContentResolver = b.getContentResolver(); 96 mLastOrderBy = Browser.BookmarkColumns.CREATED + " DESC"; 97 mChangeObserver = new ChangeObserver(); 98 mDataSetObserver = new MyDataSetObserver(); 99 // FIXME: Should have a default sort order that the user selects. 100 search(null); 101 // FIXME: This requires another query of the database after the 102 // initial search(null). 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 WebIconDatabase.getInstance().releaseIconForPageUrl(url); 185 Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, mCursor 186 .getInt(Browser.HISTORY_PROJECTION_ID_INDEX)); 187 int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); 188 if (0 == numVisits) { 189 mContentResolver.delete(uri, null, null); 190 } else { 191 // It is no longer a bookmark, but it is still a visited site. 192 ContentValues values = new ContentValues(); 193 values.put(Browser.BookmarkColumns.BOOKMARK, 0); 194 mContentResolver.update(uri, values, null, null); 195 } 196 refreshList(); 197 } 198 199 /** 200 * Delete all bookmarks from the db. Requeries the database. 201 * All bookmarks with become visited URLs or if never visited 202 * are removed 203 */ 204 public void deleteAllRows() { 205 StringBuilder deleteIds = null; 206 StringBuilder convertIds = null; 207 208 for (mCursor.moveToFirst(); !mCursor.isAfterLast(); mCursor.moveToNext()) { 209 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 210 WebIconDatabase.getInstance().releaseIconForPageUrl(url); 211 int id = mCursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX); 212 int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX); 213 if (0 == numVisits) { 214 if (deleteIds == null) { 215 deleteIds = new StringBuilder(); 216 deleteIds.append("( "); 217 } else { 218 deleteIds.append(" OR ( "); 219 } 220 deleteIds.append(BookmarkColumns._ID); 221 deleteIds.append(" = "); 222 deleteIds.append(id); 223 deleteIds.append(" )"); 224 } else { 225 // It is no longer a bookmark, but it is still a visited site. 226 if (convertIds == null) { 227 convertIds = new StringBuilder(); 228 convertIds.append("( "); 229 } else { 230 convertIds.append(" OR ( "); 231 } 232 convertIds.append(BookmarkColumns._ID); 233 convertIds.append(" = "); 234 convertIds.append(id); 235 convertIds.append(" )"); 236 } 237 } 238 239 if (deleteIds != null) { 240 mContentResolver.delete(Browser.BOOKMARKS_URI, deleteIds.toString(), 241 null); 242 } 243 if (convertIds != null) { 244 ContentValues values = new ContentValues(); 245 values.put(Browser.BookmarkColumns.BOOKMARK, 0); 246 mContentResolver.update(Browser.BOOKMARKS_URI, values, 247 convertIds.toString(), null); 248 } 249 refreshList(); 250 } 251 252 /** 253 * Refresh list to recognize a change in the database. 254 */ 255 public void refreshList() { 256 // FIXME: consider using requery(). 257 // Need to do more work to get it to function though. 258 searchInternal(mLastWhereClause, mLastSelectionArgs, mLastOrderBy); 259 } 260 261 /** 262 * Search the database for bookmarks that match the input string. 263 * @param like String to use to search the database. Strings with spaces 264 * are treated as having multiple search terms using the 265 * OR operator. Search both the title and url. 266 */ 267 public void search(String like) { 268 String whereClause = Browser.BookmarkColumns.BOOKMARK + " == 1"; 269 String[] selectionArgs = null; 270 if (like != null) { 271 String[] likes = like.split(" "); 272 int count = 0; 273 boolean firstTerm = true; 274 StringBuilder andClause = new StringBuilder(256); 275 for (int j = 0; j < likes.length; j++) { 276 if (likes[j].length() > 0) { 277 if (firstTerm) { 278 firstTerm = false; 279 } else { 280 andClause.append(" OR "); 281 } 282 andClause.append(Browser.BookmarkColumns.TITLE 283 + " LIKE ? OR " + Browser.BookmarkColumns.URL 284 + " LIKE ? "); 285 count += 2; 286 } 287 } 288 if (count > 0) { 289 selectionArgs = new String[count]; 290 count = 0; 291 for (int j = 0; j < likes.length; j++) { 292 if (likes[j].length() > 0) { 293 like = "%" + likes[j] + "%"; 294 selectionArgs[count++] = like; 295 selectionArgs[count++] = like; 296 } 297 } 298 whereClause += " AND (" + andClause + ")"; 299 } 300 } 301 searchInternal(whereClause, selectionArgs, mLastOrderBy); 302 } 303 304 /** 305 * Update the bookmark's favicon. 306 * @param cr The ContentResolver to use. 307 * @param url The url of the bookmark to update. 308 * @param favicon The favicon bitmap to write to the db. 309 */ 310 /* package */ static void updateBookmarkFavicon(ContentResolver cr, 311 String url, Bitmap favicon) { 312 if (url == null || favicon == null) { 313 return; 314 } 315 // Strip the query. 316 int query = url.indexOf('?'); 317 String noQuery = url; 318 if (query != -1) { 319 noQuery = url.substring(0, query); 320 } 321 url = noQuery + '?'; 322 // Use noQuery to search for the base url (i.e. if the url is 323 // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com) 324 // Use url to match the base url with other queries (i.e. if the url is 325 // http://www.google.com/m, search for 326 // http://www.google.com/m?some_query) 327 final String[] selArgs = new String[] { noQuery, url }; 328 final String where = "(" + Browser.BookmarkColumns.URL + " == ? OR " 329 + Browser.BookmarkColumns.URL + " GLOB ? || '*') AND " 330 + Browser.BookmarkColumns.BOOKMARK + " == 1"; 331 final String[] projection = new String[] { Browser.BookmarkColumns._ID }; 332 final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, where, 333 selArgs, null); 334 boolean succeed = c.moveToFirst(); 335 ContentValues values = null; 336 while (succeed) { 337 if (values == null) { 338 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 339 favicon.compress(Bitmap.CompressFormat.PNG, 100, os); 340 values = new ContentValues(); 341 values.put(Browser.BookmarkColumns.FAVICON, os.toByteArray()); 342 } 343 cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI, c 344 .getInt(0)), values, null, null); 345 succeed = c.moveToNext(); 346 } 347 c.close(); 348 } 349 350 /** 351 * This sorts alphabetically, with non-capitalized titles before 352 * capitalized. 353 */ 354 public void sortAlphabetical() { 355 searchInternal(mLastWhereClause, mLastSelectionArgs, 356 Browser.BookmarkColumns.TITLE + " COLLATE UNICODE ASC"); 357 } 358 359 /** 360 * Internal function used in search, sort, and refreshList. 361 */ 362 private void searchInternal(String whereClause, String[] selectionArgs, 363 String orderBy) { 364 if (mCursor != null) { 365 mCursor.unregisterContentObserver(mChangeObserver); 366 mCursor.unregisterDataSetObserver(mDataSetObserver); 367 mBookmarksPage.stopManagingCursor(mCursor); 368 mCursor.deactivate(); 369 } 370 371 mLastWhereClause = whereClause; 372 mLastSelectionArgs = selectionArgs; 373 mLastOrderBy = orderBy; 374 mCursor = mContentResolver.query( 375 Browser.BOOKMARKS_URI, 376 Browser.HISTORY_PROJECTION, 377 whereClause, 378 selectionArgs, 379 orderBy); 380 mCursor.registerContentObserver(mChangeObserver); 381 mCursor.registerDataSetObserver(mDataSetObserver); 382 mBookmarksPage.startManagingCursor(mCursor); 383 384 mDataValid = true; 385 notifyDataSetChanged(); 386 387 mCount = mCursor.getCount() + mExtraOffset; 388 } 389 390 /** 391 * How many items should be displayed in the list. 392 * @return Count of items. 393 */ 394 public int getCount() { 395 if (mDataValid) { 396 return mCount; 397 } else { 398 return 0; 399 } 400 } 401 402 public boolean areAllItemsEnabled() { 403 return true; 404 } 405 406 public boolean isEnabled(int position) { 407 return true; 408 } 409 410 /** 411 * Get the data associated with the specified position in the list. 412 * @param position Index of the item whose data we want. 413 * @return The data at the specified position. 414 */ 415 public Object getItem(int position) { 416 return null; 417 } 418 419 /** 420 * Get the row id associated with the specified position in the list. 421 * @param position Index of the item whose row id we want. 422 * @return The id of the item at the specified position. 423 */ 424 public long getItemId(int position) { 425 return position; 426 } 427 428 /** 429 * Get a View that displays the data at the specified position 430 * in the list. 431 * @param position Index of the item whose view we want. 432 * @return A View corresponding to the data at the specified position. 433 */ 434 public View getView(int position, View convertView, ViewGroup parent) { 435 if (!mDataValid) { 436 throw new IllegalStateException( 437 "this should only be called when the cursor is valid"); 438 } 439 if (position < 0 || position > mCount) { 440 throw new AssertionError( 441 "BrowserBookmarksAdapter tried to get a view out of range"); 442 } 443 if (position == 0 && !mCreateShortcut) { 444 AddNewBookmark b; 445 if (convertView instanceof AddNewBookmark) { 446 b = (AddNewBookmark) convertView; 447 } else { 448 b = new AddNewBookmark(mBookmarksPage); 449 } 450 b.setUrl(mCurrentPage); 451 return b; 452 } 453 if (convertView == null || convertView instanceof AddNewBookmark) { 454 convertView = new BookmarkItem(mBookmarksPage); 455 } 456 bind((BookmarkItem)convertView, position); 457 return convertView; 458 } 459 460 /** 461 * Return the title for this item in the list. 462 */ 463 public String getTitle(int position) { 464 return getString(Browser.HISTORY_PROJECTION_TITLE_INDEX, position); 465 } 466 467 /** 468 * Return the Url for this item in the list. 469 */ 470 public String getUrl(int position) { 471 return getString(Browser.HISTORY_PROJECTION_URL_INDEX, position); 472 } 473 474 /** 475 * Private helper function to return the title or url. 476 */ 477 private String getString(int cursorIndex, int position) { 478 if (position < mExtraOffset || position > mCount) { 479 return ""; 480 } 481 mCursor.moveToPosition(position- mExtraOffset); 482 return mCursor.getString(cursorIndex); 483 } 484 485 private void bind(BookmarkItem b, int position) { 486 mCursor.moveToPosition(position- mExtraOffset); 487 488 String title = mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX); 489 if (title.length() > BrowserSettings.MAX_TEXTVIEW_LEN) { 490 title = title.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN); 491 } 492 b.setName(title); 493 String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX); 494 if (url.length() > BrowserSettings.MAX_TEXTVIEW_LEN) { 495 url = url.substring(0, BrowserSettings.MAX_TEXTVIEW_LEN); 496 } 497 b.setUrl(url); 498 byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX); 499 if (data != null) { 500 b.setFavicon(BitmapFactory.decodeByteArray(data, 0, data.length)); 501 } else { 502 b.setFavicon(null); 503 } 504 } 505 506 private class ChangeObserver extends ContentObserver { 507 public ChangeObserver() { 508 super(new Handler()); 509 } 510 511 @Override 512 public boolean deliverSelfNotifications() { 513 return true; 514 } 515 516 @Override 517 public void onChange(boolean selfChange) { 518 refreshList(); 519 } 520 } 521 522 private class MyDataSetObserver extends DataSetObserver { 523 @Override 524 public void onChanged() { 525 mDataValid = true; 526 notifyDataSetChanged(); 527 } 528 529 @Override 530 public void onInvalidated() { 531 mDataValid = false; 532 notifyDataSetInvalidated(); 533 } 534 } 535} 536