BrowserProvider.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.app.ISearchManager; 20import android.app.SearchManager; 21import android.content.ComponentName; 22import android.content.ContentProvider; 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.UriMatcher; 28import android.database.AbstractCursor; 29import android.database.Cursor; 30import android.database.sqlite.SQLiteOpenHelper; 31import android.database.sqlite.SQLiteDatabase; 32import android.net.Uri; 33import android.os.RemoteException; 34import android.os.ServiceManager; 35import android.os.SystemProperties; 36import android.provider.Browser; 37import android.util.Log; 38import android.server.search.SearchableInfo; 39import android.text.util.Regex; 40 41public class BrowserProvider extends ContentProvider { 42 43 private SQLiteOpenHelper mOpenHelper; 44 private static final String sDatabaseName = "browser.db"; 45 private static final String TAG = "BrowserProvider"; 46 private static final String ORDER_BY = "visits DESC, date DESC"; 47 48 private static final String[] TABLE_NAMES = new String[] { 49 "bookmarks", "searches" 50 }; 51 private static final String[] SUGGEST_PROJECTION = new String[] { 52 "_id", "url", "title", "bookmark" 53 }; 54 private static final String SUGGEST_SELECTION = 55 "url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"; 56 private String[] SUGGEST_ARGS = new String[4]; 57 58 // shared suggestion array index, make sure to match COLUMNS 59 private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1; 60 private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2; 61 private static final int SUGGEST_COLUMN_TEXT_1_ID = 3; 62 private static final int SUGGEST_COLUMN_TEXT_2_ID = 4; 63 private static final int SUGGEST_COLUMN_ICON_1_ID = 5; 64 private static final int SUGGEST_COLUMN_ICON_2_ID = 6; 65 private static final int SUGGEST_COLUMN_QUERY_ID = 7; 66 67 // shared suggestion columns 68 private static final String[] COLUMNS = new String[] { 69 "_id", 70 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 71 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 72 SearchManager.SUGGEST_COLUMN_TEXT_1, 73 SearchManager.SUGGEST_COLUMN_TEXT_2, 74 SearchManager.SUGGEST_COLUMN_ICON_1, 75 SearchManager.SUGGEST_COLUMN_ICON_2, 76 SearchManager.SUGGEST_COLUMN_QUERY}; 77 78 private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3; 79 private static final int MAX_SUGGESTION_LONG_ENTRIES = 6; 80 81 // make sure that these match the index of TABLE_NAMES 82 private static final int URI_MATCH_BOOKMARKS = 0; 83 private static final int URI_MATCH_SEARCHES = 1; 84 // (id % 10) should match the table name index 85 private static final int URI_MATCH_BOOKMARKS_ID = 10; 86 private static final int URI_MATCH_SEARCHES_ID = 11; 87 // 88 private static final int URI_MATCH_SUGGEST = 20; 89 90 private static final UriMatcher URI_MATCHER; 91 92 static { 93 URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 94 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS], 95 URI_MATCH_BOOKMARKS); 96 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#", 97 URI_MATCH_BOOKMARKS_ID); 98 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES], 99 URI_MATCH_SEARCHES); 100 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#", 101 URI_MATCH_SEARCHES_ID); 102 URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY, 103 URI_MATCH_SUGGEST); 104 } 105 106 // 1 -> 2 add cache table 107 // 2 -> 3 update history table 108 // 3 -> 4 add passwords table 109 // 4 -> 5 add settings table 110 // 5 -> 6 ? 111 // 6 -> 7 ? 112 // 7 -> 8 drop proxy table 113 // 8 -> 9 drop settings table 114 // 9 -> 10 add form_urls and form_data 115 // 10 -> 11 add searches table 116 // 11 -> 12 modify cache table 117 // 12 -> 13 modify cache table 118 // 13 -> 14 correspond with Google Bookmarks schema 119 // 14 -> 15 move couple of tables to either browser private database or webview database 120 // 15 -> 17 Set it up for the SearchManager 121 // 17 -> 18 Added favicon in bookmarks table for Home shortcuts 122 // 18 -> 19 Remove labels table 123 private static final int DATABASE_VERSION = 19; 124 125 public BrowserProvider() { 126 } 127 128 129 private static CharSequence replaceSystemPropertyInString(CharSequence srcString) { 130 StringBuffer sb = new StringBuffer(); 131 int lastCharLoc = 0; 132 for (int i = 0; i < srcString.length(); ++i) { 133 char c = srcString.charAt(i); 134 if (c == '{') { 135 sb.append(srcString.subSequence(lastCharLoc, i)); 136 lastCharLoc = i; 137 inner: 138 for (int j = i; j < srcString.length(); ++j) { 139 char k = srcString.charAt(j); 140 if (k == '}') { 141 String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); 142 // See if the propertyKeyValue specifies a default value 143 int defaultOffset = propertyKeyValue.indexOf(':'); 144 if (defaultOffset == -1) { 145 sb.append(SystemProperties.get(propertyKeyValue)); 146 } else { 147 String propertyKey = propertyKeyValue.substring(0, defaultOffset); 148 String defaultValue = 149 propertyKeyValue.substring(defaultOffset + 1, 150 propertyKeyValue.length()); 151 sb.append(SystemProperties.get(propertyKey, defaultValue)); 152 } 153 lastCharLoc = j + 1; 154 i = j; 155 break inner; 156 } 157 } 158 } 159 } 160 if (srcString.length() - lastCharLoc > 0) { 161 // Put on the tail, if there is one 162 sb.append(srcString.subSequence(lastCharLoc, srcString.length())); 163 } 164 return sb; 165 } 166 167 private static class DatabaseHelper extends SQLiteOpenHelper { 168 private Context mContext; 169 170 public DatabaseHelper(Context context) { 171 super(context, sDatabaseName, null, DATABASE_VERSION); 172 mContext = context; 173 } 174 175 @Override 176 public void onCreate(SQLiteDatabase db) { 177 db.execSQL("CREATE TABLE bookmarks (" + 178 "_id INTEGER PRIMARY KEY," + 179 "title TEXT," + 180 "url TEXT," + 181 "visits INTEGER," + 182 "date LONG," + 183 "created LONG," + 184 "description TEXT," + 185 "bookmark INTEGER," + 186 "favicon BLOB DEFAULT NULL" + 187 ");"); 188 189 final CharSequence[] bookmarks = mContext.getResources() 190 .getTextArray(R.array.bookmarks); 191 int size = bookmarks.length; 192 try { 193 for (int i = 0; i < size; i = i + 2) { 194 CharSequence bookmarkDestination = replaceSystemPropertyInString(bookmarks[i + 1]); 195 db.execSQL("INSERT INTO bookmarks (title, url, visits, " + 196 "date, created, bookmark)" + " VALUES('" + 197 bookmarks[i] + "', '" + bookmarkDestination + 198 "', 0, 0, 0, 1);"); 199 } 200 } catch (ArrayIndexOutOfBoundsException e) { 201 } 202 203 db.execSQL("CREATE TABLE searches (" + 204 "_id INTEGER PRIMARY KEY," + 205 "search TEXT," + 206 "date LONG" + 207 ");"); 208 } 209 210 @Override 211 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 212 Log.w(TAG, "Upgrading database from version " + oldVersion + " to " 213 + newVersion + ", which will destroy all old data"); 214 if (oldVersion == 18) { 215 db.execSQL("DROP TABLE IF EXISTS labels"); 216 } else { 217 db.execSQL("DROP TABLE IF EXISTS bookmarks"); 218 db.execSQL("DROP TABLE IF EXISTS searches"); 219 onCreate(db); 220 } 221 } 222 } 223 224 @Override 225 public boolean onCreate() { 226 mOpenHelper = new DatabaseHelper(getContext()); 227 return true; 228 } 229 230 /* 231 * Subclass AbstractCursor so we can combine multiple Cursors and add 232 * "Google Search". 233 * Here are the rules. 234 * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus 235 * "Google Search"; 236 * 2. If bookmark/history entries are less than 237 * (MAX_SUGGESTION_SHORT_ENTRIES -1), we include Google suggest. 238 */ 239 private class MySuggestionCursor extends AbstractCursor { 240 private Cursor mHistoryCursor; 241 private Cursor mSuggestCursor; 242 private int mHistoryCount; 243 private int mSuggestionCount; 244 private boolean mBeyondCursor; 245 private String mString; 246 247 public MySuggestionCursor(Cursor hc, Cursor sc, String string) { 248 mHistoryCursor = hc; 249 mSuggestCursor = sc; 250 mHistoryCount = hc.getCount(); 251 mSuggestionCount = sc != null ? sc.getCount() : 0; 252 if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) { 253 mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount; 254 } 255 mString = string; 256 mBeyondCursor = false; 257 } 258 259 @Override 260 public boolean onMove(int oldPosition, int newPosition) { 261 if (mHistoryCursor == null) { 262 return false; 263 } 264 if (mHistoryCount > newPosition) { 265 mHistoryCursor.moveToPosition(newPosition); 266 mBeyondCursor = false; 267 } else if (mHistoryCount + mSuggestionCount > newPosition) { 268 mSuggestCursor.moveToPosition(newPosition - mHistoryCount); 269 mBeyondCursor = false; 270 } else { 271 mBeyondCursor = true; 272 } 273 return true; 274 } 275 276 @Override 277 public int getCount() { 278 if (mString.length() > 0) { 279 return mHistoryCount + mSuggestionCount + 1; 280 } else { 281 return mHistoryCount + mSuggestionCount; 282 } 283 } 284 285 @Override 286 public String[] getColumnNames() { 287 return COLUMNS; 288 } 289 290 @Override 291 public String getString(int columnIndex) { 292 if ((mPos != -1 && mHistoryCursor != null)) { 293 switch(columnIndex) { 294 case SUGGEST_COLUMN_INTENT_ACTION_ID: 295 if (mHistoryCount > mPos) { 296 return Intent.ACTION_VIEW; 297 } else { 298 return Intent.ACTION_SEARCH; 299 } 300 301 case SUGGEST_COLUMN_INTENT_DATA_ID: 302 if (mHistoryCount > mPos) { 303 return mHistoryCursor.getString(1); 304 } else { 305 return null; 306 } 307 308 case SUGGEST_COLUMN_TEXT_1_ID: 309 if (mHistoryCount > mPos) { 310 return mHistoryCursor.getString(1); 311 } else if (!mBeyondCursor) { 312 return mSuggestCursor.getString(1); 313 } else { 314 return mString; 315 } 316 317 case SUGGEST_COLUMN_TEXT_2_ID: 318 if (mHistoryCount > mPos) { 319 return mHistoryCursor.getString(2); 320 } else if (!mBeyondCursor) { 321 return mSuggestCursor.getString(2); 322 } else { 323 return getContext().getString(R.string.search_google); 324 } 325 326 case SUGGEST_COLUMN_ICON_1_ID: 327 if (mHistoryCount > mPos) { 328 if (mHistoryCursor.getInt(3) == 1) { 329 return new Integer( 330 R.drawable.ic_search_category_bookmark) 331 .toString(); 332 } else { 333 return new Integer( 334 R.drawable.ic_search_category_history) 335 .toString(); 336 } 337 } else { 338 return new Integer( 339 R.drawable.ic_search_category_suggest) 340 .toString(); 341 } 342 343 case SUGGEST_COLUMN_ICON_2_ID: 344 return new String("0"); 345 346 case SUGGEST_COLUMN_QUERY_ID: 347 if (mHistoryCount > mPos) { 348 return null; 349 } else if (!mBeyondCursor) { 350 return mSuggestCursor.getString(3); 351 } else { 352 return mString; 353 } 354 } 355 } 356 return null; 357 } 358 359 @Override 360 public double getDouble(int column) { 361 throw new UnsupportedOperationException(); 362 } 363 364 @Override 365 public float getFloat(int column) { 366 throw new UnsupportedOperationException(); 367 } 368 369 @Override 370 public int getInt(int column) { 371 throw new UnsupportedOperationException(); 372 } 373 374 @Override 375 public long getLong(int column) { 376 if ((mPos != -1) && column == 0) { 377 return mPos; // use row# as the _Id 378 } 379 throw new UnsupportedOperationException(); 380 } 381 382 @Override 383 public short getShort(int column) { 384 throw new UnsupportedOperationException(); 385 } 386 387 @Override 388 public boolean isNull(int column) { 389 throw new UnsupportedOperationException(); 390 } 391 392 // TODO Temporary change, finalize after jq's changes go in 393 public void deactivate() { 394 if (mHistoryCursor != null) { 395 mHistoryCursor.deactivate(); 396 } 397 if (mSuggestCursor != null) { 398 mSuggestCursor.deactivate(); 399 } 400 super.deactivate(); 401 } 402 403 public boolean requery() { 404 return (mHistoryCursor != null ? mHistoryCursor.requery() : false) | 405 (mSuggestCursor != null ? mSuggestCursor.requery() : false); 406 } 407 408 // TODO Temporary change, finalize after jq's changes go in 409 public void close() { 410 super.close(); 411 if (mHistoryCursor != null) { 412 mHistoryCursor.close(); 413 mHistoryCursor = null; 414 } 415 if (mSuggestCursor != null) { 416 mSuggestCursor.close(); 417 mSuggestCursor = null; 418 } 419 } 420 } 421 422 @Override 423 public Cursor query(Uri url, String[] projectionIn, String selection, 424 String[] selectionArgs, String sortOrder) 425 throws IllegalStateException { 426 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 427 428 int match = URI_MATCHER.match(url); 429 if (match == -1) { 430 throw new IllegalArgumentException("Unknown URL"); 431 } 432 433 if (match == URI_MATCH_SUGGEST) { 434 String suggestSelection; 435 String [] myArgs; 436 if (selectionArgs[0] == null || selectionArgs[0].equals("")) { 437 suggestSelection = null; 438 myArgs = null; 439 } else { 440 String like = selectionArgs[0] + "%"; 441 if (selectionArgs[0].startsWith("http")) { 442 myArgs = new String[1]; 443 myArgs[0] = like; 444 suggestSelection = selection; 445 } else { 446 SUGGEST_ARGS[0] = "http://" + like; 447 SUGGEST_ARGS[1] = "http://www." + like; 448 SUGGEST_ARGS[2] = "https://" + like; 449 SUGGEST_ARGS[3] = "https://www." + like; 450 myArgs = SUGGEST_ARGS; 451 suggestSelection = SUGGEST_SELECTION; 452 } 453 } 454 455 Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS], 456 SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, 457 ORDER_BY, 458 (new Integer(MAX_SUGGESTION_LONG_ENTRIES)).toString()); 459 460 if (Regex.WEB_URL_PATTERN.matcher(selectionArgs[0]).matches()) { 461 return new MySuggestionCursor(c, null, ""); 462 } else { 463 // get Google suggest if there is still space in the list 464 if (myArgs != null && myArgs.length > 1 465 && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) { 466 ISearchManager sm = ISearchManager.Stub 467 .asInterface(ServiceManager 468 .getService(Context.SEARCH_SERVICE)); 469 SearchableInfo si = null; 470 try { 471 // use the global search to get Google suggest provider 472 si = sm.getSearchableInfo(new ComponentName( 473 getContext(), "com.android.browser"), true); 474 475 // similar to the getSuggestions() in SearchDialog.java 476 StringBuilder uriStr = new StringBuilder("content://"); 477 uriStr.append(si.getSuggestAuthority()); 478 // if content path provided, insert it now 479 final String contentPath = si.getSuggestPath(); 480 if (contentPath != null) { 481 uriStr.append('/'); 482 uriStr.append(contentPath); 483 } 484 // append standard suggestion query path 485 uriStr.append('/' + SearchManager.SUGGEST_URI_PATH_QUERY); 486 // inject query, either as selection args or inline 487 String[] selArgs = null; 488 if (si.getSuggestSelection() != null) { 489 selArgs = new String[] {selectionArgs[0]}; 490 } else { 491 uriStr.append('/'); 492 uriStr.append(Uri.encode(selectionArgs[0])); 493 } 494 495 // finally, make the query 496 Cursor sc = getContext().getContentResolver().query( 497 Uri.parse(uriStr.toString()), null, 498 si.getSuggestSelection(), selArgs, null); 499 500 return new MySuggestionCursor(c, sc, selectionArgs[0]); 501 } catch (RemoteException e) { 502 } 503 } 504 return new MySuggestionCursor(c, null, selectionArgs[0]); 505 } 506 } 507 508 String[] projection = null; 509 if (projectionIn != null && projectionIn.length > 0) { 510 projection = new String[projectionIn.length + 1]; 511 System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length); 512 projection[projectionIn.length] = "_id AS _id"; 513 } 514 515 StringBuilder whereClause = new StringBuilder(256); 516 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { 517 whereClause.append("(_id = ").append(url.getPathSegments().get(1)) 518 .append(")"); 519 } 520 521 // Tack on the user's selection, if present 522 if (selection != null && selection.length() > 0) { 523 if (whereClause.length() > 0) { 524 whereClause.append(" AND "); 525 } 526 527 whereClause.append('('); 528 whereClause.append(selection); 529 whereClause.append(')'); 530 } 531 Cursor c = db.query(TABLE_NAMES[match % 10], projection, 532 whereClause.toString(), selectionArgs, null, null, sortOrder, 533 null); 534 c.setNotificationUri(getContext().getContentResolver(), url); 535 return c; 536 } 537 538 @Override 539 public String getType(Uri url) { 540 int match = URI_MATCHER.match(url); 541 switch (match) { 542 case URI_MATCH_BOOKMARKS: 543 return "vnd.android.cursor.dir/bookmark"; 544 545 case URI_MATCH_BOOKMARKS_ID: 546 return "vnd.android.cursor.item/bookmark"; 547 548 case URI_MATCH_SEARCHES: 549 return "vnd.android.cursor.dir/searches"; 550 551 case URI_MATCH_SEARCHES_ID: 552 return "vnd.android.cursor.item/searches"; 553 554 case URI_MATCH_SUGGEST: 555 return SearchManager.SUGGEST_MIME_TYPE; 556 557 default: 558 throw new IllegalArgumentException("Unknown URL"); 559 } 560 } 561 562 @Override 563 public Uri insert(Uri url, ContentValues initialValues) { 564 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 565 566 int match = URI_MATCHER.match(url); 567 Uri uri = null; 568 switch (match) { 569 case URI_MATCH_BOOKMARKS: { 570 // Insert into the bookmarks table 571 long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url", 572 initialValues); 573 if (rowID > 0) { 574 uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, 575 rowID); 576 } 577 break; 578 } 579 580 case URI_MATCH_SEARCHES: { 581 // Insert into the searches table 582 long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url", 583 initialValues); 584 if (rowID > 0) { 585 uri = ContentUris.withAppendedId(Browser.SEARCHES_URI, 586 rowID); 587 } 588 break; 589 } 590 591 default: 592 throw new IllegalArgumentException("Unknown URL"); 593 } 594 595 if (uri == null) { 596 throw new IllegalArgumentException("Unknown URL"); 597 } 598 getContext().getContentResolver().notifyChange(uri, null); 599 return uri; 600 } 601 602 @Override 603 public int delete(Uri url, String where, String[] whereArgs) { 604 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 605 606 int match = URI_MATCHER.match(url); 607 if (match == -1 || match == URI_MATCH_SUGGEST) { 608 throw new IllegalArgumentException("Unknown URL"); 609 } 610 611 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { 612 StringBuilder sb = new StringBuilder(); 613 if (where != null && where.length() > 0) { 614 sb.append("( "); 615 sb.append(where); 616 sb.append(" ) AND "); 617 } 618 sb.append("_id = "); 619 sb.append(url.getPathSegments().get(1)); 620 where = sb.toString(); 621 } 622 623 int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs); 624 getContext().getContentResolver().notifyChange(url, null); 625 return count; 626 } 627 628 @Override 629 public int update(Uri url, ContentValues values, String where, 630 String[] whereArgs) { 631 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 632 633 int match = URI_MATCHER.match(url); 634 if (match == -1 || match == URI_MATCH_SUGGEST) { 635 throw new IllegalArgumentException("Unknown URL"); 636 } 637 638 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { 639 StringBuilder sb = new StringBuilder(); 640 if (where != null && where.length() > 0) { 641 sb.append("( "); 642 sb.append(where); 643 sb.append(" ) AND "); 644 } 645 sb.append("_id = "); 646 sb.append(url.getPathSegments().get(1)); 647 where = sb.toString(); 648 } 649 650 int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs); 651 getContext().getContentResolver().notifyChange(url, null); 652 return ret; 653 } 654} 655