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.app.SearchManager; 20 import android.app.SearchableInfo; 21 import android.app.backup.BackupManager; 22 import android.content.ComponentName; 23 import android.content.ContentProvider; 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.SharedPreferences; 30 import android.content.UriMatcher; 31 import android.content.SharedPreferences.Editor; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ResolveInfo; 34 import android.database.AbstractCursor; 35 import android.database.ContentObserver; 36 import android.database.Cursor; 37 import android.database.sqlite.SQLiteDatabase; 38 import android.database.sqlite.SQLiteOpenHelper; 39 import android.net.Uri; 40 import android.os.Handler; 41 import android.os.Process; 42 import android.preference.PreferenceManager; 43 import android.provider.Browser; 44 import android.provider.Settings; 45 import android.provider.Browser.BookmarkColumns; 46 import android.speech.RecognizerResultsIntent; 47 import android.text.TextUtils; 48 import android.util.Log; 49 import android.util.Patterns; 50 import android.util.TypedValue; 51 52 53 import java.io.File; 54 import java.io.FilenameFilter; 55 import java.util.ArrayList; 56 import java.util.Date; 57 import java.util.regex.Matcher; 58 import java.util.regex.Pattern; 59 60 61 public class BrowserProvider extends ContentProvider { 62 63 private SQLiteOpenHelper mOpenHelper; 64 private BackupManager mBackupManager; 65 private static final String sDatabaseName = "browser.db"; 66 private static final String TAG = "BrowserProvider"; 67 private static final String ORDER_BY = "visits DESC, date DESC"; 68 69 private static final String PICASA_URL = "http://picasaweb.google.com/m/" + 70 "viewer?source=androidclient"; 71 72 private static final String[] TABLE_NAMES = new String[] { 73 "bookmarks", "searches" 74 }; 75 private static final String[] SUGGEST_PROJECTION = new String[] { 76 "_id", "url", "title", "bookmark", "user_entered" 77 }; 78 private static final String SUGGEST_SELECTION = 79 "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?" 80 + " OR title LIKE ?) AND (bookmark = 1 OR user_entered = 1)"; 81 private String[] SUGGEST_ARGS = new String[5]; 82 83 // shared suggestion array index, make sure to match COLUMNS 84 private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1; 85 private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2; 86 private static final int SUGGEST_COLUMN_TEXT_1_ID = 3; 87 private static final int SUGGEST_COLUMN_TEXT_2_ID = 4; 88 private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5; 89 private static final int SUGGEST_COLUMN_ICON_1_ID = 6; 90 private static final int SUGGEST_COLUMN_ICON_2_ID = 7; 91 private static final int SUGGEST_COLUMN_QUERY_ID = 8; 92 private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9; 93 94 // shared suggestion columns 95 private static final String[] COLUMNS = new String[] { 96 "_id", 97 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 98 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 99 SearchManager.SUGGEST_COLUMN_TEXT_1, 100 SearchManager.SUGGEST_COLUMN_TEXT_2, 101 SearchManager.SUGGEST_COLUMN_TEXT_2_URL, 102 SearchManager.SUGGEST_COLUMN_ICON_1, 103 SearchManager.SUGGEST_COLUMN_ICON_2, 104 SearchManager.SUGGEST_COLUMN_QUERY, 105 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA}; 106 107 private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3; 108 private static final int MAX_SUGGESTION_LONG_ENTRIES = 6; 109 private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING = 110 Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString(); 111 112 // make sure that these match the index of TABLE_NAMES 113 private static final int URI_MATCH_BOOKMARKS = 0; 114 private static final int URI_MATCH_SEARCHES = 1; 115 // (id % 10) should match the table name index 116 private static final int URI_MATCH_BOOKMARKS_ID = 10; 117 private static final int URI_MATCH_SEARCHES_ID = 11; 118 // 119 private static final int URI_MATCH_SUGGEST = 20; 120 private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21; 121 122 private static final UriMatcher URI_MATCHER; 123 124 static { 125 URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 126 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS], 127 URI_MATCH_BOOKMARKS); 128 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#", 129 URI_MATCH_BOOKMARKS_ID); 130 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES], 131 URI_MATCH_SEARCHES); 132 URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#", 133 URI_MATCH_SEARCHES_ID); 134 URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY, 135 URI_MATCH_SUGGEST); 136 URI_MATCHER.addURI("browser", 137 TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY, 138 URI_MATCH_BOOKMARKS_SUGGEST); 139 } 140 141 // 1 -> 2 add cache table 142 // 2 -> 3 update history table 143 // 3 -> 4 add passwords table 144 // 4 -> 5 add settings table 145 // 5 -> 6 ? 146 // 6 -> 7 ? 147 // 7 -> 8 drop proxy table 148 // 8 -> 9 drop settings table 149 // 9 -> 10 add form_urls and form_data 150 // 10 -> 11 add searches table 151 // 11 -> 12 modify cache table 152 // 12 -> 13 modify cache table 153 // 13 -> 14 correspond with Google Bookmarks schema 154 // 14 -> 15 move couple of tables to either browser private database or webview database 155 // 15 -> 17 Set it up for the SearchManager 156 // 17 -> 18 Added favicon in bookmarks table for Home shortcuts 157 // 18 -> 19 Remove labels table 158 // 19 -> 20 Added thumbnail 159 // 20 -> 21 Added touch_icon 160 // 21 -> 22 Remove "clientid" 161 // 22 -> 23 Added user_entered 162 private static final int DATABASE_VERSION = 23; 163 164 // Regular expression which matches http://, followed by some stuff, followed by 165 // optionally a trailing slash, all matched as separate groups. 166 private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?"); 167 168 private SearchManager mSearchManager; 169 170 public BrowserProvider() { 171 } 172 173 // XXX: This is a major hack to remove our dependency on gsf constants and 174 // its content provider. http://b/issue?id=2425179 175 static String getClientId(ContentResolver cr) { 176 String ret = "android-google"; 177 Cursor c = null; 178 try { 179 c = cr.query(Uri.parse("content://com.google.settings/partner"), 180 new String[] { "value" }, "name='client_id'", null, null); 181 if (c != null && c.moveToNext()) { 182 ret = c.getString(0); 183 } 184 } catch (RuntimeException ex) { 185 // fall through to return the default 186 } finally { 187 if (c != null) { 188 c.close(); 189 } 190 } 191 return ret; 192 } 193 194 private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) { 195 StringBuffer sb = new StringBuffer(); 196 int lastCharLoc = 0; 197 198 final String client_id = getClientId(context.getContentResolver()); 199 200 for (int i = 0; i < srcString.length(); ++i) { 201 char c = srcString.charAt(i); 202 if (c == '{') { 203 sb.append(srcString.subSequence(lastCharLoc, i)); 204 lastCharLoc = i; 205 inner: 206 for (int j = i; j < srcString.length(); ++j) { 207 char k = srcString.charAt(j); 208 if (k == '}') { 209 String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); 210 if (propertyKeyValue.equals("CLIENT_ID")) { 211 sb.append(client_id); 212 } else { 213 sb.append("unknown"); 214 } 215 lastCharLoc = j + 1; 216 i = j; 217 break inner; 218 } 219 } 220 } 221 } 222 if (srcString.length() - lastCharLoc > 0) { 223 // Put on the tail, if there is one 224 sb.append(srcString.subSequence(lastCharLoc, srcString.length())); 225 } 226 return sb; 227 } 228 229 private static class DatabaseHelper extends SQLiteOpenHelper { 230 private Context mContext; 231 232 public DatabaseHelper(Context context) { 233 super(context, sDatabaseName, null, DATABASE_VERSION); 234 mContext = context; 235 } 236 237 @Override 238 public void onCreate(SQLiteDatabase db) { 239 db.execSQL("CREATE TABLE bookmarks (" + 240 "_id INTEGER PRIMARY KEY," + 241 "title TEXT," + 242 "url TEXT," + 243 "visits INTEGER," + 244 "date LONG," + 245 "created LONG," + 246 "description TEXT," + 247 "bookmark INTEGER," + 248 "favicon BLOB DEFAULT NULL," + 249 "thumbnail BLOB DEFAULT NULL," + 250 "touch_icon BLOB DEFAULT NULL," + 251 "user_entered INTEGER" + 252 ");"); 253 254 final CharSequence[] bookmarks = mContext.getResources() 255 .getTextArray(R.array.bookmarks); 256 int size = bookmarks.length; 257 try { 258 for (int i = 0; i < size; i = i + 2) { 259 CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]); 260 db.execSQL("INSERT INTO bookmarks (title, url, visits, " + 261 "date, created, bookmark)" + " VALUES('" + 262 bookmarks[i] + "', '" + bookmarkDestination + 263 "', 0, 0, 0, 1);"); 264 } 265 } catch (ArrayIndexOutOfBoundsException e) { 266 } 267 268 db.execSQL("CREATE TABLE searches (" + 269 "_id INTEGER PRIMARY KEY," + 270 "search TEXT," + 271 "date LONG" + 272 ");"); 273 } 274 275 @Override 276 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 277 Log.w(TAG, "Upgrading database from version " + oldVersion + " to " 278 + newVersion); 279 if (oldVersion == 18) { 280 db.execSQL("DROP TABLE IF EXISTS labels"); 281 } 282 if (oldVersion <= 19) { 283 db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;"); 284 } 285 if (oldVersion < 21) { 286 db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;"); 287 } 288 if (oldVersion < 22) { 289 db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")"); 290 removeGears(); 291 } 292 if (oldVersion < 23) { 293 db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;"); 294 } else { 295 db.execSQL("DROP TABLE IF EXISTS bookmarks"); 296 db.execSQL("DROP TABLE IF EXISTS searches"); 297 onCreate(db); 298 } 299 } 300 301 private void removeGears() { 302 new Thread() { 303 @Override 304 public void run() { 305 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 306 String browserDataDirString = mContext.getApplicationInfo().dataDir; 307 final String appPluginsDirString = "app_plugins"; 308 final String gearsPrefix = "gears"; 309 File appPluginsDir = new File(browserDataDirString + File.separator 310 + appPluginsDirString); 311 if (!appPluginsDir.exists()) { 312 return; 313 } 314 // Delete the Gears plugin files 315 File[] gearsFiles = appPluginsDir.listFiles(new FilenameFilter() { 316 public boolean accept(File dir, String filename) { 317 return filename.startsWith(gearsPrefix); 318 } 319 }); 320 for (int i = 0; i < gearsFiles.length; ++i) { 321 if (gearsFiles[i].isDirectory()) { 322 deleteDirectory(gearsFiles[i]); 323 } else { 324 gearsFiles[i].delete(); 325 } 326 } 327 // Delete the Gears data files 328 File gearsDataDir = new File(browserDataDirString + File.separator 329 + gearsPrefix); 330 if (!gearsDataDir.exists()) { 331 return; 332 } 333 deleteDirectory(gearsDataDir); 334 } 335 336 private void deleteDirectory(File currentDir) { 337 File[] files = currentDir.listFiles(); 338 for (int i = 0; i < files.length; ++i) { 339 if (files[i].isDirectory()) { 340 deleteDirectory(files[i]); 341 } 342 files[i].delete(); 343 } 344 currentDir.delete(); 345 } 346 }.start(); 347 } 348 } 349 350 @Override 351 public boolean onCreate() { 352 final Context context = getContext(); 353 mOpenHelper = new DatabaseHelper(context); 354 mBackupManager = new BackupManager(context); 355 // we added "picasa web album" into default bookmarks for version 19. 356 // To avoid erasing the bookmark table, we added it explicitly for 357 // version 18 and 19 as in the other cases, we will erase the table. 358 if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) { 359 SharedPreferences p = PreferenceManager 360 .getDefaultSharedPreferences(context); 361 boolean fix = p.getBoolean("fix_picasa", true); 362 if (fix) { 363 fixPicasaBookmark(); 364 Editor ed = p.edit(); 365 ed.putBoolean("fix_picasa", false); 366 ed.commit(); 367 } 368 } 369 mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE); 370 mShowWebSuggestionsSettingChangeObserver 371 = new ShowWebSuggestionsSettingChangeObserver(); 372 context.getContentResolver().registerContentObserver( 373 Settings.System.getUriFor( 374 Settings.System.SHOW_WEB_SUGGESTIONS), 375 true, mShowWebSuggestionsSettingChangeObserver); 376 updateShowWebSuggestions(); 377 return true; 378 } 379 380 /** 381 * This Observer will ensure that if the user changes the system 382 * setting of whether to display web suggestions, we will 383 * change accordingly. 384 */ 385 /* package */ class ShowWebSuggestionsSettingChangeObserver 386 extends ContentObserver { 387 public ShowWebSuggestionsSettingChangeObserver() { 388 super(new Handler()); 389 } 390 391 @Override 392 public void onChange(boolean selfChange) { 393 updateShowWebSuggestions(); 394 } 395 } 396 397 private ShowWebSuggestionsSettingChangeObserver 398 mShowWebSuggestionsSettingChangeObserver; 399 400 // If non-null, then the system is set to show web suggestions, 401 // and this is the SearchableInfo to use to get them. 402 private SearchableInfo mSearchableInfo; 403 404 /** 405 * Check the system settings to see whether web suggestions are 406 * allowed. If so, store the SearchableInfo to grab suggestions 407 * while the user is typing. 408 */ 409 private void updateShowWebSuggestions() { 410 mSearchableInfo = null; 411 Context context = getContext(); 412 if (Settings.System.getInt(context.getContentResolver(), 413 Settings.System.SHOW_WEB_SUGGESTIONS, 414 1 /* default on */) == 1) { 415 ComponentName webSearchComponent = mSearchManager.getWebSearchActivity(); 416 if (webSearchComponent != null) { 417 mSearchableInfo = mSearchManager.getSearchableInfo(webSearchComponent); 418 } 419 } 420 } 421 422 private void fixPicasaBookmark() { 423 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 424 Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " + 425 "bookmark = 1 AND url = ?", new String[] { PICASA_URL }); 426 try { 427 if (!cursor.moveToFirst()) { 428 // set "created" so that it will be on the top of the list 429 db.execSQL("INSERT INTO bookmarks (title, url, visits, " + 430 "date, created, bookmark)" + " VALUES('" + 431 getContext().getString(R.string.picasa) + "', '" 432 + PICASA_URL + "', 0, 0, " + new Date().getTime() 433 + ", 1);"); 434 } 435 } finally { 436 if (cursor != null) { 437 cursor.close(); 438 } 439 } 440 } 441 442 /* 443 * Subclass AbstractCursor so we can combine multiple Cursors and add 444 * "Search the web". 445 * Here are the rules. 446 * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus 447 * "Search the web"; 448 * 2. If bookmark/history entries has a match, "Search the web" shows up at 449 * the second place. Otherwise, "Search the web" shows up at the first 450 * place. 451 */ 452 private class MySuggestionCursor extends AbstractCursor { 453 private Cursor mHistoryCursor; 454 private Cursor mSuggestCursor; 455 private int mHistoryCount; 456 private int mSuggestionCount; 457 private boolean mIncludeWebSearch; 458 private String mString; 459 private int mSuggestText1Id; 460 private int mSuggestText2Id; 461 private int mSuggestText2UrlId; 462 private int mSuggestQueryId; 463 private int mSuggestIntentExtraDataId; 464 465 public MySuggestionCursor(Cursor hc, Cursor sc, String string) { 466 mHistoryCursor = hc; 467 mSuggestCursor = sc; 468 mHistoryCount = hc.getCount(); 469 mSuggestionCount = sc != null ? sc.getCount() : 0; 470 if (mSuggestionCount > (MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount)) { 471 mSuggestionCount = MAX_SUGGESTION_LONG_ENTRIES - mHistoryCount; 472 } 473 mString = string; 474 mIncludeWebSearch = string.length() > 0; 475 476 // Some web suggest providers only give suggestions and have no description string for 477 // items. The order of the result columns may be different as well. So retrieve the 478 // column indices for the fields we need now and check before using below. 479 if (mSuggestCursor == null) { 480 mSuggestText1Id = -1; 481 mSuggestText2Id = -1; 482 mSuggestText2UrlId = -1; 483 mSuggestQueryId = -1; 484 mSuggestIntentExtraDataId = -1; 485 } else { 486 mSuggestText1Id = mSuggestCursor.getColumnIndex( 487 SearchManager.SUGGEST_COLUMN_TEXT_1); 488 mSuggestText2Id = mSuggestCursor.getColumnIndex( 489 SearchManager.SUGGEST_COLUMN_TEXT_2); 490 mSuggestText2UrlId = mSuggestCursor.getColumnIndex( 491 SearchManager.SUGGEST_COLUMN_TEXT_2_URL); 492 mSuggestQueryId = mSuggestCursor.getColumnIndex( 493 SearchManager.SUGGEST_COLUMN_QUERY); 494 mSuggestIntentExtraDataId = mSuggestCursor.getColumnIndex( 495 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); 496 } 497 } 498 499 @Override 500 public boolean onMove(int oldPosition, int newPosition) { 501 if (mHistoryCursor == null) { 502 return false; 503 } 504 if (mIncludeWebSearch) { 505 if (mHistoryCount == 0 && newPosition == 0) { 506 return true; 507 } else if (mHistoryCount > 0) { 508 if (newPosition == 0) { 509 mHistoryCursor.moveToPosition(0); 510 return true; 511 } else if (newPosition == 1) { 512 return true; 513 } 514 } 515 newPosition--; 516 } 517 if (mHistoryCount > newPosition) { 518 mHistoryCursor.moveToPosition(newPosition); 519 } else { 520 mSuggestCursor.moveToPosition(newPosition - mHistoryCount); 521 } 522 return true; 523 } 524 525 @Override 526 public int getCount() { 527 if (mIncludeWebSearch) { 528 return mHistoryCount + mSuggestionCount + 1; 529 } else { 530 return mHistoryCount + mSuggestionCount; 531 } 532 } 533 534 @Override 535 public String[] getColumnNames() { 536 return COLUMNS; 537 } 538 539 @Override 540 public String getString(int columnIndex) { 541 if ((mPos != -1 && mHistoryCursor != null)) { 542 int type = -1; // 0: web search; 1: history; 2: suggestion 543 if (mIncludeWebSearch) { 544 if (mHistoryCount == 0 && mPos == 0) { 545 type = 0; 546 } else if (mHistoryCount > 0) { 547 if (mPos == 0) { 548 type = 1; 549 } else if (mPos == 1) { 550 type = 0; 551 } 552 } 553 if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2; 554 } else { 555 type = mPos < mHistoryCount ? 1 : 2; 556 } 557 558 switch(columnIndex) { 559 case SUGGEST_COLUMN_INTENT_ACTION_ID: 560 if (type == 1) { 561 return Intent.ACTION_VIEW; 562 } else { 563 return Intent.ACTION_SEARCH; 564 } 565 566 case SUGGEST_COLUMN_INTENT_DATA_ID: 567 if (type == 1) { 568 return mHistoryCursor.getString(1); 569 } else { 570 return null; 571 } 572 573 case SUGGEST_COLUMN_TEXT_1_ID: 574 if (type == 0) { 575 return mString; 576 } else if (type == 1) { 577 return getHistoryTitle(); 578 } else { 579 if (mSuggestText1Id == -1) return null; 580 return mSuggestCursor.getString(mSuggestText1Id); 581 } 582 583 case SUGGEST_COLUMN_TEXT_2_ID: 584 if (type == 0) { 585 return getContext().getString(R.string.search_the_web); 586 } else if (type == 1) { 587 return null; // Use TEXT_2_URL instead 588 } else { 589 if (mSuggestText2Id == -1) return null; 590 return mSuggestCursor.getString(mSuggestText2Id); 591 } 592 593 case SUGGEST_COLUMN_TEXT_2_URL_ID: 594 if (type == 0) { 595 return null; 596 } else if (type == 1) { 597 return getHistoryUrl(); 598 } else { 599 if (mSuggestText2UrlId == -1) return null; 600 return mSuggestCursor.getString(mSuggestText2UrlId); 601 } 602 603 case SUGGEST_COLUMN_ICON_1_ID: 604 if (type == 1) { 605 if (mHistoryCursor.getInt(3) == 1) { 606 return Integer.valueOf( 607 R.drawable.ic_search_category_bookmark) 608 .toString(); 609 } else { 610 return Integer.valueOf( 611 R.drawable.ic_search_category_history) 612 .toString(); 613 } 614 } else { 615 return Integer.valueOf( 616 R.drawable.ic_search_category_suggest) 617 .toString(); 618 } 619 620 case SUGGEST_COLUMN_ICON_2_ID: 621 return "0"; 622 623 case SUGGEST_COLUMN_QUERY_ID: 624 if (type == 0) { 625 return mString; 626 } else if (type == 1) { 627 // Return the url in the intent query column. This is ignored 628 // within the browser because our searchable is set to 629 // android:searchMode="queryRewriteFromData", but it is used by 630 // global search for query rewriting. 631 return mHistoryCursor.getString(1); 632 } else { 633 if (mSuggestQueryId == -1) return null; 634 return mSuggestCursor.getString(mSuggestQueryId); 635 } 636 637 case SUGGEST_COLUMN_INTENT_EXTRA_DATA: 638 if (type == 0) { 639 return null; 640 } else if (type == 1) { 641 return null; 642 } else { 643 if (mSuggestIntentExtraDataId == -1) return null; 644 return mSuggestCursor.getString(mSuggestIntentExtraDataId); 645 } 646 } 647 } 648 return null; 649 } 650 651 @Override 652 public double getDouble(int column) { 653 throw new UnsupportedOperationException(); 654 } 655 656 @Override 657 public float getFloat(int column) { 658 throw new UnsupportedOperationException(); 659 } 660 661 @Override 662 public int getInt(int column) { 663 throw new UnsupportedOperationException(); 664 } 665 666 @Override 667 public long getLong(int column) { 668 if ((mPos != -1) && column == 0) { 669 return mPos; // use row# as the _Id 670 } 671 throw new UnsupportedOperationException(); 672 } 673 674 @Override 675 public short getShort(int column) { 676 throw new UnsupportedOperationException(); 677 } 678 679 @Override 680 public boolean isNull(int column) { 681 throw new UnsupportedOperationException(); 682 } 683 684 // TODO Temporary change, finalize after jq's changes go in 685 public void deactivate() { 686 if (mHistoryCursor != null) { 687 mHistoryCursor.deactivate(); 688 } 689 if (mSuggestCursor != null) { 690 mSuggestCursor.deactivate(); 691 } 692 super.deactivate(); 693 } 694 695 public boolean requery() { 696 return (mHistoryCursor != null ? mHistoryCursor.requery() : false) | 697 (mSuggestCursor != null ? mSuggestCursor.requery() : false); 698 } 699 700 // TODO Temporary change, finalize after jq's changes go in 701 public void close() { 702 super.close(); 703 if (mHistoryCursor != null) { 704 mHistoryCursor.close(); 705 mHistoryCursor = null; 706 } 707 if (mSuggestCursor != null) { 708 mSuggestCursor.close(); 709 mSuggestCursor = null; 710 } 711 } 712 713 /** 714 * Provides the title (text line 1) for a browser suggestion, which should be the 715 * webpage title. If the webpage title is empty, returns the stripped url instead. 716 * 717 * @return the title string to use 718 */ 719 private String getHistoryTitle() { 720 String title = mHistoryCursor.getString(2 /* webpage title */); 721 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) { 722 title = stripUrl(mHistoryCursor.getString(1 /* url */)); 723 } 724 return title; 725 } 726 727 /** 728 * Provides the subtitle (text line 2) for a browser suggestion, which should be the 729 * webpage url. If the webpage title is empty, then the url should go in the title 730 * instead, and the subtitle should be empty, so this would return null. 731 * 732 * @return the subtitle string to use, or null if none 733 */ 734 private String getHistoryUrl() { 735 String title = mHistoryCursor.getString(2 /* webpage title */); 736 if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) { 737 return null; 738 } else { 739 return stripUrl(mHistoryCursor.getString(1 /* url */)); 740 } 741 } 742 743 } 744 745 private static class ResultsCursor extends AbstractCursor { 746 // Array indices for RESULTS_COLUMNS 747 private static final int RESULT_ACTION_ID = 1; 748 private static final int RESULT_DATA_ID = 2; 749 private static final int RESULT_TEXT_ID = 3; 750 private static final int RESULT_ICON_ID = 4; 751 private static final int RESULT_EXTRA_ID = 5; 752 753 private static final String[] RESULTS_COLUMNS = new String[] { 754 "_id", 755 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 756 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 757 SearchManager.SUGGEST_COLUMN_TEXT_1, 758 SearchManager.SUGGEST_COLUMN_ICON_1, 759 SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA 760 }; 761 private final ArrayList<String> mResults; 762 public ResultsCursor(ArrayList<String> results) { 763 mResults = results; 764 } 765 public int getCount() { return mResults.size(); } 766 767 public String[] getColumnNames() { 768 return RESULTS_COLUMNS; 769 } 770 771 public String getString(int column) { 772 switch (column) { 773 case RESULT_ACTION_ID: 774 return RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS; 775 case RESULT_TEXT_ID: 776 // The data is used when the phone is in landscape mode. We 777 // still want to show the result string. 778 case RESULT_DATA_ID: 779 return mResults.get(mPos); 780 case RESULT_EXTRA_ID: 781 // The Intent's extra data will store the index into 782 // mResults so the BrowserActivity will know which result to 783 // use. 784 return Integer.toString(mPos); 785 case RESULT_ICON_ID: 786 return Integer.valueOf(R.drawable.magnifying_glass) 787 .toString(); 788 default: 789 return null; 790 } 791 } 792 public short getShort(int column) { 793 throw new UnsupportedOperationException(); 794 } 795 public int getInt(int column) { 796 throw new UnsupportedOperationException(); 797 } 798 public long getLong(int column) { 799 if ((mPos != -1) && column == 0) { 800 return mPos; // use row# as the _id 801 } 802 throw new UnsupportedOperationException(); 803 } 804 public float getFloat(int column) { 805 throw new UnsupportedOperationException(); 806 } 807 public double getDouble(int column) { 808 throw new UnsupportedOperationException(); 809 } 810 public boolean isNull(int column) { 811 throw new UnsupportedOperationException(); 812 } 813 } 814 815 private ResultsCursor mResultsCursor; 816 817 /** 818 * Provide a set of results to be returned to query, intended to be used 819 * by the SearchDialog when the BrowserActivity is in voice search mode. 820 * @param results Strings to display in the dropdown from the SearchDialog 821 */ 822 /* package */ void setQueryResults(ArrayList<String> results) { 823 if (results == null) { 824 mResultsCursor = null; 825 } else { 826 mResultsCursor = new ResultsCursor(results); 827 } 828 } 829 830 @Override 831 public Cursor query(Uri url, String[] projectionIn, String selection, 832 String[] selectionArgs, String sortOrder) 833 throws IllegalStateException { 834 int match = URI_MATCHER.match(url); 835 if (match == -1) { 836 throw new IllegalArgumentException("Unknown URL"); 837 } 838 if (match == URI_MATCH_SUGGEST && mResultsCursor != null) { 839 Cursor results = mResultsCursor; 840 mResultsCursor = null; 841 return results; 842 } 843 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 844 845 if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) { 846 String suggestSelection; 847 String [] myArgs; 848 if (selectionArgs[0] == null || selectionArgs[0].equals("")) { 849 suggestSelection = null; 850 myArgs = null; 851 } else { 852 String like = selectionArgs[0] + "%"; 853 if (selectionArgs[0].startsWith("http") 854 || selectionArgs[0].startsWith("file")) { 855 myArgs = new String[1]; 856 myArgs[0] = like; 857 suggestSelection = selection; 858 } else { 859 SUGGEST_ARGS[0] = "http://" + like; 860 SUGGEST_ARGS[1] = "http://www." + like; 861 SUGGEST_ARGS[2] = "https://" + like; 862 SUGGEST_ARGS[3] = "https://www." + like; 863 // To match against titles. 864 SUGGEST_ARGS[4] = like; 865 myArgs = SUGGEST_ARGS; 866 suggestSelection = SUGGEST_SELECTION; 867 } 868 } 869 870 Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS], 871 SUGGEST_PROJECTION, suggestSelection, myArgs, null, null, 872 ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING); 873 874 if (match == URI_MATCH_BOOKMARKS_SUGGEST 875 || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) { 876 return new MySuggestionCursor(c, null, ""); 877 } else { 878 // get Google suggest if there is still space in the list 879 if (myArgs != null && myArgs.length > 1 880 && mSearchableInfo != null 881 && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) { 882 Cursor sc = mSearchManager.getSuggestions(mSearchableInfo, selectionArgs[0]); 883 return new MySuggestionCursor(c, sc, selectionArgs[0]); 884 } 885 return new MySuggestionCursor(c, null, selectionArgs[0]); 886 } 887 } 888 889 String[] projection = null; 890 if (projectionIn != null && projectionIn.length > 0) { 891 projection = new String[projectionIn.length + 1]; 892 System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length); 893 projection[projectionIn.length] = "_id AS _id"; 894 } 895 896 StringBuilder whereClause = new StringBuilder(256); 897 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { 898 whereClause.append("(_id = ").append(url.getPathSegments().get(1)) 899 .append(")"); 900 } 901 902 // Tack on the user's selection, if present 903 if (selection != null && selection.length() > 0) { 904 if (whereClause.length() > 0) { 905 whereClause.append(" AND "); 906 } 907 908 whereClause.append('('); 909 whereClause.append(selection); 910 whereClause.append(')'); 911 } 912 Cursor c = db.query(TABLE_NAMES[match % 10], projection, 913 whereClause.toString(), selectionArgs, null, null, sortOrder, 914 null); 915 c.setNotificationUri(getContext().getContentResolver(), url); 916 return c; 917 } 918 919 @Override 920 public String getType(Uri url) { 921 int match = URI_MATCHER.match(url); 922 switch (match) { 923 case URI_MATCH_BOOKMARKS: 924 return "vnd.android.cursor.dir/bookmark"; 925 926 case URI_MATCH_BOOKMARKS_ID: 927 return "vnd.android.cursor.item/bookmark"; 928 929 case URI_MATCH_SEARCHES: 930 return "vnd.android.cursor.dir/searches"; 931 932 case URI_MATCH_SEARCHES_ID: 933 return "vnd.android.cursor.item/searches"; 934 935 case URI_MATCH_SUGGEST: 936 return SearchManager.SUGGEST_MIME_TYPE; 937 938 default: 939 throw new IllegalArgumentException("Unknown URL"); 940 } 941 } 942 943 @Override 944 public Uri insert(Uri url, ContentValues initialValues) { 945 boolean isBookmarkTable = false; 946 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 947 948 int match = URI_MATCHER.match(url); 949 Uri uri = null; 950 switch (match) { 951 case URI_MATCH_BOOKMARKS: { 952 // Insert into the bookmarks table 953 long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url", 954 initialValues); 955 if (rowID > 0) { 956 uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, 957 rowID); 958 } 959 isBookmarkTable = true; 960 break; 961 } 962 963 case URI_MATCH_SEARCHES: { 964 // Insert into the searches table 965 long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url", 966 initialValues); 967 if (rowID > 0) { 968 uri = ContentUris.withAppendedId(Browser.SEARCHES_URI, 969 rowID); 970 } 971 break; 972 } 973 974 default: 975 throw new IllegalArgumentException("Unknown URL"); 976 } 977 978 if (uri == null) { 979 throw new IllegalArgumentException("Unknown URL"); 980 } 981 getContext().getContentResolver().notifyChange(uri, null); 982 983 // Back up the new bookmark set if we just inserted one. 984 // A row created when bookmarks are added from scratch will have 985 // bookmark=1 in the initial value set. 986 if (isBookmarkTable 987 && initialValues.containsKey(BookmarkColumns.BOOKMARK) 988 && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) { 989 mBackupManager.dataChanged(); 990 } 991 return uri; 992 } 993 994 @Override 995 public int delete(Uri url, String where, String[] whereArgs) { 996 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 997 998 int match = URI_MATCHER.match(url); 999 if (match == -1 || match == URI_MATCH_SUGGEST) { 1000 throw new IllegalArgumentException("Unknown URL"); 1001 } 1002 1003 // need to know whether it's the bookmarks table for a couple of reasons 1004 boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID); 1005 String id = null; 1006 1007 if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) { 1008 StringBuilder sb = new StringBuilder(); 1009 if (where != null && where.length() > 0) { 1010 sb.append("( "); 1011 sb.append(where); 1012 sb.append(" ) AND "); 1013 } 1014 id = url.getPathSegments().get(1); 1015 sb.append("_id = "); 1016 sb.append(id); 1017 where = sb.toString(); 1018 } 1019 1020 ContentResolver cr = getContext().getContentResolver(); 1021 1022 // we'lll need to back up the bookmark set if we are about to delete one 1023 if (isBookmarkTable) { 1024 Cursor cursor = cr.query(Browser.BOOKMARKS_URI, 1025 new String[] { BookmarkColumns.BOOKMARK }, 1026 "_id = " + id, null, null); 1027 if (cursor.moveToNext()) { 1028 if (cursor.getInt(0) != 0) { 1029 // yep, this record is a bookmark 1030 mBackupManager.dataChanged(); 1031 } 1032 } 1033 cursor.close(); 1034 } 1035 1036 int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs); 1037 cr.notifyChange(url, null); 1038 return count; 1039 } 1040 1041 @Override 1042 public int update(Uri url, ContentValues values, String where, 1043 String[] whereArgs) { 1044 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1045 1046 int match = URI_MATCHER.match(url); 1047 if (match == -1 || match == URI_MATCH_SUGGEST) { 1048 throw new IllegalArgumentException("Unknown URL"); 1049 } 1050 1051 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) { 1052 StringBuilder sb = new StringBuilder(); 1053 if (where != null && where.length() > 0) { 1054 sb.append("( "); 1055 sb.append(where); 1056 sb.append(" ) AND "); 1057 } 1058 String id = url.getPathSegments().get(1); 1059 sb.append("_id = "); 1060 sb.append(id); 1061 where = sb.toString(); 1062 } 1063 1064 ContentResolver cr = getContext().getContentResolver(); 1065 1066 // Not all bookmark-table updates should be backed up. Look to see 1067 // whether we changed the title, url, or "is a bookmark" state, and 1068 // request a backup if so. 1069 if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) { 1070 boolean changingBookmarks = false; 1071 // Alterations to the bookmark field inherently change the bookmark 1072 // set, so we don't need to query the record; we know a priori that 1073 // we will need to back up this change. 1074 if (values.containsKey(BookmarkColumns.BOOKMARK)) { 1075 changingBookmarks = true; 1076 } else if ((values.containsKey(BookmarkColumns.TITLE) 1077 || values.containsKey(BookmarkColumns.URL)) 1078 && values.containsKey(BookmarkColumns._ID)) { 1079 // If a title or URL has been changed, check to see if it is to 1080 // a bookmark. The ID should have been included in the update, 1081 // so use it. 1082 Cursor cursor = cr.query(Browser.BOOKMARKS_URI, 1083 new String[] { BookmarkColumns.BOOKMARK }, 1084 BookmarkColumns._ID + " = " 1085 + values.getAsString(BookmarkColumns._ID), null, null); 1086 if (cursor.moveToNext()) { 1087 changingBookmarks = (cursor.getInt(0) != 0); 1088 } 1089 cursor.close(); 1090 } 1091 1092 // if this *is* a bookmark row we're altering, we need to back it up. 1093 if (changingBookmarks) { 1094 mBackupManager.dataChanged(); 1095 } 1096 } 1097 1098 int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs); 1099 cr.notifyChange(url, null); 1100 return ret; 1101 } 1102 1103 /** 1104 * Strips the provided url of preceding "http://" and any trailing "/". Does not 1105 * strip "https://". If the provided string cannot be stripped, the original string 1106 * is returned. 1107 * 1108 * TODO: Put this in TextUtils to be used by other packages doing something similar. 1109 * 1110 * @param url a url to strip, like "http://www.google.com/" 1111 * @return a stripped url like "www.google.com", or the original string if it could 1112 * not be stripped 1113 */ 1114 private static String stripUrl(String url) { 1115 if (url == null) return null; 1116 Matcher m = STRIP_URL_PATTERN.matcher(url); 1117 if (m.matches() && m.groupCount() == 3) { 1118 return m.group(2); 1119 } else { 1120 return url; 1121 } 1122 } 1123 1124 } 1125