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