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