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