1 /* 2 * Copyright (C) 2010 he 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.accounts.Account; 20 import android.accounts.AccountManager; 21 import android.app.SearchManager; 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.UriMatcher; 28 import android.content.res.Resources; 29 import android.content.res.TypedArray; 30 import android.database.AbstractCursor; 31 import android.database.ContentObserver; 32 import android.database.Cursor; 33 import android.database.DatabaseUtils; 34 import android.database.MatrixCursor; 35 import android.database.sqlite.SQLiteDatabase; 36 import android.database.sqlite.SQLiteOpenHelper; 37 import android.database.sqlite.SQLiteQueryBuilder; 38 import android.net.Uri; 39 import android.provider.BaseColumns; 40 import android.provider.Browser; 41 import android.provider.Browser.BookmarkColumns; 42 import android.provider.BrowserContract; 43 import android.provider.BrowserContract.Accounts; 44 import android.provider.BrowserContract.Bookmarks; 45 import android.provider.BrowserContract.ChromeSyncColumns; 46 import android.provider.BrowserContract.Combined; 47 import android.provider.BrowserContract.History; 48 import android.provider.BrowserContract.Images; 49 import android.provider.BrowserContract.Searches; 50 import android.provider.BrowserContract.Settings; 51 import android.provider.BrowserContract.SyncState; 52 import android.provider.ContactsContract.RawContacts; 53 import android.provider.SyncStateContract; 54 import android.text.TextUtils; 55 56 import com.android.browser.R; 57 import com.android.browser.UrlUtils; 58 import com.android.browser.widget.BookmarkThumbnailWidgetProvider; 59 import com.android.common.content.SyncStateContentProviderHelper; 60 import com.google.common.annotations.VisibleForTesting; 61 62 import java.io.ByteArrayOutputStream; 63 import java.io.File; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.util.Arrays; 67 import java.util.HashMap; 68 69 public class BrowserProvider2 extends SQLiteContentProvider { 70 71 public static final String PARAM_GROUP_BY = "groupBy"; 72 public static final String PARAM_ALLOW_EMPTY_ACCOUNTS = "allowEmptyAccounts"; 73 74 public static final String LEGACY_AUTHORITY = "browser"; 75 static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder() 76 .authority(LEGACY_AUTHORITY).scheme("content").build(); 77 78 public static interface Thumbnails { 79 public static final Uri CONTENT_URI = Uri.withAppendedPath( 80 BrowserContract.AUTHORITY_URI, "thumbnails"); 81 public static final String _ID = "_id"; 82 public static final String THUMBNAIL = "thumbnail"; 83 } 84 85 public static interface OmniboxSuggestions { 86 public static final Uri CONTENT_URI = Uri.withAppendedPath( 87 BrowserContract.AUTHORITY_URI, "omnibox_suggestions"); 88 public static final String _ID = "_id"; 89 public static final String URL = "url"; 90 public static final String TITLE = "title"; 91 public static final String IS_BOOKMARK = "bookmark"; 92 } 93 94 static final String TABLE_BOOKMARKS = "bookmarks"; 95 static final String TABLE_HISTORY = "history"; 96 static final String TABLE_IMAGES = "images"; 97 static final String TABLE_SEARCHES = "searches"; 98 static final String TABLE_SYNC_STATE = "syncstate"; 99 static final String TABLE_SETTINGS = "settings"; 100 static final String TABLE_SNAPSHOTS = "snapshots"; 101 static final String TABLE_THUMBNAILS = "thumbnails"; 102 103 static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " + 104 "ON bookmarks.url = images." + Images.URL; 105 static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " + 106 "ON history.url = images." + Images.URL; 107 108 static final String VIEW_ACCOUNTS = "v_accounts"; 109 static final String VIEW_SNAPSHOTS_COMBINED = "v_snapshots_combined"; 110 static final String VIEW_OMNIBOX_SUGGESTIONS = "v_omnibox_suggestions"; 111 112 static final String FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES = 113 "history LEFT OUTER JOIN (%s) bookmarks " + 114 "ON history.url = bookmarks.url LEFT OUTER JOIN images " + 115 "ON history.url = images.url_key"; 116 117 static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC"; 118 static final String DEFAULT_SORT_ACCOUNTS = 119 Accounts.ACCOUNT_NAME + " IS NOT NULL DESC, " 120 + Accounts.ACCOUNT_NAME + " ASC"; 121 122 private static final String TABLE_BOOKMARKS_JOIN_HISTORY = 123 "history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url"; 124 125 private static final String[] SUGGEST_PROJECTION = new String[] { 126 qualifyColumn(TABLE_HISTORY, History._ID), 127 qualifyColumn(TABLE_HISTORY, History.URL), 128 bookmarkOrHistoryColumn(Combined.TITLE), 129 bookmarkOrHistoryLiteral(Combined.URL, 130 Integer.toString(R.drawable.ic_bookmark_off_holo_dark), 131 Integer.toString(R.drawable.ic_history_holo_dark)), 132 qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED)}; 133 134 private static final String SUGGEST_SELECTION = 135 "history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ?" 136 + " OR history.title LIKE ? OR bookmarks.title LIKE ?"; 137 138 private static final String ZERO_QUERY_SUGGEST_SELECTION = 139 TABLE_HISTORY + "." + History.DATE_LAST_VISITED + " != 0"; 140 141 private static final String SUGGEST_ORDER_BY = 142 TABLE_HISTORY + "." + History.DATE_LAST_VISITED + " DESC"; 143 144 private static final String IMAGE_PRUNE = 145 "url_key NOT IN (SELECT url FROM bookmarks " + 146 "WHERE url IS NOT NULL AND deleted == 0) AND url_key NOT IN " + 147 "(SELECT url FROM history WHERE url IS NOT NULL)"; 148 149 static final int THUMBNAILS = 10; 150 static final int THUMBNAILS_ID = 11; 151 static final int OMNIBOX_SUGGESTIONS = 20; 152 153 static final int BOOKMARKS = 1000; 154 static final int BOOKMARKS_ID = 1001; 155 static final int BOOKMARKS_FOLDER = 1002; 156 static final int BOOKMARKS_FOLDER_ID = 1003; 157 static final int BOOKMARKS_SUGGESTIONS = 1004; 158 static final int BOOKMARKS_DEFAULT_FOLDER_ID = 1005; 159 160 static final int HISTORY = 2000; 161 static final int HISTORY_ID = 2001; 162 163 static final int SEARCHES = 3000; 164 static final int SEARCHES_ID = 3001; 165 166 static final int SYNCSTATE = 4000; 167 static final int SYNCSTATE_ID = 4001; 168 169 static final int IMAGES = 5000; 170 171 static final int COMBINED = 6000; 172 static final int COMBINED_ID = 6001; 173 174 static final int ACCOUNTS = 7000; 175 176 static final int SETTINGS = 8000; 177 178 static final int LEGACY = 9000; 179 static final int LEGACY_ID = 9001; 180 181 public static final long FIXED_ID_ROOT = 1; 182 183 // Default sort order for unsync'd bookmarks 184 static final String DEFAULT_BOOKMARKS_SORT_ORDER = 185 Bookmarks.IS_FOLDER + " DESC, position ASC, _id ASC"; 186 187 // Default sort order for sync'd bookmarks 188 static final String DEFAULT_BOOKMARKS_SORT_ORDER_SYNC = "position ASC, _id ASC"; 189 190 static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 191 192 static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>(); 193 static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>(); 194 static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP = 195 new HashMap<String, String>(); 196 static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>(); 197 static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>(); 198 static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>(); 199 static final HashMap<String, String> COMBINED_HISTORY_PROJECTION_MAP = new HashMap<String, String>(); 200 static final HashMap<String, String> COMBINED_BOOKMARK_PROJECTION_MAP = new HashMap<String, String>(); 201 static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>(); 202 static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>(); 203 204 static { 205 final UriMatcher matcher = URI_MATCHER; 206 final String authority = BrowserContract.AUTHORITY; 207 matcher.addURI(authority, "accounts", ACCOUNTS); 208 matcher.addURI(authority, "bookmarks", BOOKMARKS); 209 matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID); 210 matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER); 211 matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); 212 matcher.addURI(authority, "bookmarks/folder/id", BOOKMARKS_DEFAULT_FOLDER_ID); 213 matcher.addURI(authority, 214 SearchManager.SUGGEST_URI_PATH_QUERY, 215 BOOKMARKS_SUGGESTIONS); 216 matcher.addURI(authority, 217 "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY, 218 BOOKMARKS_SUGGESTIONS); 219 matcher.addURI(authority, "history", HISTORY); 220 matcher.addURI(authority, "history/#", HISTORY_ID); 221 matcher.addURI(authority, "searches", SEARCHES); 222 matcher.addURI(authority, "searches/#", SEARCHES_ID); 223 matcher.addURI(authority, "syncstate", SYNCSTATE); 224 matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID); 225 matcher.addURI(authority, "images", IMAGES); 226 matcher.addURI(authority, "combined", COMBINED); 227 matcher.addURI(authority, "combined/#", COMBINED_ID); 228 matcher.addURI(authority, "settings", SETTINGS); 229 matcher.addURI(authority, "thumbnails", THUMBNAILS); 230 matcher.addURI(authority, "thumbnails/#", THUMBNAILS_ID); 231 matcher.addURI(authority, "omnibox_suggestions", OMNIBOX_SUGGESTIONS); 232 233 // Legacy 234 matcher.addURI(LEGACY_AUTHORITY, "searches", SEARCHES); 235 matcher.addURI(LEGACY_AUTHORITY, "searches/#", SEARCHES_ID); 236 matcher.addURI(LEGACY_AUTHORITY, "bookmarks", LEGACY); 237 matcher.addURI(LEGACY_AUTHORITY, "bookmarks/#", LEGACY_ID); 238 matcher.addURI(LEGACY_AUTHORITY, 239 SearchManager.SUGGEST_URI_PATH_QUERY, 240 BOOKMARKS_SUGGESTIONS); 241 matcher.addURI(LEGACY_AUTHORITY, 242 "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY, 243 BOOKMARKS_SUGGESTIONS); 244 245 // Projection maps 246 HashMap<String, String> map; 247 248 // Accounts 249 map = ACCOUNTS_PROJECTION_MAP; 250 map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE); 251 map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME); 252 map.put(Accounts.ROOT_ID, Accounts.ROOT_ID); 253 254 // Bookmarks 255 map = BOOKMARKS_PROJECTION_MAP; 256 map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID)); 257 map.put(Bookmarks.TITLE, Bookmarks.TITLE); 258 map.put(Bookmarks.URL, Bookmarks.URL); 259 map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); 260 map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL); 261 map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON); 262 map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER); 263 map.put(Bookmarks.PARENT, Bookmarks.PARENT); 264 map.put(Bookmarks.POSITION, Bookmarks.POSITION); 265 map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER); 266 map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); 267 map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME); 268 map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE); 269 map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID); 270 map.put(Bookmarks.VERSION, Bookmarks.VERSION); 271 map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); 272 map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); 273 map.put(Bookmarks.DIRTY, Bookmarks.DIRTY); 274 map.put(Bookmarks.SYNC1, Bookmarks.SYNC1); 275 map.put(Bookmarks.SYNC2, Bookmarks.SYNC2); 276 map.put(Bookmarks.SYNC3, Bookmarks.SYNC3); 277 map.put(Bookmarks.SYNC4, Bookmarks.SYNC4); 278 map.put(Bookmarks.SYNC5, Bookmarks.SYNC5); 279 map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + 280 " FROM " + TABLE_BOOKMARKS + " A WHERE " + 281 "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT + 282 ") AS " + Bookmarks.PARENT_SOURCE_ID); 283 map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID + 284 " FROM " + TABLE_BOOKMARKS + " A WHERE " + 285 "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER + 286 ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID); 287 288 // Other bookmarks 289 OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP); 290 OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION, 291 Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION); 292 293 // History 294 map = HISTORY_PROJECTION_MAP; 295 map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID)); 296 map.put(History.TITLE, History.TITLE); 297 map.put(History.URL, History.URL); 298 map.put(History.FAVICON, History.FAVICON); 299 map.put(History.THUMBNAIL, History.THUMBNAIL); 300 map.put(History.TOUCH_ICON, History.TOUCH_ICON); 301 map.put(History.DATE_CREATED, History.DATE_CREATED); 302 map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); 303 map.put(History.VISITS, History.VISITS); 304 map.put(History.USER_ENTERED, History.USER_ENTERED); 305 306 // Sync state 307 map = SYNC_STATE_PROJECTION_MAP; 308 map.put(SyncState._ID, SyncState._ID); 309 map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME); 310 map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE); 311 map.put(SyncState.DATA, SyncState.DATA); 312 313 // Images 314 map = IMAGES_PROJECTION_MAP; 315 map.put(Images.URL, Images.URL); 316 map.put(Images.FAVICON, Images.FAVICON); 317 map.put(Images.THUMBNAIL, Images.THUMBNAIL); 318 map.put(Images.TOUCH_ICON, Images.TOUCH_ICON); 319 320 // Combined history half 321 map = COMBINED_HISTORY_PROJECTION_MAP; 322 map.put(Combined._ID, bookmarkOrHistoryColumn(Combined._ID)); 323 map.put(Combined.TITLE, bookmarkOrHistoryColumn(Combined.TITLE)); 324 map.put(Combined.URL, qualifyColumn(TABLE_HISTORY, Combined.URL)); 325 map.put(Combined.DATE_CREATED, qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED)); 326 map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); 327 map.put(Combined.IS_BOOKMARK, "CASE WHEN " + 328 TABLE_BOOKMARKS + "." + Bookmarks._ID + 329 " IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK); 330 map.put(Combined.VISITS, Combined.VISITS); 331 map.put(Combined.FAVICON, Combined.FAVICON); 332 map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); 333 map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); 334 map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED); 335 336 // Combined bookmark half 337 map = COMBINED_BOOKMARK_PROJECTION_MAP; 338 map.put(Combined._ID, Combined._ID); 339 map.put(Combined.TITLE, Combined.TITLE); 340 map.put(Combined.URL, Combined.URL); 341 map.put(Combined.DATE_CREATED, Combined.DATE_CREATED); 342 map.put(Combined.DATE_LAST_VISITED, "NULL AS " + Combined.DATE_LAST_VISITED); 343 map.put(Combined.IS_BOOKMARK, "1 AS " + Combined.IS_BOOKMARK); 344 map.put(Combined.VISITS, "0 AS " + Combined.VISITS); 345 map.put(Combined.FAVICON, Combined.FAVICON); 346 map.put(Combined.THUMBNAIL, Combined.THUMBNAIL); 347 map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON); 348 map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED); 349 350 // Searches 351 map = SEARCHES_PROJECTION_MAP; 352 map.put(Searches._ID, Searches._ID); 353 map.put(Searches.SEARCH, Searches.SEARCH); 354 map.put(Searches.DATE, Searches.DATE); 355 356 // Settings 357 map = SETTINGS_PROJECTION_MAP; 358 map.put(Settings.KEY, Settings.KEY); 359 map.put(Settings.VALUE, Settings.VALUE); 360 } 361 362 static final String bookmarkOrHistoryColumn(String column) { 363 return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " + 364 "bookmarks." + column + " ELSE history." + column + " END AS " + column; 365 } 366 367 static final String bookmarkOrHistoryLiteral(String column, String bookmarkValue, 368 String historyValue) { 369 return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN \"" + bookmarkValue + 370 "\" ELSE \"" + historyValue + "\" END"; 371 } 372 373 static final String qualifyColumn(String table, String column) { 374 return table + "." + column + " AS " + column; 375 } 376 377 DatabaseHelper mOpenHelper; 378 SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper(); 379 // This is so provider tests can intercept widget updating 380 ContentObserver mWidgetObserver = null; 381 boolean mUpdateWidgets = false; 382 boolean mSyncToNetwork = true; 383 384 final class DatabaseHelper extends SQLiteOpenHelper { 385 static final String DATABASE_NAME = "browser2.db"; 386 static final int DATABASE_VERSION = 32; 387 public DatabaseHelper(Context context) { 388 super(context, DATABASE_NAME, null, DATABASE_VERSION); 389 } 390 391 @Override 392 public void onCreate(SQLiteDatabase db) { 393 db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" + 394 Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 395 Bookmarks.TITLE + " TEXT," + 396 Bookmarks.URL + " TEXT," + 397 Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," + 398 Bookmarks.PARENT + " INTEGER," + 399 Bookmarks.POSITION + " INTEGER NOT NULL," + 400 Bookmarks.INSERT_AFTER + " INTEGER," + 401 Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," + 402 Bookmarks.ACCOUNT_NAME + " TEXT," + 403 Bookmarks.ACCOUNT_TYPE + " TEXT," + 404 Bookmarks.SOURCE_ID + " TEXT," + 405 Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," + 406 Bookmarks.DATE_CREATED + " INTEGER," + 407 Bookmarks.DATE_MODIFIED + " INTEGER," + 408 Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 409 Bookmarks.SYNC1 + " TEXT," + 410 Bookmarks.SYNC2 + " TEXT," + 411 Bookmarks.SYNC3 + " TEXT," + 412 Bookmarks.SYNC4 + " TEXT," + 413 Bookmarks.SYNC5 + " TEXT" + 414 ");"); 415 416 // TODO indices 417 418 db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" + 419 History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 420 History.TITLE + " TEXT," + 421 History.URL + " TEXT NOT NULL," + 422 History.DATE_CREATED + " INTEGER," + 423 History.DATE_LAST_VISITED + " INTEGER," + 424 History.VISITS + " INTEGER NOT NULL DEFAULT 0," + 425 History.USER_ENTERED + " INTEGER" + 426 ");"); 427 428 db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" + 429 Images.URL + " TEXT UNIQUE NOT NULL," + 430 Images.FAVICON + " BLOB," + 431 Images.THUMBNAIL + " BLOB," + 432 Images.TOUCH_ICON + " BLOB" + 433 ");"); 434 db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES + 435 "(" + Images.URL + ")"); 436 437 db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" + 438 Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 439 Searches.SEARCH + " TEXT," + 440 Searches.DATE + " LONG" + 441 ");"); 442 443 db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" + 444 Settings.KEY + " TEXT PRIMARY KEY," + 445 Settings.VALUE + " TEXT NOT NULL" + 446 ");"); 447 448 createAccountsView(db); 449 createThumbnails(db); 450 451 mSyncHelper.createDatabase(db); 452 453 if (!importFromBrowserProvider(db)) { 454 createDefaultBookmarks(db); 455 } 456 457 enableSync(db); 458 createOmniboxSuggestions(db); 459 } 460 461 void createOmniboxSuggestions(SQLiteDatabase db) { 462 db.execSQL(SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS); 463 } 464 465 void createThumbnails(SQLiteDatabase db) { 466 db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_THUMBNAILS + " (" + 467 Thumbnails._ID + " INTEGER PRIMARY KEY," + 468 Thumbnails.THUMBNAIL + " BLOB NOT NULL" + 469 ");"); 470 } 471 472 void enableSync(SQLiteDatabase db) { 473 ContentValues values = new ContentValues(); 474 values.put(Settings.KEY, Settings.KEY_SYNC_ENABLED); 475 values.put(Settings.VALUE, 1); 476 insertSettingsInTransaction(db, values); 477 // Enable bookmark sync on all accounts 478 AccountManager am = (AccountManager) getContext().getSystemService( 479 Context.ACCOUNT_SERVICE); 480 if (am == null) { 481 return; 482 } 483 Account[] accounts = am.getAccountsByType("com.google"); 484 if (accounts == null || accounts.length == 0) { 485 return; 486 } 487 for (Account account : accounts) { 488 if (ContentResolver.getIsSyncable( 489 account, BrowserContract.AUTHORITY) == 0) { 490 // Account wasn't syncable, enable it 491 ContentResolver.setIsSyncable( 492 account, BrowserContract.AUTHORITY, 1); 493 ContentResolver.setSyncAutomatically( 494 account, BrowserContract.AUTHORITY, true); 495 } 496 } 497 } 498 499 boolean importFromBrowserProvider(SQLiteDatabase db) { 500 Context context = getContext(); 501 File oldDbFile = context.getDatabasePath(BrowserProvider.sDatabaseName); 502 if (oldDbFile.exists()) { 503 BrowserProvider.DatabaseHelper helper = 504 new BrowserProvider.DatabaseHelper(context); 505 SQLiteDatabase oldDb = helper.getWritableDatabase(); 506 Cursor c = null; 507 try { 508 String table = BrowserProvider.TABLE_NAMES[BrowserProvider.URI_MATCH_BOOKMARKS]; 509 // Import bookmarks 510 c = oldDb.query(table, 511 new String[] { 512 BookmarkColumns.URL, // 0 513 BookmarkColumns.TITLE, // 1 514 BookmarkColumns.FAVICON, // 2 515 BookmarkColumns.TOUCH_ICON, // 3 516 BookmarkColumns.CREATED, // 4 517 }, BookmarkColumns.BOOKMARK + "!=0", null, 518 null, null, null); 519 if (c != null) { 520 while (c.moveToNext()) { 521 ContentValues values = new ContentValues(); 522 values.put(Bookmarks.URL, c.getString(0)); 523 values.put(Bookmarks.TITLE, c.getString(1)); 524 values.put(Bookmarks.DATE_CREATED, c.getInt(4)); 525 values.put(Bookmarks.POSITION, 0); 526 values.put(Bookmarks.PARENT, FIXED_ID_ROOT); 527 ContentValues imageValues = new ContentValues(); 528 imageValues.put(Images.URL, c.getString(0)); 529 imageValues.put(Images.FAVICON, c.getBlob(2)); 530 imageValues.put(Images.TOUCH_ICON, c.getBlob(3)); 531 db.insertOrThrow(TABLE_IMAGES, Images.THUMBNAIL, imageValues); 532 db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); 533 } 534 c.close(); 535 } 536 // Import history 537 c = oldDb.query(table, 538 new String[] { 539 BookmarkColumns.URL, // 0 540 BookmarkColumns.TITLE, // 1 541 BookmarkColumns.VISITS, // 2 542 BookmarkColumns.DATE, // 3 543 BookmarkColumns.CREATED, // 4 544 }, BookmarkColumns.VISITS + " > 0 OR " 545 + BookmarkColumns.BOOKMARK + " = 0", 546 null, null, null, null); 547 if (c != null) { 548 while (c.moveToNext()) { 549 ContentValues values = new ContentValues(); 550 values.put(History.URL, c.getString(0)); 551 values.put(History.TITLE, c.getString(1)); 552 values.put(History.VISITS, c.getInt(2)); 553 values.put(History.DATE_LAST_VISITED, c.getLong(3)); 554 values.put(History.DATE_CREATED, c.getLong(4)); 555 db.insertOrThrow(TABLE_HISTORY, History.FAVICON, values); 556 } 557 c.close(); 558 } 559 // Wipe the old DB, in case the delete fails. 560 oldDb.delete(table, null, null); 561 } finally { 562 if (c != null) c.close(); 563 oldDb.close(); 564 helper.close(); 565 } 566 if (!oldDbFile.delete()) { 567 oldDbFile.deleteOnExit(); 568 } 569 return true; 570 } 571 return false; 572 } 573 574 void createAccountsView(SQLiteDatabase db) { 575 db.execSQL("CREATE VIEW IF NOT EXISTS v_accounts AS " 576 + "SELECT NULL AS " + Accounts.ACCOUNT_NAME 577 + ", NULL AS " + Accounts.ACCOUNT_TYPE 578 + ", " + FIXED_ID_ROOT + " AS " + Accounts.ROOT_ID 579 + " UNION ALL SELECT " + Accounts.ACCOUNT_NAME 580 + ", " + Accounts.ACCOUNT_TYPE + ", " 581 + Bookmarks._ID + " AS " + Accounts.ROOT_ID 582 + " FROM " + TABLE_BOOKMARKS + " WHERE " 583 + ChromeSyncColumns.SERVER_UNIQUE + " = \"" 584 + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "\" AND " 585 + Bookmarks.IS_DELETED + " = 0"); 586 } 587 588 @Override 589 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 590 if (oldVersion < 32) { 591 createOmniboxSuggestions(db); 592 } 593 if (oldVersion < 31) { 594 createThumbnails(db); 595 } 596 if (oldVersion < 30) { 597 db.execSQL("DROP VIEW IF EXISTS " + VIEW_SNAPSHOTS_COMBINED); 598 db.execSQL("DROP TABLE IF EXISTS " + TABLE_SNAPSHOTS); 599 } 600 if (oldVersion < 28) { 601 enableSync(db); 602 } 603 if (oldVersion < 27) { 604 createAccountsView(db); 605 } 606 if (oldVersion < 26) { 607 db.execSQL("DROP VIEW IF EXISTS combined"); 608 } 609 if (oldVersion < 25) { 610 db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS); 611 db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY); 612 db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES); 613 db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES); 614 db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS); 615 mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info 616 onCreate(db); 617 } 618 } 619 620 public void onOpen(SQLiteDatabase db) { 621 db.enableWriteAheadLogging(); 622 mSyncHelper.onDatabaseOpened(db); 623 } 624 625 private void createDefaultBookmarks(SQLiteDatabase db) { 626 ContentValues values = new ContentValues(); 627 // TODO figure out how to deal with localization for the defaults 628 629 // Bookmarks folder 630 values.put(Bookmarks._ID, FIXED_ID_ROOT); 631 values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS); 632 values.put(Bookmarks.TITLE, "Bookmarks"); 633 values.putNull(Bookmarks.PARENT); 634 values.put(Bookmarks.POSITION, 0); 635 values.put(Bookmarks.IS_FOLDER, true); 636 values.put(Bookmarks.DIRTY, true); 637 db.insertOrThrow(TABLE_BOOKMARKS, null, values); 638 639 addDefaultBookmarks(db, FIXED_ID_ROOT); 640 } 641 642 private void addDefaultBookmarks(SQLiteDatabase db, long parentId) { 643 Resources res = getContext().getResources(); 644 final CharSequence[] bookmarks = res.getTextArray( 645 R.array.bookmarks); 646 int size = bookmarks.length; 647 TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads); 648 try { 649 String parent = Long.toString(parentId); 650 String now = Long.toString(System.currentTimeMillis()); 651 for (int i = 0; i < size; i = i + 2) { 652 CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(), 653 bookmarks[i + 1]); 654 db.execSQL("INSERT INTO bookmarks (" + 655 Bookmarks.TITLE + ", " + 656 Bookmarks.URL + ", " + 657 Bookmarks.IS_FOLDER + "," + 658 Bookmarks.PARENT + "," + 659 Bookmarks.POSITION + "," + 660 Bookmarks.DATE_CREATED + 661 ") VALUES (" + 662 "'" + bookmarks[i] + "', " + 663 "'" + bookmarkDestination + "', " + 664 "0," + 665 parent + "," + 666 Integer.toString(i) + "," + 667 now + 668 ");"); 669 670 int faviconId = preloads.getResourceId(i, 0); 671 int thumbId = preloads.getResourceId(i + 1, 0); 672 byte[] thumb = null, favicon = null; 673 try { 674 thumb = readRaw(res, thumbId); 675 } catch (IOException e) { 676 } 677 try { 678 favicon = readRaw(res, faviconId); 679 } catch (IOException e) { 680 } 681 if (thumb != null || favicon != null) { 682 ContentValues imageValues = new ContentValues(); 683 imageValues.put(Images.URL, bookmarkDestination.toString()); 684 if (favicon != null) { 685 imageValues.put(Images.FAVICON, favicon); 686 } 687 if (thumb != null) { 688 imageValues.put(Images.THUMBNAIL, thumb); 689 } 690 db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); 691 } 692 } 693 } catch (ArrayIndexOutOfBoundsException e) { 694 } 695 } 696 697 private byte[] readRaw(Resources res, int id) throws IOException { 698 if (id == 0) { 699 return null; 700 } 701 InputStream is = res.openRawResource(id); 702 try { 703 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 704 byte[] buf = new byte[4096]; 705 int read; 706 while ((read = is.read(buf)) > 0) { 707 bos.write(buf, 0, read); 708 } 709 bos.flush(); 710 return bos.toByteArray(); 711 } finally { 712 is.close(); 713 } 714 } 715 716 // XXX: This is a major hack to remove our dependency on gsf constants and 717 // its content provider. http://b/issue?id=2425179 718 private String getClientId(ContentResolver cr) { 719 String ret = "android-google"; 720 Cursor c = null; 721 try { 722 c = cr.query(Uri.parse("content://com.google.settings/partner"), 723 new String[] { "value" }, "name='client_id'", null, null); 724 if (c != null && c.moveToNext()) { 725 ret = c.getString(0); 726 } 727 } catch (RuntimeException ex) { 728 // fall through to return the default 729 } finally { 730 if (c != null) { 731 c.close(); 732 } 733 } 734 return ret; 735 } 736 737 private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) { 738 StringBuffer sb = new StringBuffer(); 739 int lastCharLoc = 0; 740 741 final String client_id = getClientId(context.getContentResolver()); 742 743 for (int i = 0; i < srcString.length(); ++i) { 744 char c = srcString.charAt(i); 745 if (c == '{') { 746 sb.append(srcString.subSequence(lastCharLoc, i)); 747 lastCharLoc = i; 748 inner: 749 for (int j = i; j < srcString.length(); ++j) { 750 char k = srcString.charAt(j); 751 if (k == '}') { 752 String propertyKeyValue = srcString.subSequence(i + 1, j).toString(); 753 if (propertyKeyValue.equals("CLIENT_ID")) { 754 sb.append(client_id); 755 } else { 756 sb.append("unknown"); 757 } 758 lastCharLoc = j + 1; 759 i = j; 760 break inner; 761 } 762 } 763 } 764 } 765 if (srcString.length() - lastCharLoc > 0) { 766 // Put on the tail, if there is one 767 sb.append(srcString.subSequence(lastCharLoc, srcString.length())); 768 } 769 return sb; 770 } 771 } 772 773 @Override 774 public SQLiteOpenHelper getDatabaseHelper(Context context) { 775 synchronized (this) { 776 if (mOpenHelper == null) { 777 mOpenHelper = new DatabaseHelper(context); 778 } 779 return mOpenHelper; 780 } 781 } 782 783 @Override 784 public boolean isCallerSyncAdapter(Uri uri) { 785 return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false); 786 } 787 788 @VisibleForTesting 789 public void setWidgetObserver(ContentObserver obs) { 790 mWidgetObserver = obs; 791 } 792 793 void refreshWidgets() { 794 mUpdateWidgets = true; 795 } 796 797 @Override 798 protected void onEndTransaction(boolean callerIsSyncAdapter) { 799 super.onEndTransaction(callerIsSyncAdapter); 800 if (mUpdateWidgets) { 801 if (mWidgetObserver == null) { 802 BookmarkThumbnailWidgetProvider.refreshWidgets(getContext()); 803 } else { 804 mWidgetObserver.dispatchChange(false); 805 } 806 mUpdateWidgets = false; 807 } 808 mSyncToNetwork = true; 809 } 810 811 @Override 812 public String getType(Uri uri) { 813 final int match = URI_MATCHER.match(uri); 814 switch (match) { 815 case LEGACY: 816 case BOOKMARKS: 817 return Bookmarks.CONTENT_TYPE; 818 case LEGACY_ID: 819 case BOOKMARKS_ID: 820 return Bookmarks.CONTENT_ITEM_TYPE; 821 case HISTORY: 822 return History.CONTENT_TYPE; 823 case HISTORY_ID: 824 return History.CONTENT_ITEM_TYPE; 825 case SEARCHES: 826 return Searches.CONTENT_TYPE; 827 case SEARCHES_ID: 828 return Searches.CONTENT_ITEM_TYPE; 829 } 830 return null; 831 } 832 833 boolean isNullAccount(String account) { 834 if (account == null) return true; 835 account = account.trim(); 836 return account.length() == 0 || account.equals("null"); 837 } 838 839 Object[] getSelectionWithAccounts(Uri uri, String selection, String[] selectionArgs) { 840 // Look for account info 841 String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); 842 String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); 843 boolean hasAccounts = false; 844 if (accountType != null && accountName != null) { 845 if (!isNullAccount(accountType) && !isNullAccount(accountName)) { 846 selection = DatabaseUtils.concatenateWhere(selection, 847 Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? "); 848 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 849 new String[] { accountType, accountName }); 850 hasAccounts = true; 851 } else { 852 selection = DatabaseUtils.concatenateWhere(selection, 853 Bookmarks.ACCOUNT_NAME + " IS NULL AND " + 854 Bookmarks.ACCOUNT_TYPE + " IS NULL"); 855 } 856 } 857 return new Object[] { selection, selectionArgs, hasAccounts }; 858 } 859 860 @Override 861 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 862 String sortOrder) { 863 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 864 final int match = URI_MATCHER.match(uri); 865 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 866 String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); 867 String groupBy = uri.getQueryParameter(PARAM_GROUP_BY); 868 switch (match) { 869 case ACCOUNTS: { 870 qb.setTables(VIEW_ACCOUNTS); 871 qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP); 872 String allowEmpty = uri.getQueryParameter(PARAM_ALLOW_EMPTY_ACCOUNTS); 873 if ("false".equals(allowEmpty)) { 874 selection = DatabaseUtils.concatenateWhere(selection, 875 SQL_WHERE_ACCOUNT_HAS_BOOKMARKS); 876 } 877 if (sortOrder == null) { 878 sortOrder = DEFAULT_SORT_ACCOUNTS; 879 } 880 break; 881 } 882 883 case BOOKMARKS_FOLDER_ID: 884 case BOOKMARKS_ID: 885 case BOOKMARKS: { 886 // Only show deleted bookmarks if requested to do so 887 if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)) { 888 selection = DatabaseUtils.concatenateWhere( 889 Bookmarks.IS_DELETED + "=0", selection); 890 } 891 892 if (match == BOOKMARKS_ID) { 893 // Tack on the ID of the specific bookmark requested 894 selection = DatabaseUtils.concatenateWhere(selection, 895 TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?"); 896 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 897 new String[] { Long.toString(ContentUris.parseId(uri)) }); 898 } else if (match == BOOKMARKS_FOLDER_ID) { 899 // Tack on the ID of the specific folder requested 900 selection = DatabaseUtils.concatenateWhere(selection, 901 TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?"); 902 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 903 new String[] { Long.toString(ContentUris.parseId(uri)) }); 904 } 905 906 Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); 907 selection = (String) withAccount[0]; 908 selectionArgs = (String[]) withAccount[1]; 909 boolean hasAccounts = (Boolean) withAccount[2]; 910 911 // Set a default sort order if one isn't specified 912 if (TextUtils.isEmpty(sortOrder)) { 913 if (hasAccounts) { 914 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; 915 } else { 916 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; 917 } 918 } 919 920 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); 921 qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); 922 break; 923 } 924 925 case BOOKMARKS_FOLDER: { 926 // Look for an account 927 boolean useAccount = false; 928 String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); 929 String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); 930 if (!isNullAccount(accountType) && !isNullAccount(accountName)) { 931 useAccount = true; 932 } 933 934 qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); 935 String[] args; 936 String query; 937 // Set a default sort order if one isn't specified 938 if (TextUtils.isEmpty(sortOrder)) { 939 if (useAccount) { 940 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC; 941 } else { 942 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; 943 } 944 } 945 if (!useAccount) { 946 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); 947 String where = Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0"; 948 where = DatabaseUtils.concatenateWhere(where, selection); 949 args = new String[] { Long.toString(FIXED_ID_ROOT) }; 950 if (selectionArgs != null) { 951 args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); 952 } 953 query = qb.buildQuery(projection, where, null, null, sortOrder, null); 954 } else { 955 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); 956 String where = Bookmarks.ACCOUNT_TYPE + "=? AND " + 957 Bookmarks.ACCOUNT_NAME + "=? " + 958 "AND parent = " + 959 "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " + 960 ChromeSyncColumns.SERVER_UNIQUE + "=" + 961 "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " + 962 "AND account_type = ? AND account_name = ?) " + 963 "AND " + Bookmarks.IS_DELETED + "=0"; 964 where = DatabaseUtils.concatenateWhere(where, selection); 965 String bookmarksBarQuery = qb.buildQuery(projection, 966 where, null, null, null, null); 967 args = new String[] {accountType, accountName, 968 accountType, accountName}; 969 if (selectionArgs != null) { 970 args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); 971 } 972 973 where = Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" + 974 " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?"; 975 where = DatabaseUtils.concatenateWhere(where, selection); 976 qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP); 977 String otherBookmarksQuery = qb.buildQuery(projection, 978 where, null, null, null, null); 979 980 query = qb.buildUnionQuery( 981 new String[] { bookmarksBarQuery, otherBookmarksQuery }, 982 sortOrder, limit); 983 984 args = DatabaseUtils.appendSelectionArgs(args, new String[] { 985 accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS, 986 }); 987 if (selectionArgs != null) { 988 args = DatabaseUtils.appendSelectionArgs(args, selectionArgs); 989 } 990 } 991 992 Cursor cursor = db.rawQuery(query, args); 993 if (cursor != null) { 994 cursor.setNotificationUri(getContext().getContentResolver(), 995 BrowserContract.AUTHORITY_URI); 996 } 997 return cursor; 998 } 999 1000 case BOOKMARKS_DEFAULT_FOLDER_ID: { 1001 String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME); 1002 String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE); 1003 long id = queryDefaultFolderId(accountName, accountType); 1004 MatrixCursor c = new MatrixCursor(new String[] {Bookmarks._ID}); 1005 c.newRow().add(id); 1006 return c; 1007 } 1008 1009 case BOOKMARKS_SUGGESTIONS: { 1010 return doSuggestQuery(selection, selectionArgs, limit); 1011 } 1012 1013 case HISTORY_ID: { 1014 selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); 1015 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1016 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1017 // fall through 1018 } 1019 case HISTORY: { 1020 filterSearchClient(selectionArgs); 1021 if (sortOrder == null) { 1022 sortOrder = DEFAULT_SORT_HISTORY; 1023 } 1024 qb.setProjectionMap(HISTORY_PROJECTION_MAP); 1025 qb.setTables(TABLE_HISTORY_JOIN_IMAGES); 1026 break; 1027 } 1028 1029 case SEARCHES_ID: { 1030 selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); 1031 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1032 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1033 // fall through 1034 } 1035 case SEARCHES: { 1036 qb.setTables(TABLE_SEARCHES); 1037 qb.setProjectionMap(SEARCHES_PROJECTION_MAP); 1038 break; 1039 } 1040 1041 case SYNCSTATE: { 1042 return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder); 1043 } 1044 1045 case SYNCSTATE_ID: { 1046 selection = appendAccountToSelection(uri, selection); 1047 String selectionWithId = 1048 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 1049 + (selection == null ? "" : " AND (" + selection + ")"); 1050 return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder); 1051 } 1052 1053 case IMAGES: { 1054 qb.setTables(TABLE_IMAGES); 1055 qb.setProjectionMap(IMAGES_PROJECTION_MAP); 1056 break; 1057 } 1058 1059 case LEGACY_ID: 1060 case COMBINED_ID: { 1061 selection = DatabaseUtils.concatenateWhere( 1062 selection, Combined._ID + " = CAST(? AS INTEGER)"); 1063 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1064 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1065 // fall through 1066 } 1067 case LEGACY: 1068 case COMBINED: { 1069 if ((match == LEGACY || match == LEGACY_ID) 1070 && projection == null) { 1071 projection = Browser.HISTORY_PROJECTION; 1072 } 1073 String[] args = createCombinedQuery(uri, projection, qb); 1074 if (selectionArgs == null) { 1075 selectionArgs = args; 1076 } else { 1077 selectionArgs = DatabaseUtils.appendSelectionArgs(args, selectionArgs); 1078 } 1079 break; 1080 } 1081 1082 case SETTINGS: { 1083 qb.setTables(TABLE_SETTINGS); 1084 qb.setProjectionMap(SETTINGS_PROJECTION_MAP); 1085 break; 1086 } 1087 1088 case THUMBNAILS_ID: { 1089 selection = DatabaseUtils.concatenateWhere( 1090 selection, Thumbnails._ID + " = ?"); 1091 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1092 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1093 // fall through 1094 } 1095 case THUMBNAILS: { 1096 qb.setTables(TABLE_THUMBNAILS); 1097 break; 1098 } 1099 1100 case OMNIBOX_SUGGESTIONS: { 1101 qb.setTables(VIEW_OMNIBOX_SUGGESTIONS); 1102 break; 1103 } 1104 1105 default: { 1106 throw new UnsupportedOperationException("Unknown URL " + uri.toString()); 1107 } 1108 } 1109 1110 Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, 1111 null, sortOrder, limit); 1112 cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI); 1113 return cursor; 1114 } 1115 1116 private Cursor doSuggestQuery(String selection, String[] selectionArgs, String limit) { 1117 if (TextUtils.isEmpty(selectionArgs[0])) { 1118 selection = ZERO_QUERY_SUGGEST_SELECTION; 1119 selectionArgs = null; 1120 } else { 1121 String like = selectionArgs[0] + "%"; 1122 if (selectionArgs[0].startsWith("http") 1123 || selectionArgs[0].startsWith("file")) { 1124 selectionArgs[0] = like; 1125 } else { 1126 selectionArgs = new String[6]; 1127 selectionArgs[0] = "http://" + like; 1128 selectionArgs[1] = "http://www." + like; 1129 selectionArgs[2] = "https://" + like; 1130 selectionArgs[3] = "https://www." + like; 1131 // To match against titles. 1132 selectionArgs[4] = like; 1133 selectionArgs[5] = like; 1134 selection = SUGGEST_SELECTION; 1135 } 1136 selection = DatabaseUtils.concatenateWhere(selection, 1137 Bookmarks.IS_DELETED + "=0 AND " + Bookmarks.IS_FOLDER + "=0"); 1138 1139 } 1140 Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_BOOKMARKS_JOIN_HISTORY, 1141 SUGGEST_PROJECTION, selection, selectionArgs, null, null, 1142 SUGGEST_ORDER_BY, null); 1143 1144 return new SuggestionsCursor(c); 1145 } 1146 1147 private String[] createCombinedQuery( 1148 Uri uri, String[] projection, SQLiteQueryBuilder qb) { 1149 String[] args = null; 1150 StringBuilder whereBuilder = new StringBuilder(128); 1151 whereBuilder.append(Bookmarks.IS_DELETED); 1152 whereBuilder.append(" = 0"); 1153 // Look for account info 1154 Object[] withAccount = getSelectionWithAccounts(uri, null, null); 1155 String selection = (String) withAccount[0]; 1156 String[] selectionArgs = (String[]) withAccount[1]; 1157 if (selection != null) { 1158 whereBuilder.append(" AND " + selection); 1159 if (selectionArgs != null) { 1160 // We use the selection twice, hence we need to duplicate the args 1161 args = new String[selectionArgs.length * 2]; 1162 System.arraycopy(selectionArgs, 0, args, 0, selectionArgs.length); 1163 System.arraycopy(selectionArgs, 0, args, selectionArgs.length, 1164 selectionArgs.length); 1165 } 1166 } 1167 String where = whereBuilder.toString(); 1168 // Build the bookmark subquery for history union subquery 1169 qb.setTables(TABLE_BOOKMARKS); 1170 String subQuery = qb.buildQuery(null, where, null, null, null, null); 1171 // Build the history union subquery 1172 qb.setTables(String.format(FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES, subQuery)); 1173 qb.setProjectionMap(COMBINED_HISTORY_PROJECTION_MAP); 1174 String historySubQuery = qb.buildQuery(null, 1175 null, null, null, null, null); 1176 // Build the bookmark union subquery 1177 qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES); 1178 qb.setProjectionMap(COMBINED_BOOKMARK_PROJECTION_MAP); 1179 where += String.format(" AND %s NOT IN (SELECT %s FROM %s)", 1180 Combined.URL, History.URL, TABLE_HISTORY); 1181 String bookmarksSubQuery = qb.buildQuery(null, where, 1182 null, null, null, null); 1183 // Put it all together 1184 String query = qb.buildUnionQuery( 1185 new String[] {historySubQuery, bookmarksSubQuery}, 1186 null, null); 1187 qb.setTables("(" + query + ")"); 1188 qb.setProjectionMap(null); 1189 return args; 1190 } 1191 1192 int deleteBookmarks(String selection, String[] selectionArgs, 1193 boolean callerIsSyncAdapter) { 1194 //TODO cascade deletes down from folders 1195 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1196 if (callerIsSyncAdapter) { 1197 return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); 1198 } 1199 ContentValues values = new ContentValues(); 1200 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); 1201 values.put(Bookmarks.IS_DELETED, 1); 1202 return updateBookmarksInTransaction(values, selection, selectionArgs, 1203 callerIsSyncAdapter); 1204 } 1205 1206 @Override 1207 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, 1208 boolean callerIsSyncAdapter) { 1209 final int match = URI_MATCHER.match(uri); 1210 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1211 int deleted = 0; 1212 switch (match) { 1213 case BOOKMARKS_ID: { 1214 selection = DatabaseUtils.concatenateWhere(selection, 1215 TABLE_BOOKMARKS + "._id=?"); 1216 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1217 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1218 // fall through 1219 } 1220 case BOOKMARKS: { 1221 // Look for account info 1222 Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); 1223 selection = (String) withAccount[0]; 1224 selectionArgs = (String[]) withAccount[1]; 1225 deleted = deleteBookmarks(selection, selectionArgs, callerIsSyncAdapter); 1226 pruneImages(); 1227 if (deleted > 0) { 1228 refreshWidgets(); 1229 } 1230 break; 1231 } 1232 1233 case HISTORY_ID: { 1234 selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); 1235 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1236 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1237 // fall through 1238 } 1239 case HISTORY: { 1240 filterSearchClient(selectionArgs); 1241 deleted = db.delete(TABLE_HISTORY, selection, selectionArgs); 1242 pruneImages(); 1243 break; 1244 } 1245 1246 case SEARCHES_ID: { 1247 selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?"); 1248 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1249 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1250 // fall through 1251 } 1252 case SEARCHES: { 1253 deleted = db.delete(TABLE_SEARCHES, selection, selectionArgs); 1254 break; 1255 } 1256 1257 case SYNCSTATE: { 1258 deleted = mSyncHelper.delete(db, selection, selectionArgs); 1259 break; 1260 } 1261 case SYNCSTATE_ID: { 1262 String selectionWithId = 1263 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 1264 + (selection == null ? "" : " AND (" + selection + ")"); 1265 deleted = mSyncHelper.delete(db, selectionWithId, selectionArgs); 1266 break; 1267 } 1268 case LEGACY_ID: { 1269 selection = DatabaseUtils.concatenateWhere( 1270 selection, Combined._ID + " = CAST(? AS INTEGER)"); 1271 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1272 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1273 // fall through 1274 } 1275 case LEGACY: { 1276 String[] projection = new String[] { Combined._ID, 1277 Combined.IS_BOOKMARK, Combined.URL }; 1278 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1279 String[] args = createCombinedQuery(uri, projection, qb); 1280 if (selectionArgs == null) { 1281 selectionArgs = args; 1282 } else { 1283 selectionArgs = DatabaseUtils.appendSelectionArgs( 1284 args, selectionArgs); 1285 } 1286 Cursor c = qb.query(db, projection, selection, selectionArgs, 1287 null, null, null); 1288 while (c.moveToNext()) { 1289 long id = c.getLong(0); 1290 boolean isBookmark = c.getInt(1) != 0; 1291 String url = c.getString(2); 1292 if (isBookmark) { 1293 deleted += deleteBookmarks(Bookmarks._ID + "=?", 1294 new String[] { Long.toString(id) }, 1295 callerIsSyncAdapter); 1296 db.delete(TABLE_HISTORY, History.URL + "=?", 1297 new String[] { url }); 1298 } else { 1299 deleted += db.delete(TABLE_HISTORY, 1300 Bookmarks._ID + "=?", 1301 new String[] { Long.toString(id) }); 1302 } 1303 } 1304 c.close(); 1305 break; 1306 } 1307 case THUMBNAILS_ID: { 1308 selection = DatabaseUtils.concatenateWhere( 1309 selection, Thumbnails._ID + " = ?"); 1310 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1311 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1312 // fall through 1313 } 1314 case THUMBNAILS: { 1315 deleted = db.delete(TABLE_THUMBNAILS, selection, selectionArgs); 1316 break; 1317 } 1318 default: { 1319 throw new UnsupportedOperationException("Unknown delete URI " + uri); 1320 } 1321 } 1322 if (deleted > 0) { 1323 postNotifyUri(uri); 1324 if (shouldNotifyLegacy(uri)) { 1325 postNotifyUri(LEGACY_AUTHORITY_URI); 1326 } 1327 } 1328 return deleted; 1329 } 1330 1331 long queryDefaultFolderId(String accountName, String accountType) { 1332 if (!isNullAccount(accountName) && !isNullAccount(accountType)) { 1333 final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 1334 Cursor c = db.query(TABLE_BOOKMARKS, new String[] { Bookmarks._ID }, 1335 ChromeSyncColumns.SERVER_UNIQUE + " = ?" + 1336 " AND account_type = ? AND account_name = ?", 1337 new String[] { ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR, 1338 accountType, accountName }, null, null, null); 1339 try { 1340 if (c.moveToFirst()) { 1341 return c.getLong(0); 1342 } 1343 } finally { 1344 c.close(); 1345 } 1346 } 1347 return FIXED_ID_ROOT; 1348 } 1349 1350 @Override 1351 public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 1352 int match = URI_MATCHER.match(uri); 1353 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1354 long id = -1; 1355 if (match == LEGACY) { 1356 // Intercept and route to the correct table 1357 Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK); 1358 values.remove(BookmarkColumns.BOOKMARK); 1359 if (bookmark == null || bookmark == 0) { 1360 match = HISTORY; 1361 } else { 1362 match = BOOKMARKS; 1363 values.remove(BookmarkColumns.DATE); 1364 values.remove(BookmarkColumns.VISITS); 1365 values.remove(BookmarkColumns.USER_ENTERED); 1366 values.put(Bookmarks.IS_FOLDER, 0); 1367 } 1368 } 1369 switch (match) { 1370 case BOOKMARKS: { 1371 // Mark rows dirty if they're not coming from a sync adapter 1372 if (!callerIsSyncAdapter) { 1373 long now = System.currentTimeMillis(); 1374 values.put(Bookmarks.DATE_CREATED, now); 1375 values.put(Bookmarks.DATE_MODIFIED, now); 1376 values.put(Bookmarks.DIRTY, 1); 1377 1378 boolean hasAccounts = values.containsKey(Bookmarks.ACCOUNT_TYPE) 1379 || values.containsKey(Bookmarks.ACCOUNT_NAME); 1380 String accountType = values 1381 .getAsString(Bookmarks.ACCOUNT_TYPE); 1382 String accountName = values 1383 .getAsString(Bookmarks.ACCOUNT_NAME); 1384 boolean hasParent = values.containsKey(Bookmarks.PARENT); 1385 if (hasParent && hasAccounts) { 1386 // Let's make sure it's valid 1387 long parentId = values.getAsLong(Bookmarks.PARENT); 1388 hasParent = isValidParent( 1389 accountType, accountName, parentId); 1390 } else if (hasParent && !hasAccounts) { 1391 long parentId = values.getAsLong(Bookmarks.PARENT); 1392 hasParent = setParentValues(parentId, values); 1393 } 1394 1395 // If no parent is set default to the "Bookmarks Bar" folder 1396 if (!hasParent) { 1397 values.put(Bookmarks.PARENT, 1398 queryDefaultFolderId(accountName, accountType)); 1399 } 1400 } 1401 1402 // If no position is requested put the bookmark at the beginning of the list 1403 if (!values.containsKey(Bookmarks.POSITION)) { 1404 values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE)); 1405 } 1406 1407 // Extract out the image values so they can be inserted into the images table 1408 String url = values.getAsString(Bookmarks.URL); 1409 ContentValues imageValues = extractImageValues(values, url); 1410 Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER); 1411 if ((isFolder == null || !isFolder) 1412 && imageValues != null && !TextUtils.isEmpty(url)) { 1413 int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", 1414 new String[] { url }); 1415 if (count == 0) { 1416 db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); 1417 } 1418 } 1419 1420 id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values); 1421 refreshWidgets(); 1422 break; 1423 } 1424 1425 case HISTORY: { 1426 // If no created time is specified set it to now 1427 if (!values.containsKey(History.DATE_CREATED)) { 1428 values.put(History.DATE_CREATED, System.currentTimeMillis()); 1429 } 1430 String url = values.getAsString(History.URL); 1431 url = filterSearchClient(url); 1432 values.put(History.URL, url); 1433 1434 // Extract out the image values so they can be inserted into the images table 1435 ContentValues imageValues = extractImageValues(values, 1436 values.getAsString(History.URL)); 1437 if (imageValues != null) { 1438 db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues); 1439 } 1440 1441 id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); 1442 break; 1443 } 1444 1445 case SEARCHES: { 1446 id = insertSearchesInTransaction(db, values); 1447 break; 1448 } 1449 1450 case SYNCSTATE: { 1451 id = mSyncHelper.insert(db, values); 1452 break; 1453 } 1454 1455 case SETTINGS: { 1456 id = 0; 1457 insertSettingsInTransaction(db, values); 1458 break; 1459 } 1460 1461 case THUMBNAILS: { 1462 id = db.replaceOrThrow(TABLE_THUMBNAILS, null, values); 1463 break; 1464 } 1465 1466 default: { 1467 throw new UnsupportedOperationException("Unknown insert URI " + uri); 1468 } 1469 } 1470 1471 if (id >= 0) { 1472 postNotifyUri(uri); 1473 if (shouldNotifyLegacy(uri)) { 1474 postNotifyUri(LEGACY_AUTHORITY_URI); 1475 } 1476 return ContentUris.withAppendedId(uri, id); 1477 } else { 1478 return null; 1479 } 1480 } 1481 1482 private String[] getAccountNameAndType(long id) { 1483 if (id <= 0) { 1484 return null; 1485 } 1486 Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id); 1487 Cursor c = query(uri, 1488 new String[] { Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE }, 1489 null, null, null); 1490 try { 1491 if (c.moveToFirst()) { 1492 String parentName = c.getString(0); 1493 String parentType = c.getString(1); 1494 return new String[] { parentName, parentType }; 1495 } 1496 return null; 1497 } finally { 1498 c.close(); 1499 } 1500 } 1501 1502 private boolean setParentValues(long parentId, ContentValues values) { 1503 String[] parent = getAccountNameAndType(parentId); 1504 if (parent == null) { 1505 return false; 1506 } 1507 values.put(Bookmarks.ACCOUNT_NAME, parent[0]); 1508 values.put(Bookmarks.ACCOUNT_TYPE, parent[1]); 1509 return true; 1510 } 1511 1512 private boolean isValidParent(String accountType, String accountName, 1513 long parentId) { 1514 String[] parent = getAccountNameAndType(parentId); 1515 if (parent != null 1516 && TextUtils.equals(accountName, parent[0]) 1517 && TextUtils.equals(accountType, parent[1])) { 1518 return true; 1519 } 1520 return false; 1521 } 1522 1523 private void filterSearchClient(String[] selectionArgs) { 1524 if (selectionArgs != null) { 1525 for (int i = 0; i < selectionArgs.length; i++) { 1526 selectionArgs[i] = filterSearchClient(selectionArgs[i]); 1527 } 1528 } 1529 } 1530 1531 // Filters out the client= param for search urls 1532 private String filterSearchClient(String url) { 1533 // remove "client" before updating it to the history so that it wont 1534 // show up in the auto-complete list. 1535 int index = url.indexOf("client="); 1536 if (index > 0 && url.contains(".google.")) { 1537 int end = url.indexOf('&', index); 1538 if (end > 0) { 1539 url = url.substring(0, index) 1540 .concat(url.substring(end + 1)); 1541 } else { 1542 // the url.charAt(index-1) should be either '?' or '&' 1543 url = url.substring(0, index-1); 1544 } 1545 } 1546 return url; 1547 } 1548 1549 /** 1550 * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them. 1551 */ 1552 private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) { 1553 String search = values.getAsString(Searches.SEARCH); 1554 if (TextUtils.isEmpty(search)) { 1555 throw new IllegalArgumentException("Must include the SEARCH field"); 1556 } 1557 Cursor cursor = null; 1558 try { 1559 cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID }, 1560 Searches.SEARCH + "=?", new String[] { search }, null, null, null); 1561 if (cursor.moveToNext()) { 1562 long id = cursor.getLong(0); 1563 db.update(TABLE_SEARCHES, values, Searches._ID + "=?", 1564 new String[] { Long.toString(id) }); 1565 return id; 1566 } else { 1567 return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values); 1568 } 1569 } finally { 1570 if (cursor != null) cursor.close(); 1571 } 1572 } 1573 1574 /** 1575 * Settings are unique, so perform an UPSERT manually since SQLite doesn't support them. 1576 */ 1577 private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) { 1578 String key = values.getAsString(Settings.KEY); 1579 if (TextUtils.isEmpty(key)) { 1580 throw new IllegalArgumentException("Must include the KEY field"); 1581 } 1582 String[] keyArray = new String[] { key }; 1583 Cursor cursor = null; 1584 try { 1585 cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY }, 1586 Settings.KEY + "=?", keyArray, null, null, null); 1587 if (cursor.moveToNext()) { 1588 long id = cursor.getLong(0); 1589 db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray); 1590 return id; 1591 } else { 1592 return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values); 1593 } 1594 } finally { 1595 if (cursor != null) cursor.close(); 1596 } 1597 } 1598 1599 @Override 1600 public int updateInTransaction(Uri uri, ContentValues values, String selection, 1601 String[] selectionArgs, boolean callerIsSyncAdapter) { 1602 int match = URI_MATCHER.match(uri); 1603 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1604 if (match == LEGACY || match == LEGACY_ID) { 1605 // Intercept and route to the correct table 1606 Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK); 1607 values.remove(BookmarkColumns.BOOKMARK); 1608 if (bookmark == null || bookmark == 0) { 1609 if (match == LEGACY) { 1610 match = HISTORY; 1611 } else { 1612 match = HISTORY_ID; 1613 } 1614 } else { 1615 if (match == LEGACY) { 1616 match = BOOKMARKS; 1617 } else { 1618 match = BOOKMARKS_ID; 1619 } 1620 values.remove(BookmarkColumns.DATE); 1621 values.remove(BookmarkColumns.VISITS); 1622 values.remove(BookmarkColumns.USER_ENTERED); 1623 } 1624 } 1625 int modified = 0; 1626 switch (match) { 1627 case BOOKMARKS_ID: { 1628 selection = DatabaseUtils.concatenateWhere(selection, 1629 TABLE_BOOKMARKS + "._id=?"); 1630 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1631 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1632 // fall through 1633 } 1634 case BOOKMARKS: { 1635 Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs); 1636 selection = (String) withAccount[0]; 1637 selectionArgs = (String[]) withAccount[1]; 1638 modified = updateBookmarksInTransaction(values, selection, selectionArgs, 1639 callerIsSyncAdapter); 1640 if (modified > 0) { 1641 refreshWidgets(); 1642 } 1643 break; 1644 } 1645 1646 case HISTORY_ID: { 1647 selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?"); 1648 selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs, 1649 new String[] { Long.toString(ContentUris.parseId(uri)) }); 1650 // fall through 1651 } 1652 case HISTORY: { 1653 modified = updateHistoryInTransaction(values, selection, selectionArgs); 1654 break; 1655 } 1656 1657 case SYNCSTATE: { 1658 modified = mSyncHelper.update(mDb, values, 1659 appendAccountToSelection(uri, selection), selectionArgs); 1660 break; 1661 } 1662 1663 case SYNCSTATE_ID: { 1664 selection = appendAccountToSelection(uri, selection); 1665 String selectionWithId = 1666 (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ") 1667 + (selection == null ? "" : " AND (" + selection + ")"); 1668 modified = mSyncHelper.update(mDb, values, 1669 selectionWithId, selectionArgs); 1670 break; 1671 } 1672 1673 case IMAGES: { 1674 String url = values.getAsString(Images.URL); 1675 if (TextUtils.isEmpty(url)) { 1676 throw new IllegalArgumentException("Images.URL is required"); 1677 } 1678 if (!shouldUpdateImages(db, url, values)) { 1679 return 0; 1680 } 1681 int count = db.update(TABLE_IMAGES, values, Images.URL + "=?", 1682 new String[] { url }); 1683 if (count == 0) { 1684 db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values); 1685 count = 1; 1686 } 1687 // Only favicon is exposed in the public API. If we updated 1688 // the thumbnail or touch icon don't bother notifying the 1689 // legacy authority since it can't read it anyway. 1690 boolean updatedLegacy = false; 1691 if (getUrlCount(db, TABLE_BOOKMARKS, url) > 0) { 1692 postNotifyUri(Bookmarks.CONTENT_URI); 1693 updatedLegacy = values.containsKey(Images.FAVICON); 1694 refreshWidgets(); 1695 } 1696 if (getUrlCount(db, TABLE_HISTORY, url) > 0) { 1697 postNotifyUri(History.CONTENT_URI); 1698 updatedLegacy = values.containsKey(Images.FAVICON); 1699 } 1700 if (pruneImages() > 0 || updatedLegacy) { 1701 postNotifyUri(LEGACY_AUTHORITY_URI); 1702 } 1703 // Even though we may be calling notifyUri on Bookmarks, don't 1704 // sync to network as images aren't synced. Otherwise this 1705 // unnecessarily triggers a bookmark sync. 1706 mSyncToNetwork = false; 1707 return count; 1708 } 1709 1710 case SEARCHES: { 1711 modified = db.update(TABLE_SEARCHES, values, selection, selectionArgs); 1712 break; 1713 } 1714 1715 case ACCOUNTS: { 1716 Account[] accounts = AccountManager.get(getContext()).getAccounts(); 1717 mSyncHelper.onAccountsChanged(mDb, accounts); 1718 break; 1719 } 1720 1721 case THUMBNAILS: { 1722 modified = db.update(TABLE_THUMBNAILS, values, 1723 selection, selectionArgs); 1724 break; 1725 } 1726 1727 default: { 1728 throw new UnsupportedOperationException("Unknown update URI " + uri); 1729 } 1730 } 1731 pruneImages(); 1732 if (modified > 0) { 1733 postNotifyUri(uri); 1734 if (shouldNotifyLegacy(uri)) { 1735 postNotifyUri(LEGACY_AUTHORITY_URI); 1736 } 1737 } 1738 return modified; 1739 } 1740 1741 // We want to avoid sending out more URI notifications than we have to 1742 // Thus, we check to see if the images we are about to store are already there 1743 // This is used because things like a site's favion or touch icon is rarely 1744 // changed, but the browser tries to update it every time the page loads. 1745 // Without this, we will always send out 3 URI notifications per page load. 1746 // With this, that drops to 0 or 1, depending on if the thumbnail changed. 1747 private boolean shouldUpdateImages( 1748 SQLiteDatabase db, String url, ContentValues values) { 1749 final String[] projection = new String[] { 1750 Images.FAVICON, 1751 Images.THUMBNAIL, 1752 Images.TOUCH_ICON, 1753 }; 1754 Cursor cursor = db.query(TABLE_IMAGES, projection, Images.URL + "=?", 1755 new String[] { url }, null, null, null); 1756 byte[] nfavicon = values.getAsByteArray(Images.FAVICON); 1757 byte[] nthumb = values.getAsByteArray(Images.THUMBNAIL); 1758 byte[] ntouch = values.getAsByteArray(Images.TOUCH_ICON); 1759 byte[] cfavicon = null; 1760 byte[] cthumb = null; 1761 byte[] ctouch = null; 1762 try { 1763 if (cursor.getCount() <= 0) { 1764 return nfavicon != null || nthumb != null || ntouch != null; 1765 } 1766 while (cursor.moveToNext()) { 1767 if (nfavicon != null) { 1768 cfavicon = cursor.getBlob(0); 1769 if (!Arrays.equals(nfavicon, cfavicon)) { 1770 return true; 1771 } 1772 } 1773 if (nthumb != null) { 1774 cthumb = cursor.getBlob(1); 1775 if (!Arrays.equals(nthumb, cthumb)) { 1776 return true; 1777 } 1778 } 1779 if (ntouch != null) { 1780 ctouch = cursor.getBlob(2); 1781 if (!Arrays.equals(ntouch, ctouch)) { 1782 return true; 1783 } 1784 } 1785 } 1786 } finally { 1787 cursor.close(); 1788 } 1789 return false; 1790 } 1791 1792 int getUrlCount(SQLiteDatabase db, String table, String url) { 1793 Cursor c = db.query(table, new String[] { "COUNT(*)" }, 1794 "url = ?", new String[] { url }, null, null, null); 1795 try { 1796 int count = 0; 1797 if (c.moveToFirst()) { 1798 count = c.getInt(0); 1799 } 1800 return count; 1801 } finally { 1802 c.close(); 1803 } 1804 } 1805 1806 /** 1807 * Does a query to find the matching bookmarks and updates each one with the provided values. 1808 */ 1809 int updateBookmarksInTransaction(ContentValues values, String selection, 1810 String[] selectionArgs, boolean callerIsSyncAdapter) { 1811 int count = 0; 1812 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1813 final String[] bookmarksProjection = new String[] { 1814 Bookmarks._ID, // 0 1815 Bookmarks.VERSION, // 1 1816 Bookmarks.URL, // 2 1817 Bookmarks.TITLE, // 3 1818 Bookmarks.IS_FOLDER, // 4 1819 Bookmarks.ACCOUNT_NAME, // 5 1820 Bookmarks.ACCOUNT_TYPE, // 6 1821 }; 1822 Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, 1823 selection, selectionArgs, null, null, null); 1824 boolean updatingParent = values.containsKey(Bookmarks.PARENT); 1825 String parentAccountName = null; 1826 String parentAccountType = null; 1827 if (updatingParent) { 1828 long parent = values.getAsLong(Bookmarks.PARENT); 1829 Cursor c = db.query(TABLE_BOOKMARKS, new String[] { 1830 Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE}, 1831 "_id = ?", new String[] { Long.toString(parent) }, 1832 null, null, null); 1833 if (c.moveToFirst()) { 1834 parentAccountName = c.getString(0); 1835 parentAccountType = c.getString(1); 1836 } 1837 c.close(); 1838 } else if (values.containsKey(Bookmarks.ACCOUNT_NAME) 1839 || values.containsKey(Bookmarks.ACCOUNT_TYPE)) { 1840 // TODO: Implement if needed (no one needs this yet) 1841 } 1842 try { 1843 String[] args = new String[1]; 1844 // Mark the bookmark dirty if the caller isn't a sync adapter 1845 if (!callerIsSyncAdapter) { 1846 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); 1847 values.put(Bookmarks.DIRTY, 1); 1848 } 1849 1850 boolean updatingUrl = values.containsKey(Bookmarks.URL); 1851 String url = null; 1852 if (updatingUrl) { 1853 url = values.getAsString(Bookmarks.URL); 1854 } 1855 ContentValues imageValues = extractImageValues(values, url); 1856 1857 while (cursor.moveToNext()) { 1858 long id = cursor.getLong(0); 1859 args[0] = Long.toString(id); 1860 String accountName = cursor.getString(5); 1861 String accountType = cursor.getString(6); 1862 // If we are updating the parent and either the account name or 1863 // type do not match that of the new parent 1864 if (updatingParent 1865 && (!TextUtils.equals(accountName, parentAccountName) 1866 || !TextUtils.equals(accountType, parentAccountType))) { 1867 // Parent is a different account 1868 // First, insert a new bookmark/folder with the new account 1869 // Then, if this is a folder, reparent all it's children 1870 // Finally, delete the old bookmark/folder 1871 ContentValues newValues = valuesFromCursor(cursor); 1872 newValues.putAll(values); 1873 newValues.remove(Bookmarks._ID); 1874 newValues.remove(Bookmarks.VERSION); 1875 newValues.put(Bookmarks.ACCOUNT_NAME, parentAccountName); 1876 newValues.put(Bookmarks.ACCOUNT_TYPE, parentAccountType); 1877 Uri insertUri = insertInTransaction(Bookmarks.CONTENT_URI, 1878 newValues, callerIsSyncAdapter); 1879 long newId = ContentUris.parseId(insertUri); 1880 if (cursor.getInt(4) != 0) { 1881 // This is a folder, reparent 1882 ContentValues updateChildren = new ContentValues(1); 1883 updateChildren.put(Bookmarks.PARENT, newId); 1884 count += updateBookmarksInTransaction(updateChildren, 1885 Bookmarks.PARENT + "=?", new String[] { 1886 Long.toString(id)}, callerIsSyncAdapter); 1887 } 1888 // Now, delete the old one 1889 Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id); 1890 deleteInTransaction(uri, null, null, callerIsSyncAdapter); 1891 count += 1; 1892 } else { 1893 if (!callerIsSyncAdapter) { 1894 // increase the local version for non-sync changes 1895 values.put(Bookmarks.VERSION, cursor.getLong(1) + 1); 1896 } 1897 count += db.update(TABLE_BOOKMARKS, values, "_id=?", args); 1898 } 1899 1900 // Update the images over in their table 1901 if (imageValues != null) { 1902 if (!updatingUrl) { 1903 url = cursor.getString(2); 1904 imageValues.put(Images.URL, url); 1905 } 1906 1907 if (!TextUtils.isEmpty(url)) { 1908 args[0] = url; 1909 if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { 1910 db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); 1911 } 1912 } 1913 } 1914 } 1915 } finally { 1916 if (cursor != null) cursor.close(); 1917 } 1918 return count; 1919 } 1920 1921 ContentValues valuesFromCursor(Cursor c) { 1922 int count = c.getColumnCount(); 1923 ContentValues values = new ContentValues(count); 1924 String[] colNames = c.getColumnNames(); 1925 for (int i = 0; i < count; i++) { 1926 switch (c.getType(i)) { 1927 case Cursor.FIELD_TYPE_BLOB: 1928 values.put(colNames[i], c.getBlob(i)); 1929 break; 1930 case Cursor.FIELD_TYPE_FLOAT: 1931 values.put(colNames[i], c.getFloat(i)); 1932 break; 1933 case Cursor.FIELD_TYPE_INTEGER: 1934 values.put(colNames[i], c.getLong(i)); 1935 break; 1936 case Cursor.FIELD_TYPE_STRING: 1937 values.put(colNames[i], c.getString(i)); 1938 break; 1939 } 1940 } 1941 return values; 1942 } 1943 1944 /** 1945 * Does a query to find the matching bookmarks and updates each one with the provided values. 1946 */ 1947 int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) { 1948 int count = 0; 1949 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 1950 filterSearchClient(selectionArgs); 1951 Cursor cursor = query(History.CONTENT_URI, 1952 new String[] { History._ID, History.URL }, 1953 selection, selectionArgs, null); 1954 try { 1955 String[] args = new String[1]; 1956 1957 boolean updatingUrl = values.containsKey(History.URL); 1958 String url = null; 1959 if (updatingUrl) { 1960 url = filterSearchClient(values.getAsString(History.URL)); 1961 values.put(History.URL, url); 1962 } 1963 ContentValues imageValues = extractImageValues(values, url); 1964 1965 while (cursor.moveToNext()) { 1966 args[0] = cursor.getString(0); 1967 count += db.update(TABLE_HISTORY, values, "_id=?", args); 1968 1969 // Update the images over in their table 1970 if (imageValues != null) { 1971 if (!updatingUrl) { 1972 url = cursor.getString(1); 1973 imageValues.put(Images.URL, url); 1974 } 1975 args[0] = url; 1976 if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) { 1977 db.insert(TABLE_IMAGES, Images.FAVICON, imageValues); 1978 } 1979 } 1980 } 1981 } finally { 1982 if (cursor != null) cursor.close(); 1983 } 1984 return count; 1985 } 1986 1987 String appendAccountToSelection(Uri uri, String selection) { 1988 final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME); 1989 final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE); 1990 1991 final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType); 1992 if (partialUri) { 1993 // Throw when either account is incomplete 1994 throw new IllegalArgumentException( 1995 "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri); 1996 } 1997 1998 // Accounts are valid by only checking one parameter, since we've 1999 // already ruled out partial accounts. 2000 final boolean validAccount = !TextUtils.isEmpty(accountName); 2001 if (validAccount) { 2002 StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=" 2003 + DatabaseUtils.sqlEscapeString(accountName) + " AND " 2004 + RawContacts.ACCOUNT_TYPE + "=" 2005 + DatabaseUtils.sqlEscapeString(accountType)); 2006 if (!TextUtils.isEmpty(selection)) { 2007 selectionSb.append(" AND ("); 2008 selectionSb.append(selection); 2009 selectionSb.append(')'); 2010 } 2011 return selectionSb.toString(); 2012 } else { 2013 return selection; 2014 } 2015 } 2016 2017 ContentValues extractImageValues(ContentValues values, String url) { 2018 ContentValues imageValues = null; 2019 // favicon 2020 if (values.containsKey(Bookmarks.FAVICON)) { 2021 imageValues = new ContentValues(); 2022 imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON)); 2023 values.remove(Bookmarks.FAVICON); 2024 } 2025 2026 // thumbnail 2027 if (values.containsKey(Bookmarks.THUMBNAIL)) { 2028 if (imageValues == null) { 2029 imageValues = new ContentValues(); 2030 } 2031 imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL)); 2032 values.remove(Bookmarks.THUMBNAIL); 2033 } 2034 2035 // touch icon 2036 if (values.containsKey(Bookmarks.TOUCH_ICON)) { 2037 if (imageValues == null) { 2038 imageValues = new ContentValues(); 2039 } 2040 imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON)); 2041 values.remove(Bookmarks.TOUCH_ICON); 2042 } 2043 2044 if (imageValues != null) { 2045 imageValues.put(Images.URL, url); 2046 } 2047 return imageValues; 2048 } 2049 2050 int pruneImages() { 2051 final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 2052 return db.delete(TABLE_IMAGES, IMAGE_PRUNE, null); 2053 } 2054 2055 boolean shouldNotifyLegacy(Uri uri) { 2056 if (uri.getPathSegments().contains("history") 2057 || uri.getPathSegments().contains("bookmarks") 2058 || uri.getPathSegments().contains("searches")) { 2059 return true; 2060 } 2061 return false; 2062 } 2063 2064 @Override 2065 protected boolean syncToNetwork(Uri uri) { 2066 if (BrowserContract.AUTHORITY.equals(uri.getAuthority()) 2067 && uri.getPathSegments().contains("bookmarks")) { 2068 return mSyncToNetwork; 2069 } 2070 if (LEGACY_AUTHORITY.equals(uri.getAuthority())) { 2071 // Allow for 3rd party sync adapters 2072 return true; 2073 } 2074 return false; 2075 } 2076 2077 static class SuggestionsCursor extends AbstractCursor { 2078 private static final int ID_INDEX = 0; 2079 private static final int URL_INDEX = 1; 2080 private static final int TITLE_INDEX = 2; 2081 private static final int ICON_INDEX = 3; 2082 private static final int LAST_ACCESS_TIME_INDEX = 4; 2083 // shared suggestion array index, make sure to match COLUMNS 2084 private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1; 2085 private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2; 2086 private static final int SUGGEST_COLUMN_TEXT_1_ID = 3; 2087 private static final int SUGGEST_COLUMN_TEXT_2_TEXT_ID = 4; 2088 private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5; 2089 private static final int SUGGEST_COLUMN_ICON_1_ID = 6; 2090 private static final int SUGGEST_COLUMN_LAST_ACCESS_HINT_ID = 7; 2091 2092 // shared suggestion columns 2093 private static final String[] COLUMNS = new String[] { 2094 BaseColumns._ID, 2095 SearchManager.SUGGEST_COLUMN_INTENT_ACTION, 2096 SearchManager.SUGGEST_COLUMN_INTENT_DATA, 2097 SearchManager.SUGGEST_COLUMN_TEXT_1, 2098 SearchManager.SUGGEST_COLUMN_TEXT_2, 2099 SearchManager.SUGGEST_COLUMN_TEXT_2_URL, 2100 SearchManager.SUGGEST_COLUMN_ICON_1, 2101 SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT}; 2102 2103 private final Cursor mSource; 2104 2105 public SuggestionsCursor(Cursor cursor) { 2106 mSource = cursor; 2107 } 2108 2109 @Override 2110 public String[] getColumnNames() { 2111 return COLUMNS; 2112 } 2113 2114 @Override 2115 public String getString(int columnIndex) { 2116 switch (columnIndex) { 2117 case ID_INDEX: 2118 return mSource.getString(columnIndex); 2119 case SUGGEST_COLUMN_INTENT_ACTION_ID: 2120 return Intent.ACTION_VIEW; 2121 case SUGGEST_COLUMN_INTENT_DATA_ID: 2122 return mSource.getString(URL_INDEX); 2123 case SUGGEST_COLUMN_TEXT_2_TEXT_ID: 2124 case SUGGEST_COLUMN_TEXT_2_URL_ID: 2125 return UrlUtils.stripUrl(mSource.getString(URL_INDEX)); 2126 case SUGGEST_COLUMN_TEXT_1_ID: 2127 return mSource.getString(TITLE_INDEX); 2128 case SUGGEST_COLUMN_ICON_1_ID: 2129 return mSource.getString(ICON_INDEX); 2130 case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID: 2131 return mSource.getString(LAST_ACCESS_TIME_INDEX); 2132 } 2133 return null; 2134 } 2135 2136 @Override 2137 public int getCount() { 2138 return mSource.getCount(); 2139 } 2140 2141 @Override 2142 public double getDouble(int column) { 2143 throw new UnsupportedOperationException(); 2144 } 2145 2146 @Override 2147 public float getFloat(int column) { 2148 throw new UnsupportedOperationException(); 2149 } 2150 2151 @Override 2152 public int getInt(int column) { 2153 throw new UnsupportedOperationException(); 2154 } 2155 2156 @Override 2157 public long getLong(int column) { 2158 switch (column) { 2159 case ID_INDEX: 2160 return mSource.getLong(ID_INDEX); 2161 case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID: 2162 return mSource.getLong(LAST_ACCESS_TIME_INDEX); 2163 } 2164 throw new UnsupportedOperationException(); 2165 } 2166 2167 @Override 2168 public short getShort(int column) { 2169 throw new UnsupportedOperationException(); 2170 } 2171 2172 @Override 2173 public boolean isNull(int column) { 2174 return mSource.isNull(column); 2175 } 2176 2177 @Override 2178 public boolean onMove(int oldPosition, int newPosition) { 2179 return mSource.moveToPosition(newPosition); 2180 } 2181 } 2182 2183 // --------------------------------------------------- 2184 // SQL below, be warned 2185 // --------------------------------------------------- 2186 2187 private static final String SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS = 2188 "CREATE VIEW IF NOT EXISTS v_omnibox_suggestions " 2189 + " AS " 2190 + " SELECT _id, url, title, 1 AS bookmark, 0 AS visits, 0 AS date" 2191 + " FROM bookmarks " 2192 + " WHERE deleted = 0 AND folder = 0 " 2193 + " UNION ALL " 2194 + " SELECT _id, url, title, 0 AS bookmark, visits, date " 2195 + " FROM history " 2196 + " WHERE url NOT IN (SELECT url FROM bookmarks" 2197 + " WHERE deleted = 0 AND folder = 0) " 2198 + " ORDER BY bookmark DESC, visits DESC, date DESC "; 2199 2200 private static final String SQL_WHERE_ACCOUNT_HAS_BOOKMARKS = 2201 "0 < ( " 2202 + "SELECT count(*) " 2203 + "FROM bookmarks " 2204 + "WHERE deleted = 0 AND folder = 0 " 2205 + " AND ( " 2206 + " v_accounts.account_name = bookmarks.account_name " 2207 + " OR (v_accounts.account_name IS NULL AND bookmarks.account_name IS NULL) " 2208 + " ) " 2209 + " AND ( " 2210 + " v_accounts.account_type = bookmarks.account_type " 2211 + " OR (v_accounts.account_type IS NULL AND bookmarks.account_type IS NULL) " 2212 + " ) " 2213 + ")"; 2214 } 2215