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