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