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