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