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