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