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