1// Copyright 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.chrome.browser; 6 7import android.annotation.SuppressLint; 8import android.app.SearchManager; 9import android.content.ContentProvider; 10import android.content.ContentUris; 11import android.content.ContentValues; 12import android.content.Context; 13import android.content.SharedPreferences; 14import android.content.UriMatcher; 15import android.database.Cursor; 16import android.database.MatrixCursor; 17import android.net.Uri; 18import android.os.Binder; 19import android.os.Build; 20import android.os.Bundle; 21import android.os.Parcel; 22import android.os.Parcelable; 23import android.os.UserHandle; 24import android.preference.PreferenceManager; 25import android.provider.BaseColumns; 26import android.provider.Browser; 27import android.provider.Browser.BookmarkColumns; 28import android.provider.Browser.SearchColumns; 29import android.text.TextUtils; 30import android.util.Log; 31 32import org.chromium.base.CalledByNative; 33import org.chromium.base.CalledByNativeUnchecked; 34import org.chromium.base.ThreadUtils; 35import org.chromium.base.VisibleForTesting; 36import org.chromium.chrome.browser.database.SQLiteCursor; 37import org.chromium.sync.notifier.SyncStatusHelper; 38 39import java.util.ArrayList; 40import java.util.Arrays; 41import java.util.HashMap; 42import java.util.List; 43import java.util.Vector; 44import java.util.concurrent.atomic.AtomicBoolean; 45 46/** 47 * This class provides access to user data stored in Chrome, such as bookmarks, most visited pages, 48 * etc. It is used to support android.provider.Browser. 49 */ 50public class ChromeBrowserProvider extends ContentProvider { 51 private static final String TAG = "ChromeBrowserProvider"; 52 53 // The permission required for using the bookmark folders API. Android build system does 54 // not generate Manifest.java for java libraries, hence use the permission name string. When 55 // making changes to this permission, also update the permission in AndroidManifest.xml. 56 private static final String PERMISSION_READ_WRITE_BOOKMARKS = "READ_WRITE_BOOKMARK_FOLDERS"; 57 58 // Defines the API methods that the Client can call by name. 59 static final String CLIENT_API_BOOKMARK_NODE_EXISTS = "BOOKMARK_NODE_EXISTS"; 60 static final String CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE = "CREATE_BOOKMARKS_FOLDER_ONCE"; 61 static final String CLIENT_API_GET_EDITABLE_BOOKMARK_FOLDER_HIERARCHY = 62 "GET_EDITABLE_BOOKMARK_FOLDER_HIERARCHY"; 63 static final String CLIENT_API_GET_BOOKMARK_NODE = "GET_BOOKMARK_NODE"; 64 static final String CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER = "GET_DEFAULT_BOOKMARK_FOLDER"; 65 static final String CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID = 66 "GET_MOBILE_BOOKMARKS_FOLDER_ID"; 67 static final String CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH = 68 "IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH"; 69 static final String CLIENT_API_DELETE_ALL_USER_BOOKMARKS = "DELETE_ALL_USER_BOOKMARKS"; 70 static final String CLIENT_API_RESULT_KEY = "result"; 71 72 73 // Defines Chrome's API authority, so it can be run and tested 74 // independently. 75 private static final String API_AUTHORITY_SUFFIX = ".browser"; 76 77 private static final String BROWSER_CONTRACT_API_AUTHORITY = 78 "com.google.android.apps.chrome.browser-contract"; 79 80 // These values are taken from android.provider.BrowserContract.java since 81 // that class is hidden from the SDK. 82 private static final String BROWSER_CONTRACT_AUTHORITY = "com.android.browser"; 83 private static final String BROWSER_CONTRACT_HISTORY_CONTENT_TYPE = 84 "vnd.android.cursor.dir/browser-history"; 85 private static final String BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE = 86 "vnd.android.cursor.item/browser-history"; 87 88 // This Authority is for internal interface. It's concatenated with 89 // Context.getPackageName() so that we can install different channels 90 // SxS and have different authorities. 91 private static final String AUTHORITY_SUFFIX = ".ChromeBrowserProvider"; 92 private static final String BOOKMARKS_PATH = "bookmarks"; 93 private static final String SEARCHES_PATH = "searches"; 94 private static final String HISTORY_PATH = "history"; 95 private static final String COMBINED_PATH = "combined"; 96 private static final String BOOKMARK_FOLDER_PATH = "hierarchy"; 97 98 public static final Uri BROWSER_CONTRACTS_BOOKMAKRS_API_URI = buildContentUri( 99 BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH); 100 101 public static final Uri BROWSER_CONTRACTS_SEARCHES_API_URI = buildContentUri( 102 BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH); 103 104 public static final Uri BROWSER_CONTRACTS_HISTORY_API_URI = buildContentUri( 105 BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH); 106 107 public static final Uri BROWSER_CONTRACTS_COMBINED_API_URI = buildContentUri( 108 BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH); 109 110 /** The parameter used to specify a bookmark parent ID in ContentValues. */ 111 public static final String BOOKMARK_PARENT_ID_PARAM = "parentId"; 112 113 /** The parameter used to specify whether this is a bookmark folder. */ 114 public static final String BOOKMARK_IS_FOLDER_PARAM = "isFolder"; 115 116 /** 117 * Invalid ID value for the Android ContentProvider API calls. 118 * The value 0 is intentional: if the ID represents a bookmark node then it's the root node 119 * and not accessible. Otherwise it represents a SQLite row id, so 0 is also invalid. 120 */ 121 public static final long INVALID_CONTENT_PROVIDER_ID = 0; 122 123 // ID used to indicate an invalid id for bookmark nodes. 124 // Client API queries should use ChromeBrowserProviderClient.INVALID_BOOKMARK_ID. 125 static final long INVALID_BOOKMARK_ID = -1; 126 127 private static final String LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY = "last_bookmark_folder_id"; 128 129 private static final int URI_MATCH_BOOKMARKS = 0; 130 private static final int URI_MATCH_BOOKMARKS_ID = 1; 131 private static final int URL_MATCH_API_BOOKMARK = 2; 132 private static final int URL_MATCH_API_BOOKMARK_ID = 3; 133 private static final int URL_MATCH_API_SEARCHES = 4; 134 private static final int URL_MATCH_API_SEARCHES_ID = 5; 135 private static final int URL_MATCH_API_HISTORY_CONTENT = 6; 136 private static final int URL_MATCH_API_HISTORY_CONTENT_ID = 7; 137 private static final int URL_MATCH_API_BOOKMARK_CONTENT = 8; 138 private static final int URL_MATCH_API_BOOKMARK_CONTENT_ID = 9; 139 private static final int URL_MATCH_BOOKMARK_SUGGESTIONS_ID = 10; 140 private static final int URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID = 11; 141 142 // TODO : Using Android.provider.Browser.HISTORY_PROJECTION once THUMBNAIL, 143 // TOUCH_ICON, and USER_ENTERED fields are supported. 144 private static final String[] BOOKMARK_DEFAULT_PROJECTION = new String[] { 145 BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, 146 BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, 147 BookmarkColumns.FAVICON, BookmarkColumns.CREATED 148 }; 149 150 private static final String[] SUGGEST_PROJECTION = new String[] { 151 BookmarkColumns._ID, 152 BookmarkColumns.TITLE, 153 BookmarkColumns.URL, 154 BookmarkColumns.DATE, 155 BookmarkColumns.BOOKMARK 156 }; 157 158 private final Object mInitializeUriMatcherLock = new Object(); 159 private final Object mLoadNativeLock = new Object(); 160 private UriMatcher mUriMatcher; 161 private long mLastModifiedBookmarkFolderId = INVALID_BOOKMARK_ID; 162 private long mNativeChromeBrowserProvider; 163 private BookmarkNode mMobileBookmarksFolder; 164 165 /** 166 * Records whether we've received a call to one of the public ContentProvider APIs. 167 */ 168 protected boolean mContentProviderApiCalled; 169 170 private void ensureUriMatcherInitialized() { 171 synchronized (mInitializeUriMatcherLock) { 172 if (mUriMatcher != null) return; 173 174 mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 175 // The internal URIs 176 String authority = getContext().getPackageName() + AUTHORITY_SUFFIX; 177 mUriMatcher.addURI(authority, BOOKMARKS_PATH, URI_MATCH_BOOKMARKS); 178 mUriMatcher.addURI(authority, BOOKMARKS_PATH + "/#", URI_MATCH_BOOKMARKS_ID); 179 // The internal authority for public APIs 180 String apiAuthority = getContext().getPackageName() + API_AUTHORITY_SUFFIX; 181 mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK); 182 mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); 183 mUriMatcher.addURI(apiAuthority, SEARCHES_PATH, URL_MATCH_API_SEARCHES); 184 mUriMatcher.addURI(apiAuthority, SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID); 185 mUriMatcher.addURI(apiAuthority, HISTORY_PATH, URL_MATCH_API_HISTORY_CONTENT); 186 mUriMatcher.addURI(apiAuthority, HISTORY_PATH + "/#", URL_MATCH_API_HISTORY_CONTENT_ID); 187 mUriMatcher.addURI(apiAuthority, COMBINED_PATH, URL_MATCH_API_BOOKMARK); 188 mUriMatcher.addURI(apiAuthority, COMBINED_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); 189 // The internal authority for BrowserContracts 190 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH, 191 URL_MATCH_API_HISTORY_CONTENT); 192 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH + "/#", 193 URL_MATCH_API_HISTORY_CONTENT_ID); 194 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH, 195 URL_MATCH_API_BOOKMARK); 196 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH + "/#", 197 URL_MATCH_API_BOOKMARK_ID); 198 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH, 199 URL_MATCH_API_SEARCHES); 200 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH + "/#", 201 URL_MATCH_API_SEARCHES_ID); 202 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH, 203 URL_MATCH_API_BOOKMARK_CONTENT); 204 mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH + "/#", 205 URL_MATCH_API_BOOKMARK_CONTENT_ID); 206 // Added the Android Framework URIs, so the provider can easily switched 207 // by adding 'browser' and 'com.android.browser' in manifest. 208 // The Android's BrowserContract 209 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH, 210 URL_MATCH_API_HISTORY_CONTENT); 211 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH + "/#", 212 URL_MATCH_API_HISTORY_CONTENT_ID); 213 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined", URL_MATCH_API_BOOKMARK); 214 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined/#", URL_MATCH_API_BOOKMARK_ID); 215 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH, URL_MATCH_API_SEARCHES); 216 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH + "/#", 217 URL_MATCH_API_SEARCHES_ID); 218 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH, 219 URL_MATCH_API_BOOKMARK_CONTENT); 220 mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH + "/#", 221 URL_MATCH_API_BOOKMARK_CONTENT_ID); 222 // For supporting android.provider.browser.BookmarkColumns and 223 // SearchColumns 224 mUriMatcher.addURI("browser", BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK); 225 mUriMatcher.addURI("browser", BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID); 226 mUriMatcher.addURI("browser", SEARCHES_PATH, URL_MATCH_API_SEARCHES); 227 mUriMatcher.addURI("browser", SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID); 228 229 mUriMatcher.addURI(apiAuthority, 230 BOOKMARKS_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY, 231 URL_MATCH_BOOKMARK_SUGGESTIONS_ID); 232 mUriMatcher.addURI(apiAuthority, 233 SearchManager.SUGGEST_URI_PATH_QUERY, 234 URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID); 235 } 236 } 237 238 @Override 239 public boolean onCreate() { 240 // Pre-load shared preferences object, this happens on a separate thread 241 PreferenceManager.getDefaultSharedPreferences(getContext()); 242 return true; 243 } 244 245 /** 246 * Lazily fetches the last modified bookmark folder id. 247 */ 248 private long getLastModifiedBookmarkFolderId() { 249 if (mLastModifiedBookmarkFolderId == INVALID_BOOKMARK_ID) { 250 SharedPreferences sharedPreferences = 251 PreferenceManager.getDefaultSharedPreferences(getContext()); 252 mLastModifiedBookmarkFolderId = sharedPreferences.getLong( 253 LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, INVALID_BOOKMARK_ID); 254 } 255 return mLastModifiedBookmarkFolderId; 256 } 257 258 private String buildSuggestWhere(String selection, int argc) { 259 StringBuilder sb = new StringBuilder(selection); 260 for (int i = 0; i < argc - 1; i++) { 261 sb.append(" OR "); 262 sb.append(selection); 263 } 264 return sb.toString(); 265 } 266 267 private String getReadWritePermissionNameForBookmarkFolders() { 268 return getContext().getApplicationContext().getPackageName() + ".permission." 269 + PERMISSION_READ_WRITE_BOOKMARKS; 270 } 271 272 private Cursor getBookmarkHistorySuggestions(String selection, String[] selectionArgs, 273 String sortOrder, boolean excludeHistory) { 274 boolean matchTitles = false; 275 Vector<String> args = new Vector<String>(); 276 String like = selectionArgs[0] + "%"; 277 if (selectionArgs[0].startsWith("http") || selectionArgs[0].startsWith("file")) { 278 args.add(like); 279 } else { 280 // Match against common URL prefixes. 281 args.add("http://" + like); 282 args.add("https://" + like); 283 args.add("http://www." + like); 284 args.add("https://www." + like); 285 args.add("file://" + like); 286 matchTitles = true; 287 } 288 289 StringBuilder urlWhere = new StringBuilder("("); 290 urlWhere.append(buildSuggestWhere(selection, args.size())); 291 if (matchTitles) { 292 args.add(like); 293 urlWhere.append(" OR title LIKE ?"); 294 } 295 urlWhere.append(")"); 296 297 if (excludeHistory) { 298 urlWhere.append(" AND bookmark=?"); 299 args.add("1"); 300 } 301 302 selectionArgs = args.toArray(selectionArgs); 303 Cursor cursor = queryBookmarkFromAPI(SUGGEST_PROJECTION, urlWhere.toString(), 304 selectionArgs, sortOrder); 305 return new ChromeBrowserProviderSuggestionsCursor(cursor); 306 } 307 308 /** 309 * @see android.content.ContentUris#parseId(Uri) 310 * @return The id from a content URI or -1 if the URI has no id or is malformed. 311 */ 312 private static long getContentUriId(Uri uri) { 313 try { 314 return ContentUris.parseId(uri); 315 } catch (UnsupportedOperationException e) { 316 return -1; 317 } catch (NumberFormatException e) { 318 return -1; 319 } 320 } 321 322 @Override 323 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 324 String sortOrder) { 325 if (!canHandleContentProviderApiCall()) return null; 326 327 // Check for invalid id values if provided. 328 long bookmarkId = getContentUriId(uri); 329 if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return null; 330 331 int match = mUriMatcher.match(uri); 332 Cursor cursor = null; 333 switch (match) { 334 case URL_MATCH_BOOKMARK_SUGGESTIONS_ID: 335 cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, true); 336 break; 337 case URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID: 338 cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, false); 339 break; 340 case URL_MATCH_API_BOOKMARK: 341 cursor = queryBookmarkFromAPI(projection, selection, selectionArgs, sortOrder); 342 break; 343 case URL_MATCH_API_BOOKMARK_ID: 344 cursor = queryBookmarkFromAPI(projection, buildWhereClause(bookmarkId, selection), 345 selectionArgs, sortOrder); 346 break; 347 case URL_MATCH_API_SEARCHES: 348 cursor = querySearchTermFromAPI(projection, selection, selectionArgs, sortOrder); 349 break; 350 case URL_MATCH_API_SEARCHES_ID: 351 cursor = querySearchTermFromAPI(projection, buildWhereClause(bookmarkId, selection), 352 selectionArgs, sortOrder); 353 break; 354 case URL_MATCH_API_HISTORY_CONTENT: 355 cursor = queryBookmarkFromAPI(projection, buildHistoryWhereClause(selection), 356 selectionArgs, sortOrder); 357 break; 358 case URL_MATCH_API_HISTORY_CONTENT_ID: 359 cursor = queryBookmarkFromAPI(projection, 360 buildHistoryWhereClause(bookmarkId, selection), selectionArgs, sortOrder); 361 break; 362 case URL_MATCH_API_BOOKMARK_CONTENT: 363 cursor = queryBookmarkFromAPI(projection, buildBookmarkWhereClause(selection), 364 selectionArgs, sortOrder); 365 break; 366 case URL_MATCH_API_BOOKMARK_CONTENT_ID: 367 cursor = queryBookmarkFromAPI(projection, 368 buildBookmarkWhereClause(bookmarkId, selection), selectionArgs, sortOrder); 369 break; 370 default: 371 throw new IllegalArgumentException(TAG + ": query - unknown URL uri = " + uri); 372 } 373 if (cursor == null) { 374 cursor = new MatrixCursor(new String[] { }); 375 } 376 cursor.setNotificationUri(getContext().getContentResolver(), uri); 377 return cursor; 378 } 379 380 @Override 381 public Uri insert(Uri uri, ContentValues values) { 382 if (!canHandleContentProviderApiCall()) return null; 383 384 int match = mUriMatcher.match(uri); 385 Uri res = null; 386 long id; 387 switch (match) { 388 case URI_MATCH_BOOKMARKS: 389 id = addBookmark(values); 390 if (id == INVALID_BOOKMARK_ID) return null; 391 break; 392 case URL_MATCH_API_BOOKMARK_CONTENT: 393 values.put(BookmarkColumns.BOOKMARK, 1); 394 //$FALL-THROUGH$ 395 case URL_MATCH_API_BOOKMARK: 396 case URL_MATCH_API_HISTORY_CONTENT: 397 id = addBookmarkFromAPI(values); 398 if (id == INVALID_CONTENT_PROVIDER_ID) return null; 399 break; 400 case URL_MATCH_API_SEARCHES: 401 id = addSearchTermFromAPI(values); 402 if (id == INVALID_CONTENT_PROVIDER_ID) return null; 403 break; 404 default: 405 throw new IllegalArgumentException(TAG + ": insert - unknown URL " + uri); 406 } 407 408 res = ContentUris.withAppendedId(uri, id); 409 notifyChange(res); 410 return res; 411 } 412 413 @Override 414 public int delete(Uri uri, String selection, String[] selectionArgs) { 415 if (!canHandleContentProviderApiCall()) return 0; 416 417 // Check for invalid id values if provided. 418 long bookmarkId = getContentUriId(uri); 419 if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0; 420 421 int match = mUriMatcher.match(uri); 422 int result; 423 switch (match) { 424 case URI_MATCH_BOOKMARKS_ID : 425 result = nativeRemoveBookmark(mNativeChromeBrowserProvider, bookmarkId); 426 break; 427 case URL_MATCH_API_BOOKMARK_ID: 428 result = removeBookmarkFromAPI( 429 buildWhereClause(bookmarkId, selection), selectionArgs); 430 break; 431 case URL_MATCH_API_BOOKMARK: 432 result = removeBookmarkFromAPI(selection, selectionArgs); 433 break; 434 case URL_MATCH_API_SEARCHES_ID: 435 result = removeSearchFromAPI(buildWhereClause(bookmarkId, selection), 436 selectionArgs); 437 break; 438 case URL_MATCH_API_SEARCHES: 439 result = removeSearchFromAPI(selection, selectionArgs); 440 break; 441 case URL_MATCH_API_HISTORY_CONTENT: 442 result = removeHistoryFromAPI(selection, selectionArgs); 443 break; 444 case URL_MATCH_API_HISTORY_CONTENT_ID: 445 result = removeHistoryFromAPI(buildWhereClause(bookmarkId, selection), 446 selectionArgs); 447 break; 448 case URL_MATCH_API_BOOKMARK_CONTENT: 449 result = removeBookmarkFromAPI(buildBookmarkWhereClause(selection), selectionArgs); 450 break; 451 case URL_MATCH_API_BOOKMARK_CONTENT_ID: 452 result = removeBookmarkFromAPI(buildBookmarkWhereClause(bookmarkId, selection), 453 selectionArgs); 454 break; 455 default: 456 throw new IllegalArgumentException(TAG + ": delete - unknown URL " + uri); 457 } 458 if (result != 0) notifyChange(uri); 459 return result; 460 } 461 462 @Override 463 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 464 if (!canHandleContentProviderApiCall()) return 0; 465 466 // Check for invalid id values if provided. 467 long bookmarkId = getContentUriId(uri); 468 if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0; 469 470 int match = mUriMatcher.match(uri); 471 int result; 472 switch (match) { 473 case URI_MATCH_BOOKMARKS_ID: 474 String url = null; 475 if (values.containsKey(Browser.BookmarkColumns.URL)) { 476 url = values.getAsString(Browser.BookmarkColumns.URL); 477 } 478 String title = values.getAsString(Browser.BookmarkColumns.TITLE); 479 long parentId = INVALID_BOOKMARK_ID; 480 if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { 481 parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); 482 } 483 result = nativeUpdateBookmark(mNativeChromeBrowserProvider, bookmarkId, url, title, 484 parentId); 485 updateLastModifiedBookmarkFolder(parentId); 486 break; 487 case URL_MATCH_API_BOOKMARK_ID: 488 result = updateBookmarkFromAPI(values, buildWhereClause(bookmarkId, selection), 489 selectionArgs); 490 break; 491 case URL_MATCH_API_BOOKMARK: 492 result = updateBookmarkFromAPI(values, selection, selectionArgs); 493 break; 494 case URL_MATCH_API_SEARCHES_ID: 495 result = updateSearchTermFromAPI(values, buildWhereClause(bookmarkId, selection), 496 selectionArgs); 497 break; 498 case URL_MATCH_API_SEARCHES: 499 result = updateSearchTermFromAPI(values, selection, selectionArgs); 500 break; 501 case URL_MATCH_API_HISTORY_CONTENT: 502 result = updateBookmarkFromAPI(values, buildHistoryWhereClause(selection), 503 selectionArgs); 504 break; 505 case URL_MATCH_API_HISTORY_CONTENT_ID: 506 result = updateBookmarkFromAPI(values, 507 buildHistoryWhereClause(bookmarkId, selection), selectionArgs); 508 break; 509 case URL_MATCH_API_BOOKMARK_CONTENT: 510 result = updateBookmarkFromAPI(values, buildBookmarkWhereClause(selection), 511 selectionArgs); 512 break; 513 case URL_MATCH_API_BOOKMARK_CONTENT_ID: 514 result = updateBookmarkFromAPI(values, 515 buildBookmarkWhereClause(bookmarkId, selection), selectionArgs); 516 break; 517 default: 518 throw new IllegalArgumentException(TAG + ": update - unknown URL " + uri); 519 } 520 if (result != 0) notifyChange(uri); 521 return result; 522 } 523 524 @Override 525 public String getType(Uri uri) { 526 ensureUriMatcherInitialized(); 527 int match = mUriMatcher.match(uri); 528 switch (match) { 529 case URI_MATCH_BOOKMARKS: 530 case URL_MATCH_API_BOOKMARK: 531 return "vnd.android.cursor.dir/bookmark"; 532 case URI_MATCH_BOOKMARKS_ID: 533 case URL_MATCH_API_BOOKMARK_ID: 534 return "vnd.android.cursor.item/bookmark"; 535 case URL_MATCH_API_SEARCHES: 536 return "vnd.android.cursor.dir/searches"; 537 case URL_MATCH_API_SEARCHES_ID: 538 return "vnd.android.cursor.item/searches"; 539 case URL_MATCH_API_HISTORY_CONTENT: 540 return BROWSER_CONTRACT_HISTORY_CONTENT_TYPE; 541 case URL_MATCH_API_HISTORY_CONTENT_ID: 542 return BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE; 543 default: 544 throw new IllegalArgumentException(TAG + ": getType - unknown URL " + uri); 545 } 546 } 547 548 private long addBookmark(ContentValues values) { 549 String url = values.getAsString(Browser.BookmarkColumns.URL); 550 String title = values.getAsString(Browser.BookmarkColumns.TITLE); 551 boolean isFolder = false; 552 if (values.containsKey(BOOKMARK_IS_FOLDER_PARAM)) { 553 isFolder = values.getAsBoolean(BOOKMARK_IS_FOLDER_PARAM); 554 } 555 long parentId = INVALID_BOOKMARK_ID; 556 if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { 557 parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); 558 } 559 long id = nativeAddBookmark(mNativeChromeBrowserProvider, url, title, isFolder, parentId); 560 if (id == INVALID_BOOKMARK_ID) return id; 561 562 if (isFolder) { 563 updateLastModifiedBookmarkFolder(id); 564 } else { 565 updateLastModifiedBookmarkFolder(parentId); 566 } 567 return id; 568 } 569 570 private void updateLastModifiedBookmarkFolder(long id) { 571 if (getLastModifiedBookmarkFolderId() == id) return; 572 573 mLastModifiedBookmarkFolderId = id; 574 SharedPreferences sharedPreferences = 575 PreferenceManager.getDefaultSharedPreferences(getContext()); 576 sharedPreferences.edit() 577 .putLong(LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, mLastModifiedBookmarkFolderId) 578 .apply(); 579 } 580 581 public static String getApiAuthority(Context context) { 582 return context.getPackageName() + API_AUTHORITY_SUFFIX; 583 } 584 585 public static String getInternalAuthority(Context context) { 586 return context.getPackageName() + AUTHORITY_SUFFIX; 587 } 588 589 public static Uri getBookmarksUri(Context context) { 590 return buildContentUri(getInternalAuthority(context), BOOKMARKS_PATH); 591 } 592 593 public static Uri getBookmarkFolderUri(Context context) { 594 return buildContentUri(getInternalAuthority(context), BOOKMARK_FOLDER_PATH); 595 } 596 597 public static Uri getBookmarksApiUri(Context context) { 598 return buildContentUri(getApiAuthority(context), BOOKMARKS_PATH); 599 } 600 601 public static Uri getSearchesApiUri(Context context) { 602 return buildContentUri(getApiAuthority(context), SEARCHES_PATH); 603 } 604 605 private boolean bookmarkNodeExists(long nodeId) { 606 if (nodeId < 0) return false; 607 return nativeBookmarkNodeExists(mNativeChromeBrowserProvider, nodeId); 608 } 609 610 private long createBookmarksFolderOnce(String title, long parentId) { 611 return nativeCreateBookmarksFolderOnce(mNativeChromeBrowserProvider, title, parentId); 612 } 613 614 private BookmarkNode getEditableBookmarkFolderHierarchy() { 615 return nativeGetEditableBookmarkFolders(mNativeChromeBrowserProvider); 616 } 617 618 protected BookmarkNode getBookmarkNode(long nodeId, boolean getParent, boolean getChildren, 619 boolean getFavicons, boolean getThumbnails) { 620 // Don't allow going up the hierarchy if sync is disabled and the requested node 621 // is the Mobile Bookmarks folder. 622 if (getParent && nodeId == getMobileBookmarksFolderId() 623 && !SyncStatusHelper.get(getContext()).isSyncEnabled()) { 624 getParent = false; 625 } 626 627 BookmarkNode node = nativeGetBookmarkNode(mNativeChromeBrowserProvider, nodeId, getParent, 628 getChildren); 629 if (!getFavicons && !getThumbnails) return node; 630 631 // Favicons and thumbnails need to be populated separately as they are provided 632 // asynchronously by Chromium services other than the bookmark model. 633 if (node.parent() != null) populateNodeImages(node.parent(), getFavicons, getThumbnails); 634 for (BookmarkNode child : node.children()) { 635 populateNodeImages(child, getFavicons, getThumbnails); 636 } 637 638 return node; 639 } 640 641 private BookmarkNode getDefaultBookmarkFolder() { 642 // Try to access the bookmark folder last modified by us. If it doesn't exist anymore 643 // then use the synced node (Mobile Bookmarks). 644 BookmarkNode lastModified = getBookmarkNode(getLastModifiedBookmarkFolderId(), false, false, 645 false, false); 646 if (lastModified == null || lastModified.isUrl()) { 647 lastModified = getMobileBookmarksFolder(); 648 mLastModifiedBookmarkFolderId = lastModified != null ? lastModified.id() : 649 INVALID_BOOKMARK_ID; 650 } 651 return lastModified; 652 } 653 654 private void populateNodeImages(BookmarkNode node, boolean favicon, boolean thumbnail) { 655 if (node == null || node.type() != Type.URL) return; 656 657 if (favicon) { 658 node.setFavicon(nativeGetFaviconOrTouchIcon(mNativeChromeBrowserProvider, node.url())); 659 } 660 661 if (thumbnail) { 662 node.setThumbnail(nativeGetThumbnail(mNativeChromeBrowserProvider, node.url())); 663 } 664 } 665 666 private BookmarkNode getMobileBookmarksFolder() { 667 if (mMobileBookmarksFolder == null) { 668 mMobileBookmarksFolder = nativeGetMobileBookmarksFolder(mNativeChromeBrowserProvider); 669 } 670 return mMobileBookmarksFolder; 671 } 672 673 protected long getMobileBookmarksFolderId() { 674 BookmarkNode mobileBookmarks = getMobileBookmarksFolder(); 675 return mobileBookmarks != null ? mobileBookmarks.id() : INVALID_BOOKMARK_ID; 676 } 677 678 private boolean isBookmarkInMobileBookmarksBranch(long nodeId) { 679 if (nodeId <= 0) return false; 680 return nativeIsBookmarkInMobileBookmarksBranch(mNativeChromeBrowserProvider, nodeId); 681 } 682 683 static String argKey(int i) { 684 return "arg" + i; 685 } 686 687 @Override 688 public Bundle call(String method, String arg, Bundle extras) { 689 // TODO(shashishekhar): Refactor this code into a separate class. 690 // Caller must have the READ_WRITE_BOOKMARK_FOLDERS permission. 691 getContext().enforcePermission(getReadWritePermissionNameForBookmarkFolders(), 692 Binder.getCallingPid(), Binder.getCallingUid(), TAG); 693 if (!canHandleContentProviderApiCall()) return null; 694 if (method == null || extras == null) return null; 695 696 Bundle result = new Bundle(); 697 if (CLIENT_API_BOOKMARK_NODE_EXISTS.equals(method)) { 698 result.putBoolean(CLIENT_API_RESULT_KEY, 699 bookmarkNodeExists(extras.getLong(argKey(0)))); 700 } else if (CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE.equals(method)) { 701 result.putLong(CLIENT_API_RESULT_KEY, 702 createBookmarksFolderOnce(extras.getString(argKey(0)), 703 extras.getLong(argKey(1)))); 704 } else if (CLIENT_API_GET_EDITABLE_BOOKMARK_FOLDER_HIERARCHY.equals(method)) { 705 result.putParcelable(CLIENT_API_RESULT_KEY, getEditableBookmarkFolderHierarchy()); 706 } else if (CLIENT_API_GET_BOOKMARK_NODE.equals(method)) { 707 result.putParcelable(CLIENT_API_RESULT_KEY, 708 getBookmarkNode(extras.getLong(argKey(0)), 709 extras.getBoolean(argKey(1)), 710 extras.getBoolean(argKey(2)), 711 extras.getBoolean(argKey(3)), 712 extras.getBoolean(argKey(4)))); 713 } else if (CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER.equals(method)) { 714 result.putParcelable(CLIENT_API_RESULT_KEY, getDefaultBookmarkFolder()); 715 } else if (method.equals(CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID)) { 716 result.putLong(CLIENT_API_RESULT_KEY, getMobileBookmarksFolderId()); 717 } else if (CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH.equals(method)) { 718 result.putBoolean(CLIENT_API_RESULT_KEY, 719 isBookmarkInMobileBookmarksBranch(extras.getLong(argKey(0)))); 720 } else if (CLIENT_API_DELETE_ALL_USER_BOOKMARKS.equals(method)) { 721 nativeRemoveAllUserBookmarks(mNativeChromeBrowserProvider); 722 } else { 723 Log.w(TAG, "Received invalid method " + method); 724 return null; 725 } 726 727 return result; 728 } 729 730 /** 731 * Checks whether Chrome is sufficiently initialized to handle a call to the 732 * ChromeBrowserProvider. 733 */ 734 private boolean canHandleContentProviderApiCall() { 735 mContentProviderApiCalled = true; 736 737 if (isInUiThread()) return false; 738 if (!ensureNativeChromeLoaded()) return false; 739 return true; 740 } 741 742 /** 743 * The type of a BookmarkNode. 744 */ 745 public enum Type { 746 URL, 747 FOLDER, 748 BOOKMARK_BAR, 749 OTHER_NODE, 750 MOBILE 751 } 752 753 /** 754 * Simple Data Object representing the chrome bookmark node. 755 */ 756 public static class BookmarkNode implements Parcelable { 757 private final long mId; 758 private final String mName; 759 private final String mUrl; 760 private final Type mType; 761 private final BookmarkNode mParent; 762 private final List<BookmarkNode> mChildren = new ArrayList<BookmarkNode>(); 763 764 // Favicon and thumbnail optionally set in a 2-step procedure. 765 private byte[] mFavicon; 766 private byte[] mThumbnail; 767 768 /** Used to pass structured data back from the native code. */ 769 @VisibleForTesting 770 public BookmarkNode(long id, Type type, String name, String url, BookmarkNode parent) { 771 mId = id; 772 mName = name; 773 mUrl = url; 774 mType = type; 775 mParent = parent; 776 } 777 778 /** 779 * @return The id of this bookmark entry. 780 */ 781 public long id() { 782 return mId; 783 } 784 785 /** 786 * @return The name of this bookmark entry. 787 */ 788 public String name() { 789 return mName; 790 } 791 792 /** 793 * @return The URL of this bookmark entry. 794 */ 795 public String url() { 796 return mUrl; 797 } 798 799 /** 800 * @return The type of this bookmark entry. 801 */ 802 public Type type() { 803 return mType; 804 } 805 806 /** 807 * @return The bookmark favicon, if any. 808 */ 809 public byte[] favicon() { 810 return mFavicon; 811 } 812 813 /** 814 * @return The bookmark thumbnail, if any. 815 */ 816 public byte[] thumbnail() { 817 return mThumbnail; 818 } 819 820 /** 821 * @return The parent folder of this bookmark entry. 822 */ 823 public BookmarkNode parent() { 824 return mParent; 825 } 826 827 /** 828 * Adds a child to this node. 829 * 830 * <p> 831 * Used solely by the native code. 832 */ 833 @VisibleForTesting 834 @CalledByNativeUnchecked("BookmarkNode") 835 public void addChild(BookmarkNode child) { 836 mChildren.add(child); 837 } 838 839 /** 840 * @return The child bookmark nodes of this node. 841 */ 842 public List<BookmarkNode> children() { 843 return mChildren; 844 } 845 846 /** 847 * @return Whether this node represents a bookmarked URL or not. 848 */ 849 public boolean isUrl() { 850 return mUrl != null; 851 } 852 853 /** 854 * @return true if the two individual nodes contain the same information. 855 * The existence of parent and children nodes is checked, but their contents are not. 856 */ 857 public boolean equalContents(BookmarkNode node) { 858 return node != null && 859 mId == node.mId && 860 !(mName == null ^ node.mName == null) && 861 (mName == null || mName.equals(node.mName)) && 862 !(mUrl == null ^ node.mUrl == null) && 863 (mUrl == null || mUrl.equals(node.mUrl)) && 864 mType == node.mType && 865 byteArrayEqual(mFavicon, node.mFavicon) && 866 byteArrayEqual(mThumbnail, node.mThumbnail) && 867 !(mParent == null ^ node.mParent == null) && 868 children().size() == node.children().size(); 869 } 870 871 private static boolean byteArrayEqual(byte[] byte1, byte[] byte2) { 872 if (byte1 == null && byte2 != null) return byte2.length == 0; 873 if (byte2 == null && byte1 != null) return byte1.length == 0; 874 return Arrays.equals(byte1, byte2); 875 } 876 877 @CalledByNative("BookmarkNode") 878 private static BookmarkNode create( 879 long id, int type, String name, String url, BookmarkNode parent) { 880 return new BookmarkNode(id, Type.values()[type], name, url, parent); 881 } 882 883 @VisibleForTesting 884 public void setFavicon(byte[] favicon) { 885 mFavicon = favicon; 886 } 887 888 @VisibleForTesting 889 public void setThumbnail(byte[] thumbnail) { 890 mThumbnail = thumbnail; 891 } 892 893 @Override 894 public int describeContents() { 895 return 0; 896 } 897 898 @Override 899 public void writeToParcel(Parcel dest, int flags) { 900 // Write the current node id. 901 dest.writeLong(mId); 902 903 // Serialize the full hierarchy from the root. 904 getHierarchyRoot().writeNodeContentsRecursive(dest); 905 } 906 907 @VisibleForTesting 908 public BookmarkNode getHierarchyRoot() { 909 BookmarkNode root = this; 910 while (root.parent() != null) { 911 root = root.parent(); 912 } 913 return root; 914 } 915 916 private void writeNodeContentsRecursive(Parcel dest) { 917 writeNodeContents(dest); 918 dest.writeInt(mChildren.size()); 919 for (BookmarkNode child : mChildren) { 920 child.writeNodeContentsRecursive(dest); 921 } 922 } 923 924 private void writeNodeContents(Parcel dest) { 925 dest.writeLong(mId); 926 dest.writeString(mName); 927 dest.writeString(mUrl); 928 dest.writeInt(mType.ordinal()); 929 dest.writeByteArray(mFavicon); 930 dest.writeByteArray(mThumbnail); 931 dest.writeLong(mParent != null ? mParent.mId : INVALID_BOOKMARK_ID); 932 } 933 934 public static final Creator<BookmarkNode> CREATOR = new Creator<BookmarkNode>() { 935 private HashMap<Long, BookmarkNode> mNodeMap; 936 937 @Override 938 public BookmarkNode createFromParcel(Parcel source) { 939 mNodeMap = new HashMap<Long, BookmarkNode>(); 940 long currentNodeId = source.readLong(); 941 readNodeContentsRecursive(source); 942 BookmarkNode node = getNode(currentNodeId); 943 mNodeMap.clear(); 944 return node; 945 } 946 947 @Override 948 public BookmarkNode[] newArray(int size) { 949 return new BookmarkNode[size]; 950 } 951 952 private BookmarkNode getNode(long id) { 953 if (id == INVALID_BOOKMARK_ID) return null; 954 Long nodeId = Long.valueOf(id); 955 if (!mNodeMap.containsKey(nodeId)) { 956 Log.e(TAG, "Invalid BookmarkNode hierarchy. Unknown id " + id); 957 return null; 958 } 959 return mNodeMap.get(nodeId); 960 } 961 962 private BookmarkNode readNodeContents(Parcel source) { 963 long id = source.readLong(); 964 String name = source.readString(); 965 String url = source.readString(); 966 int type = source.readInt(); 967 byte[] favicon = source.createByteArray(); 968 byte[] thumbnail = source.createByteArray(); 969 long parentId = source.readLong(); 970 if (type < 0 || type >= Type.values().length) { 971 Log.w(TAG, "Invalid node type ordinal value."); 972 return null; 973 } 974 975 BookmarkNode node = new BookmarkNode(id, Type.values()[type], name, url, 976 getNode(parentId)); 977 node.setFavicon(favicon); 978 node.setThumbnail(thumbnail); 979 return node; 980 } 981 982 private BookmarkNode readNodeContentsRecursive(Parcel source) { 983 BookmarkNode node = readNodeContents(source); 984 if (node == null) return null; 985 986 Long nodeId = Long.valueOf(node.id()); 987 if (mNodeMap.containsKey(nodeId)) { 988 Log.e(TAG, "Invalid BookmarkNode hierarchy. Duplicate id " + node.id()); 989 return null; 990 } 991 mNodeMap.put(nodeId, node); 992 993 int numChildren = source.readInt(); 994 for (int i = 0; i < numChildren; ++i) { 995 node.addChild(readNodeContentsRecursive(source)); 996 } 997 998 return node; 999 } 1000 }; 1001 } 1002 1003 private long addBookmarkFromAPI(ContentValues values) { 1004 BookmarkRow row = BookmarkRow.fromContentValues(values); 1005 if (row.mUrl == null) { 1006 throw new IllegalArgumentException("Must have a bookmark URL"); 1007 } 1008 return nativeAddBookmarkFromAPI(mNativeChromeBrowserProvider, 1009 row.mUrl, row.mCreated, row.mIsBookmark, row.mDate, row.mFavicon, 1010 row.mTitle, row.mVisits, row.mParentId); 1011 } 1012 1013 private Cursor queryBookmarkFromAPI(String[] projectionIn, String selection, 1014 String[] selectionArgs, String sortOrder) { 1015 String[] projection = null; 1016 if (projectionIn == null || projectionIn.length == 0) { 1017 projection = BOOKMARK_DEFAULT_PROJECTION; 1018 } else { 1019 projection = projectionIn; 1020 } 1021 1022 return nativeQueryBookmarkFromAPI(mNativeChromeBrowserProvider, projection, selection, 1023 selectionArgs, sortOrder); 1024 } 1025 1026 private int updateBookmarkFromAPI(ContentValues values, String selection, 1027 String[] selectionArgs) { 1028 BookmarkRow row = BookmarkRow.fromContentValues(values); 1029 return nativeUpdateBookmarkFromAPI(mNativeChromeBrowserProvider, 1030 row.mUrl, row.mCreated, row.mIsBookmark, row.mDate, 1031 row.mFavicon, row.mTitle, row.mVisits, row.mParentId, selection, selectionArgs); 1032 } 1033 1034 private int removeBookmarkFromAPI(String selection, String[] selectionArgs) { 1035 return nativeRemoveBookmarkFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs); 1036 } 1037 1038 private int removeHistoryFromAPI(String selection, String[] selectionArgs) { 1039 return nativeRemoveHistoryFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs); 1040 } 1041 1042 @CalledByNative 1043 private void onBookmarkChanged() { 1044 notifyChange(buildAPIContentUri(getContext(), BOOKMARKS_PATH)); 1045 } 1046 1047 @CalledByNative 1048 private void onHistoryChanged() { 1049 notifyChange(buildAPIContentUri(getContext(), HISTORY_PATH)); 1050 } 1051 1052 @CalledByNative 1053 private void onSearchTermChanged() { 1054 notifyChange(buildAPIContentUri(getContext(), SEARCHES_PATH)); 1055 } 1056 1057 private long addSearchTermFromAPI(ContentValues values) { 1058 SearchRow row = SearchRow.fromContentValues(values); 1059 if (row.mTerm == null) { 1060 throw new IllegalArgumentException("Must have a search term"); 1061 } 1062 return nativeAddSearchTermFromAPI(mNativeChromeBrowserProvider, row.mTerm, row.mDate); 1063 } 1064 1065 private int updateSearchTermFromAPI(ContentValues values, String selection, 1066 String[] selectionArgs) { 1067 SearchRow row = SearchRow.fromContentValues(values); 1068 return nativeUpdateSearchTermFromAPI(mNativeChromeBrowserProvider, 1069 row.mTerm, row.mDate, selection, selectionArgs); 1070 } 1071 1072 private Cursor querySearchTermFromAPI(String[] projectionIn, String selection, 1073 String[] selectionArgs, String sortOrder) { 1074 String[] projection = null; 1075 if (projectionIn == null || projectionIn.length == 0) { 1076 projection = android.provider.Browser.SEARCHES_PROJECTION; 1077 } else { 1078 projection = projectionIn; 1079 } 1080 return nativeQuerySearchTermFromAPI(mNativeChromeBrowserProvider, projection, selection, 1081 selectionArgs, sortOrder); 1082 } 1083 1084 private int removeSearchFromAPI(String selection, String[] selectionArgs) { 1085 return nativeRemoveSearchTermFromAPI(mNativeChromeBrowserProvider, 1086 selection, selectionArgs); 1087 } 1088 1089 private static boolean isInUiThread() { 1090 if (!ThreadUtils.runningOnUiThread()) return false; 1091 1092 if (!"REL".equals(Build.VERSION.CODENAME)) { 1093 throw new IllegalStateException("Shouldn't run in the UI thread"); 1094 } 1095 1096 Log.w(TAG, "ChromeBrowserProvider methods cannot be called from the UI thread."); 1097 return true; 1098 } 1099 1100 private static Uri buildContentUri(String authority, String path) { 1101 return Uri.parse("content://" + authority + "/" + path); 1102 } 1103 1104 private static Uri buildAPIContentUri(Context context, String path) { 1105 return buildContentUri(context.getPackageName() + API_AUTHORITY_SUFFIX, path); 1106 } 1107 1108 private static String buildWhereClause(long id, String selection) { 1109 StringBuffer sb = new StringBuffer(); 1110 sb.append(BaseColumns._ID); 1111 sb.append(" = "); 1112 sb.append(id); 1113 if (!TextUtils.isEmpty(selection)) { 1114 sb.append(" AND ("); 1115 sb.append(selection); 1116 sb.append(")"); 1117 } 1118 return sb.toString(); 1119 } 1120 1121 private static String buildHistoryWhereClause(long id, String selection) { 1122 return buildWhereClause(id, buildBookmarkWhereClause(selection, false)); 1123 } 1124 1125 private static String buildHistoryWhereClause(String selection) { 1126 return buildBookmarkWhereClause(selection, false); 1127 } 1128 1129 /** 1130 * @return a SQL where class which is inserted the bookmark condition. 1131 */ 1132 private static String buildBookmarkWhereClause(String selection, boolean isBookmark) { 1133 StringBuffer sb = new StringBuffer(); 1134 sb.append(BookmarkColumns.BOOKMARK); 1135 sb.append(isBookmark ? " = 1 " : " = 0"); 1136 if (!TextUtils.isEmpty(selection)) { 1137 sb.append(" AND ("); 1138 sb.append(selection); 1139 sb.append(")"); 1140 } 1141 return sb.toString(); 1142 } 1143 1144 private static String buildBookmarkWhereClause(long id, String selection) { 1145 return buildWhereClause(id, buildBookmarkWhereClause(selection, true)); 1146 } 1147 1148 private static String buildBookmarkWhereClause(String selection) { 1149 return buildBookmarkWhereClause(selection, true); 1150 } 1151 1152 // Wrap the value of BookmarkColumn. 1153 private static class BookmarkRow { 1154 Boolean mIsBookmark; 1155 Long mCreated; 1156 String mUrl; 1157 Long mDate; 1158 byte[] mFavicon; 1159 String mTitle; 1160 Integer mVisits; 1161 long mParentId; 1162 1163 static BookmarkRow fromContentValues(ContentValues values) { 1164 BookmarkRow row = new BookmarkRow(); 1165 if (values.containsKey(BookmarkColumns.URL)) { 1166 row.mUrl = values.getAsString(BookmarkColumns.URL); 1167 } 1168 if (values.containsKey(BookmarkColumns.BOOKMARK)) { 1169 row.mIsBookmark = values.getAsInteger(BookmarkColumns.BOOKMARK) != 0; 1170 } 1171 if (values.containsKey(BookmarkColumns.CREATED)) { 1172 row.mCreated = values.getAsLong(BookmarkColumns.CREATED); 1173 } 1174 if (values.containsKey(BookmarkColumns.DATE)) { 1175 row.mDate = values.getAsLong(BookmarkColumns.DATE); 1176 } 1177 if (values.containsKey(BookmarkColumns.FAVICON)) { 1178 row.mFavicon = values.getAsByteArray(BookmarkColumns.FAVICON); 1179 // We need to know that the caller set the favicon column. 1180 if (row.mFavicon == null) { 1181 row.mFavicon = new byte[0]; 1182 } 1183 } 1184 if (values.containsKey(BookmarkColumns.TITLE)) { 1185 row.mTitle = values.getAsString(BookmarkColumns.TITLE); 1186 } 1187 if (values.containsKey(BookmarkColumns.VISITS)) { 1188 row.mVisits = values.getAsInteger(BookmarkColumns.VISITS); 1189 } 1190 if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { 1191 row.mParentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); 1192 } 1193 return row; 1194 } 1195 } 1196 1197 // Wrap the value of SearchColumn. 1198 private static class SearchRow { 1199 String mTerm; 1200 Long mDate; 1201 1202 static SearchRow fromContentValues(ContentValues values) { 1203 SearchRow row = new SearchRow(); 1204 if (values.containsKey(SearchColumns.SEARCH)) { 1205 row.mTerm = values.getAsString(SearchColumns.SEARCH); 1206 } 1207 if (values.containsKey(SearchColumns.DATE)) { 1208 row.mDate = values.getAsLong(SearchColumns.DATE); 1209 } 1210 return row; 1211 } 1212 } 1213 1214 /** 1215 * Returns true if the native side of the class is initialized. 1216 */ 1217 protected boolean isNativeSideInitialized() { 1218 return mNativeChromeBrowserProvider != 0; 1219 } 1220 1221 /** 1222 * Make sure chrome is running. This method mustn't run on UI thread. 1223 * 1224 * @return Whether the native chrome process is running successfully once this has returned. 1225 */ 1226 private boolean ensureNativeChromeLoaded() { 1227 ensureUriMatcherInitialized(); 1228 1229 synchronized (mLoadNativeLock) { 1230 if (mNativeChromeBrowserProvider != 0) return true; 1231 1232 final AtomicBoolean retVal = new AtomicBoolean(true); 1233 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 1234 @Override 1235 public void run() { 1236 retVal.set(ensureNativeChromeLoadedOnUIThread()); 1237 } 1238 }); 1239 return retVal.get(); 1240 } 1241 } 1242 1243 /** 1244 * This method should only run on UI thread. 1245 */ 1246 protected boolean ensureNativeChromeLoadedOnUIThread() { 1247 if (isNativeSideInitialized()) return true; 1248 mNativeChromeBrowserProvider = nativeInit(); 1249 return isNativeSideInitialized(); 1250 } 1251 1252 @Override 1253 protected void finalize() throws Throwable { 1254 try { 1255 // Tests might try to destroy this in the wrong thread. 1256 ThreadUtils.runOnUiThreadBlocking(new Runnable() { 1257 @Override 1258 public void run() { 1259 ensureNativeChromeDestroyedOnUIThread(); 1260 } 1261 }); 1262 } finally { 1263 super.finalize(); 1264 } 1265 } 1266 1267 /** 1268 * This method should only run on UI thread. 1269 */ 1270 private void ensureNativeChromeDestroyedOnUIThread() { 1271 if (isNativeSideInitialized()) { 1272 nativeDestroy(mNativeChromeBrowserProvider); 1273 mNativeChromeBrowserProvider = 0; 1274 } 1275 } 1276 1277 @SuppressLint("NewApi") 1278 private void notifyChange(final Uri uri) { 1279 // If the calling user is different than current one, we need to post a 1280 // task to notify change, otherwise, a system level hidden permission 1281 // INTERACT_ACROSS_USERS_FULL is needed. 1282 // The related APIs were added in API 17, it should be safe to fallback to 1283 // normal way for notifying change, because caller can't be other users in 1284 // devices whose API level is less than API 17. 1285 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 1286 UserHandle callingUserHandle = Binder.getCallingUserHandle(); 1287 if (callingUserHandle != null && 1288 !callingUserHandle.equals(android.os.Process.myUserHandle())) { 1289 ThreadUtils.postOnUiThread(new Runnable() { 1290 @Override 1291 public void run() { 1292 getContext().getContentResolver().notifyChange(uri, null); 1293 } 1294 }); 1295 return; 1296 } 1297 } 1298 getContext().getContentResolver().notifyChange(uri, null); 1299 } 1300 1301 private native long nativeInit(); 1302 private native void nativeDestroy(long nativeChromeBrowserProvider); 1303 1304 // Public API native methods. 1305 private native long nativeAddBookmark(long nativeChromeBrowserProvider, 1306 String url, String title, boolean isFolder, long parentId); 1307 1308 private native int nativeRemoveBookmark(long nativeChromeBrowserProvider, long id); 1309 1310 private native int nativeUpdateBookmark(long nativeChromeBrowserProvider, 1311 long id, String url, String title, long parentId); 1312 1313 private native long nativeAddBookmarkFromAPI(long nativeChromeBrowserProvider, 1314 String url, Long created, Boolean isBookmark, Long date, byte[] favicon, 1315 String title, Integer visits, long parentId); 1316 1317 private native SQLiteCursor nativeQueryBookmarkFromAPI(long nativeChromeBrowserProvider, 1318 String[] projection, String selection, String[] selectionArgs, String sortOrder); 1319 1320 private native int nativeUpdateBookmarkFromAPI(long nativeChromeBrowserProvider, 1321 String url, Long created, Boolean isBookmark, Long date, byte[] favicon, 1322 String title, Integer visits, long parentId, String selection, String[] selectionArgs); 1323 1324 private native int nativeRemoveBookmarkFromAPI(long nativeChromeBrowserProvider, 1325 String selection, String[] selectionArgs); 1326 1327 private native int nativeRemoveHistoryFromAPI(long nativeChromeBrowserProvider, 1328 String selection, String[] selectionArgs); 1329 1330 private native long nativeAddSearchTermFromAPI(long nativeChromeBrowserProvider, 1331 String term, Long date); 1332 1333 private native SQLiteCursor nativeQuerySearchTermFromAPI(long nativeChromeBrowserProvider, 1334 String[] projection, String selection, String[] selectionArgs, String sortOrder); 1335 1336 private native int nativeUpdateSearchTermFromAPI(long nativeChromeBrowserProvider, 1337 String search, Long date, String selection, String[] selectionArgs); 1338 1339 private native int nativeRemoveSearchTermFromAPI(long nativeChromeBrowserProvider, 1340 String selection, String[] selectionArgs); 1341 1342 // Client API native methods. 1343 private native boolean nativeBookmarkNodeExists(long nativeChromeBrowserProvider, long id); 1344 1345 private native long nativeCreateBookmarksFolderOnce(long nativeChromeBrowserProvider, 1346 String title, long parentId); 1347 1348 private native BookmarkNode nativeGetEditableBookmarkFolders(long nativeChromeBrowserProvider); 1349 1350 private native void nativeRemoveAllUserBookmarks(long nativeChromeBrowserProvider); 1351 1352 private native BookmarkNode nativeGetBookmarkNode(long nativeChromeBrowserProvider, 1353 long id, boolean getParent, boolean getChildren); 1354 1355 private native BookmarkNode nativeGetMobileBookmarksFolder(long nativeChromeBrowserProvider); 1356 1357 private native boolean nativeIsBookmarkInMobileBookmarksBranch(long nativeChromeBrowserProvider, 1358 long id); 1359 1360 private native byte[] nativeGetFaviconOrTouchIcon(long nativeChromeBrowserProvider, String url); 1361 1362 private native byte[] nativeGetThumbnail(long nativeChromeBrowserProvider, String url); 1363} 1364