Browser.java revision 91f2a20a5957095a6cba9f97326ea0f1eab17195
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 android.provider; 18 19import android.content.ContentResolver; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.Intent; 23import android.database.Cursor; 24import android.database.DatabaseUtils; 25import android.net.Uri; 26import android.util.Log; 27import android.webkit.WebIconDatabase; 28 29import java.util.Date; 30 31public class Browser { 32 private static final String LOGTAG = "browser"; 33 public static final Uri BOOKMARKS_URI = 34 Uri.parse("content://browser/bookmarks"); 35 36 /** 37 * The name of extra data when starting Browser with ACTION_VIEW or 38 * ACTION_SEARCH intent. 39 * <p> 40 * The value should be an integer between 0 and 1000. If not set or set to 41 * 0, the Browser will use default. If set to 100, the Browser will start 42 * with 100%. 43 */ 44 public static final String INITIAL_ZOOM_LEVEL = "browser.initialZoomLevel"; 45 46 /** 47 * The name of the extra data when starting the Browser from another 48 * application. 49 * <p> 50 * The value is a unique identification string that will be used to 51 * indentify the calling application. The Browser will attempt to reuse the 52 * same window each time the application launches the Browser with the same 53 * identifier. 54 */ 55 public static final String EXTRA_APPLICATION_ID = 56 "com.android.browser.application_id"; 57 58 /** 59 * The name of the extra data in the VIEW intent. The data are key/value 60 * pairs in the format of Bundle. They will be sent in the HTTP request 61 * headers for the provided url. The keys can't be the standard HTTP headers 62 * as they are set by the WebView. The url's schema must be http(s). 63 * <p> 64 */ 65 public static final String EXTRA_HEADERS = "com.android.browser.headers"; 66 67 /* if you change column order you must also change indices 68 below */ 69 public static final String[] HISTORY_PROJECTION = new String[] { 70 BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, 71 BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, 72 BookmarkColumns.FAVICON, BookmarkColumns.THUMBNAIL, 73 BookmarkColumns.TOUCH_ICON, BookmarkColumns.USER_ENTERED }; 74 75 /* these indices dependent on HISTORY_PROJECTION */ 76 public static final int HISTORY_PROJECTION_ID_INDEX = 0; 77 public static final int HISTORY_PROJECTION_URL_INDEX = 1; 78 public static final int HISTORY_PROJECTION_VISITS_INDEX = 2; 79 public static final int HISTORY_PROJECTION_DATE_INDEX = 3; 80 public static final int HISTORY_PROJECTION_BOOKMARK_INDEX = 4; 81 public static final int HISTORY_PROJECTION_TITLE_INDEX = 5; 82 public static final int HISTORY_PROJECTION_FAVICON_INDEX = 6; 83 /** 84 * @hide 85 */ 86 public static final int HISTORY_PROJECTION_THUMBNAIL_INDEX = 7; 87 /** 88 * @hide 89 */ 90 public static final int HISTORY_PROJECTION_TOUCH_ICON_INDEX = 8; 91 92 /* columns needed to determine whether to truncate history */ 93 public static final String[] TRUNCATE_HISTORY_PROJECTION = new String[] { 94 BookmarkColumns._ID, BookmarkColumns.DATE, }; 95 public static final int TRUNCATE_HISTORY_PROJECTION_ID_INDEX = 0; 96 97 /* truncate this many history items at a time */ 98 public static final int TRUNCATE_N_OLDEST = 5; 99 100 public static final Uri SEARCHES_URI = 101 Uri.parse("content://browser/searches"); 102 103 /* if you change column order you must also change indices 104 below */ 105 public static final String[] SEARCHES_PROJECTION = new String[] { 106 SearchColumns._ID, SearchColumns.SEARCH, SearchColumns.DATE }; 107 108 /* these indices dependent on SEARCHES_PROJECTION */ 109 public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1; 110 public static final int SEARCHES_PROJECTION_DATE_INDEX = 2; 111 112 private static final String SEARCHES_WHERE_CLAUSE = "search = ?"; 113 114 /* Set a cap on the count of history items in the history/bookmark 115 table, to prevent db and layout operations from dragging to a 116 crawl. Revisit this cap when/if db/layout performance 117 improvements are made. Note: this does not affect bookmark 118 entries -- if the user wants more bookmarks than the cap, they 119 get them. */ 120 private static final int MAX_HISTORY_COUNT = 250; 121 122 /** 123 * URI for writing geolocation permissions. This requires the 124 * {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS}. 125 */ 126 public static final Uri GEOLOCATION_URI = 127 Uri.parse("content://browser/geolocation"); 128 129 private static final String GEOLOCATION_WHERE_CLAUSE = GeolocationColumns.ORIGIN + " = ?"; 130 131 /** 132 * Open the AddBookmark activity to save a bookmark. Launch with 133 * and/or url, which can be edited by the user before saving. 134 * @param c Context used to launch the AddBookmark activity. 135 * @param title Title for the bookmark. Can be null or empty string. 136 * @param url Url for the bookmark. Can be null or empty string. 137 */ 138 public static final void saveBookmark(Context c, 139 String title, 140 String url) { 141 Intent i = new Intent(Intent.ACTION_INSERT, Browser.BOOKMARKS_URI); 142 i.putExtra("title", title); 143 i.putExtra("url", url); 144 c.startActivity(i); 145 } 146 147 /** 148 * Stores a String extra in an {@link Intent} representing the title of a 149 * page to share. When receiving an {@link Intent#ACTION_SEND} from the 150 * Browser, use this to access the title. 151 * @hide 152 */ 153 public final static String EXTRA_SHARE_TITLE = "share_title"; 154 155 /** 156 * Stores a Bitmap extra in an {@link Intent} representing the screenshot of 157 * a page to share. When receiving an {@link Intent#ACTION_SEND} from the 158 * Browser, use this to access the screenshot. 159 * @hide 160 */ 161 public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot"; 162 163 /** 164 * Stores a Bitmap extra in an {@link Intent} representing the favicon of a 165 * page to share. When receiving an {@link Intent#ACTION_SEND} from the 166 * Browser, use this to access the favicon. 167 * @hide 168 */ 169 public final static String EXTRA_SHARE_FAVICON = "share_favicon"; 170 171 public static final void sendString(Context c, String s) { 172 sendString(c, s, c.getString(com.android.internal.R.string.sendText)); 173 } 174 175 /** 176 * Find an application to handle the given string and, if found, invoke 177 * it with the given string as a parameter. 178 * @param c Context used to launch the new activity. 179 * @param stringToSend The string to be handled. 180 * @param chooserDialogTitle The title of the dialog that allows the user 181 * to select between multiple applications that are all capable of handling 182 * the string. 183 * @hide pending API council approval 184 */ 185 public static final void sendString(Context c, 186 String stringToSend, 187 String chooserDialogTitle) { 188 Intent send = new Intent(Intent.ACTION_SEND); 189 send.setType("text/plain"); 190 send.putExtra(Intent.EXTRA_TEXT, stringToSend); 191 192 try { 193 c.startActivity(Intent.createChooser(send, chooserDialogTitle)); 194 } catch(android.content.ActivityNotFoundException ex) { 195 // if no app handles it, do nothing 196 } 197 } 198 199 /** 200 * Return a cursor pointing to a list of all the bookmarks. 201 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 202 * @param cr The ContentResolver used to access the database. 203 */ 204 public static final Cursor getAllBookmarks(ContentResolver cr) throws 205 IllegalStateException { 206 return cr.query(BOOKMARKS_URI, 207 new String[] { BookmarkColumns.URL }, 208 "bookmark = 1", null, null); 209 } 210 211 /** 212 * Return a cursor pointing to a list of all visited site urls. 213 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 214 * @param cr The ContentResolver used to access the database. 215 */ 216 public static final Cursor getAllVisitedUrls(ContentResolver cr) throws 217 IllegalStateException { 218 return cr.query(BOOKMARKS_URI, 219 new String[] { BookmarkColumns.URL }, null, null, null); 220 } 221 222 /** 223 * Update the visited history to acknowledge that a site has been 224 * visited. 225 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 226 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 227 * @param cr The ContentResolver used to access the database. 228 * @param url The site being visited. 229 * @param real If true, this is an actual visit, and should add to the 230 * number of visits. If false, the user entered it manually. 231 */ 232 public static final void updateVisitedHistory(ContentResolver cr, 233 String url, boolean real) { 234 long now = new Date().getTime(); 235 try { 236 StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = "); 237 DatabaseUtils.appendEscapedSQLString(sb, url); 238 Cursor c = cr.query( 239 BOOKMARKS_URI, 240 HISTORY_PROJECTION, 241 sb.toString(), 242 null, 243 null); 244 /* We should only get one answer that is exactly the same. */ 245 if (c.moveToFirst()) { 246 ContentValues map = new ContentValues(); 247 if (real) { 248 map.put(BookmarkColumns.VISITS, c 249 .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1); 250 } else { 251 map.put(BookmarkColumns.USER_ENTERED, 1); 252 } 253 map.put(BookmarkColumns.DATE, now); 254 cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null); 255 } else { 256 truncateHistory(cr); 257 ContentValues map = new ContentValues(); 258 int visits; 259 int user_entered; 260 if (real) { 261 visits = 1; 262 user_entered = 0; 263 } else { 264 visits = 0; 265 user_entered = 1; 266 } 267 map.put(BookmarkColumns.URL, url); 268 map.put(BookmarkColumns.VISITS, visits); 269 map.put(BookmarkColumns.DATE, now); 270 map.put(BookmarkColumns.BOOKMARK, 0); 271 map.put(BookmarkColumns.TITLE, url); 272 map.put(BookmarkColumns.CREATED, 0); 273 map.put(BookmarkColumns.USER_ENTERED, user_entered); 274 cr.insert(BOOKMARKS_URI, map); 275 } 276 c.deactivate(); 277 } catch (IllegalStateException e) { 278 return; 279 } 280 } 281 282 /** 283 * Returns all the URLs in the history. 284 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 285 * @param cr The ContentResolver used to access the database. 286 * @hide pending API council approval 287 */ 288 public static final String[] getVisitedHistory(ContentResolver cr) { 289 try { 290 String[] projection = new String[] { 291 "url" 292 }; 293 Cursor c = cr.query(BOOKMARKS_URI, projection, "visits > 0", null, 294 null); 295 String[] str = new String[c.getCount()]; 296 int i = 0; 297 while (c.moveToNext()) { 298 str[i] = c.getString(0); 299 i++; 300 } 301 c.deactivate(); 302 return str; 303 } catch (IllegalStateException e) { 304 return new String[0]; 305 } 306 } 307 308 /** 309 * If there are more than MAX_HISTORY_COUNT non-bookmark history 310 * items in the bookmark/history table, delete TRUNCATE_N_OLDEST 311 * of them. This is used to keep our history table to a 312 * reasonable size. Note: it does not prune bookmarks. If the 313 * user wants 1000 bookmarks, the user gets 1000 bookmarks. 314 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 315 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 316 * 317 * @param cr The ContentResolver used to access the database. 318 */ 319 public static final void truncateHistory(ContentResolver cr) { 320 try { 321 // Select non-bookmark history, ordered by date 322 Cursor c = cr.query( 323 BOOKMARKS_URI, 324 TRUNCATE_HISTORY_PROJECTION, 325 "bookmark = 0", 326 null, 327 BookmarkColumns.DATE); 328 // Log.v(LOGTAG, "history count " + c.count()); 329 if (c.moveToFirst() && c.getCount() >= MAX_HISTORY_COUNT) { 330 /* eliminate oldest history items */ 331 for (int i = 0; i < TRUNCATE_N_OLDEST; i++) { 332 // Log.v(LOGTAG, "truncate history " + 333 // c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX)); 334 deleteHistoryWhere( 335 cr, "_id = " + 336 c.getInt(TRUNCATE_HISTORY_PROJECTION_ID_INDEX)); 337 if (!c.moveToNext()) break; 338 } 339 } 340 c.deactivate(); 341 } catch (IllegalStateException e) { 342 Log.e(LOGTAG, "truncateHistory", e); 343 return; 344 } 345 } 346 347 /** 348 * Returns whether there is any history to clear. 349 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 350 * @param cr The ContentResolver used to access the database. 351 * @return boolean True if the history can be cleared. 352 */ 353 public static final boolean canClearHistory(ContentResolver cr) { 354 try { 355 Cursor c = cr.query( 356 BOOKMARKS_URI, 357 new String [] { BookmarkColumns._ID, 358 BookmarkColumns.BOOKMARK, 359 BookmarkColumns.VISITS }, 360 "bookmark = 0 OR visits > 0", 361 null, 362 null 363 ); 364 boolean ret = c.moveToFirst(); 365 c.deactivate(); 366 return ret; 367 } catch (IllegalStateException e) { 368 return false; 369 } 370 } 371 372 /** 373 * Delete all entries from the bookmarks/history table which are 374 * not bookmarks. Also set all visited bookmarks to unvisited. 375 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 376 * @param cr The ContentResolver used to access the database. 377 */ 378 public static final void clearHistory(ContentResolver cr) { 379 deleteHistoryWhere(cr, null); 380 } 381 382 /** 383 * Helper function to delete all history items and revert all 384 * bookmarks to zero visits which meet the criteria provided. 385 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 386 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 387 * @param cr The ContentResolver used to access the database. 388 * @param whereClause String to limit the items affected. 389 * null means all items. 390 */ 391 private static final void deleteHistoryWhere(ContentResolver cr, 392 String whereClause) { 393 try { 394 Cursor c = cr.query(BOOKMARKS_URI, 395 HISTORY_PROJECTION, 396 whereClause, 397 null, 398 null); 399 if (!c.moveToFirst()) { 400 c.deactivate(); 401 return; 402 } 403 404 final WebIconDatabase iconDb = WebIconDatabase.getInstance(); 405 /* Delete favicons, and revert bookmarks which have been visited 406 * to simply bookmarks. 407 */ 408 StringBuffer sb = new StringBuffer(); 409 boolean firstTime = true; 410 do { 411 String url = c.getString(HISTORY_PROJECTION_URL_INDEX); 412 boolean isBookmark = 413 c.getInt(HISTORY_PROJECTION_BOOKMARK_INDEX) == 1; 414 if (isBookmark) { 415 if (firstTime) { 416 firstTime = false; 417 } else { 418 sb.append(" OR "); 419 } 420 sb.append("( _id = "); 421 sb.append(c.getInt(0)); 422 sb.append(" )"); 423 } else { 424 iconDb.releaseIconForPageUrl(url); 425 } 426 } while (c.moveToNext()); 427 c.deactivate(); 428 429 if (!firstTime) { 430 ContentValues map = new ContentValues(); 431 map.put(BookmarkColumns.VISITS, 0); 432 map.put(BookmarkColumns.DATE, 0); 433 /* FIXME: Should I also remove the title? */ 434 cr.update(BOOKMARKS_URI, map, sb.toString(), null); 435 } 436 437 String deleteWhereClause = BookmarkColumns.BOOKMARK + " = 0"; 438 if (whereClause != null) { 439 deleteWhereClause += " AND " + whereClause; 440 } 441 cr.delete(BOOKMARKS_URI, deleteWhereClause, null); 442 } catch (IllegalStateException e) { 443 return; 444 } 445 } 446 447 /** 448 * Delete all history items from begin to end. 449 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 450 * @param cr The ContentResolver used to access the database. 451 * @param begin First date to remove. If -1, all dates before end. 452 * Inclusive. 453 * @param end Last date to remove. If -1, all dates after begin. 454 * Non-inclusive. 455 */ 456 public static final void deleteHistoryTimeFrame(ContentResolver cr, 457 long begin, long end) { 458 String whereClause; 459 String date = BookmarkColumns.DATE; 460 if (-1 == begin) { 461 if (-1 == end) { 462 clearHistory(cr); 463 return; 464 } 465 whereClause = date + " < " + Long.toString(end); 466 } else if (-1 == end) { 467 whereClause = date + " >= " + Long.toString(begin); 468 } else { 469 whereClause = date + " >= " + Long.toString(begin) + " AND " + date 470 + " < " + Long.toString(end); 471 } 472 deleteHistoryWhere(cr, whereClause); 473 } 474 475 /** 476 * Remove a specific url from the history database. 477 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 478 * @param cr The ContentResolver used to access the database. 479 * @param url url to remove. 480 */ 481 public static final void deleteFromHistory(ContentResolver cr, 482 String url) { 483 StringBuilder sb = new StringBuilder(BookmarkColumns.URL + " = "); 484 DatabaseUtils.appendEscapedSQLString(sb, url); 485 String matchesUrl = sb.toString(); 486 deleteHistoryWhere(cr, matchesUrl); 487 } 488 489 /** 490 * Add a search string to the searches database. 491 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 492 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 493 * @param cr The ContentResolver used to access the database. 494 * @param search The string to add to the searches database. 495 */ 496 public static final void addSearchUrl(ContentResolver cr, String search) { 497 long now = new Date().getTime(); 498 try { 499 Cursor c = cr.query( 500 SEARCHES_URI, 501 SEARCHES_PROJECTION, 502 SEARCHES_WHERE_CLAUSE, 503 new String [] { search }, 504 null); 505 ContentValues map = new ContentValues(); 506 map.put(SearchColumns.SEARCH, search); 507 map.put(SearchColumns.DATE, now); 508 /* We should only get one answer that is exactly the same. */ 509 if (c.moveToFirst()) { 510 cr.update(SEARCHES_URI, map, "_id = " + c.getInt(0), null); 511 } else { 512 cr.insert(SEARCHES_URI, map); 513 } 514 c.deactivate(); 515 } catch (IllegalStateException e) { 516 Log.e(LOGTAG, "addSearchUrl", e); 517 return; 518 } 519 } 520 /** 521 * Remove all searches from the search database. 522 * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} 523 * @param cr The ContentResolver used to access the database. 524 */ 525 public static final void clearSearches(ContentResolver cr) { 526 // FIXME: Should this clear the urls to which these searches lead? 527 // (i.e. remove google.com/query= blah blah blah) 528 try { 529 cr.delete(SEARCHES_URI, null, null); 530 } catch (IllegalStateException e) { 531 Log.e(LOGTAG, "clearSearches", e); 532 } 533 } 534 535 /** 536 * Request all icons from the database. 537 * Requires {@link android.Manifest.permission#READ_HISTORY_BOOKMARKS} 538 * @param cr The ContentResolver used to access the database. 539 * @param where Clause to be used to limit the query from the database. 540 * Must be an allowable string to be passed into a database query. 541 * @param listener IconListener that gets the icons once they are 542 * retrieved. 543 */ 544 public static final void requestAllIcons(ContentResolver cr, String where, 545 WebIconDatabase.IconListener listener) { 546 try { 547 final Cursor c = cr.query( 548 BOOKMARKS_URI, 549 HISTORY_PROJECTION, 550 where, null, null); 551 if (c.moveToFirst()) { 552 final WebIconDatabase db = WebIconDatabase.getInstance(); 553 do { 554 db.requestIconForPageUrl( 555 c.getString(HISTORY_PROJECTION_URL_INDEX), 556 listener); 557 } while (c.moveToNext()); 558 } 559 c.deactivate(); 560 } catch (IllegalStateException e) { 561 Log.e(LOGTAG, "requestAllIcons", e); 562 } 563 } 564 565 /** 566 * Allows geolocation for the specified origin. 567 * This requires the {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS} 568 * permission. 569 * 570 * @param origin The origin to allow geolocation for, e.g. "http://www.google.com". The string 571 * should not include a trailing slash. 572 */ 573 public static void allowGeolocation(ContentResolver cr, String origin) { 574 try { 575 ContentValues map = new ContentValues(); 576 map.put(GeolocationColumns.ORIGIN, origin); 577 cr.insert(GEOLOCATION_URI, map); 578 } catch (IllegalStateException e) { 579 Log.e(LOGTAG, "allowGeolocation", e); 580 return; 581 } 582 } 583 584 /** 585 * Clears the geolocation permission state for the specified origin. 586 * This requires the {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS} 587 * permission. 588 * 589 * @param origin The origin to allow geolocation for, e.g. "http://www.google.com". The string 590 * should not include a trailing slash. 591 */ 592 public static void clearGeolocation(ContentResolver cr, String origin) { 593 try { 594 String[] whereArgs = { origin }; 595 cr.delete(GEOLOCATION_URI, GEOLOCATION_WHERE_CLAUSE, whereArgs); 596 } catch (IllegalStateException e) { 597 Log.e(LOGTAG, "clearGeolocation", e); 598 } 599 } 600 601 public static class BookmarkColumns implements BaseColumns { 602 public static final String URL = "url"; 603 public static final String VISITS = "visits"; 604 public static final String DATE = "date"; 605 public static final String BOOKMARK = "bookmark"; 606 public static final String TITLE = "title"; 607 public static final String CREATED = "created"; 608 public static final String FAVICON = "favicon"; 609 /** 610 * @hide 611 */ 612 public static final String THUMBNAIL = "thumbnail"; 613 /** 614 * @hide 615 */ 616 public static final String TOUCH_ICON = "touch_icon"; 617 /** 618 * @hide 619 */ 620 public static final String USER_ENTERED = "user_entered"; 621 } 622 623 public static class SearchColumns implements BaseColumns { 624 public static final String URL = "url"; 625 public static final String SEARCH = "search"; 626 public static final String DATE = "date"; 627 } 628 629 public static class GeolocationColumns { 630 public static final String ORIGIN = "origin"; 631 } 632} 633