root/chrome/android/java/src/org/chromium/chrome/browser/ChromeBrowserProvider.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ensureUriMatcherInitialized
  2. onCreate
  3. getLastModifiedBookmarkFolderId
  4. buildSuggestWhere
  5. getReadWritePermissionNameForBookmarkFolders
  6. getBookmarkHistorySuggestions
  7. getContentUriId
  8. query
  9. insert
  10. delete
  11. update
  12. getType
  13. addBookmark
  14. updateLastModifiedBookmarkFolder
  15. getApiAuthority
  16. getInternalAuthority
  17. getBookmarksUri
  18. getBookmarkFolderUri
  19. getBookmarksApiUri
  20. getSearchesApiUri
  21. bookmarkNodeExists
  22. createBookmarksFolderOnce
  23. getBookmarkFolderHierarchy
  24. getBookmarkNode
  25. getDefaultBookmarkFolder
  26. populateNodeImages
  27. getMobileBookmarksFolder
  28. getMobileBookmarksFolderId
  29. isBookmarkInMobileBookmarksBranch
  30. argKey
  31. call
  32. canHandleContentProviderApiCall
  33. id
  34. name
  35. url
  36. type
  37. favicon
  38. thumbnail
  39. parent
  40. CalledByNativeUnchecked
  41. addChild
  42. children
  43. isUrl
  44. equalContents
  45. byteArrayEqual
  46. CalledByNative
  47. create
  48. setFavicon
  49. setThumbnail
  50. describeContents
  51. writeToParcel
  52. getHierarchyRoot
  53. writeNodeContentsRecursive
  54. writeNodeContents
  55. addBookmarkFromAPI
  56. queryBookmarkFromAPI
  57. updateBookmarkFromAPI
  58. removeBookmarkFromAPI
  59. removeHistoryFromAPI
  60. onBookmarkChanged
  61. onSearchTermChanged
  62. addSearchTermFromAPI
  63. updateSearchTermFromAPI
  64. querySearchTermFromAPI
  65. removeSearchFromAPI
  66. isInUiThread
  67. buildContentUri
  68. buildAPIContentUri
  69. buildWhereClause
  70. buildHistoryWhereClause
  71. buildHistoryWhereClause
  72. buildBookmarkWhereClause
  73. buildBookmarkWhereClause
  74. buildBookmarkWhereClause
  75. fromContentValues
  76. fromContentValues
  77. isNativeSideInitialized
  78. ensureNativeChromeLoaded
  79. ensureNativeChromeLoadedOnUIThread
  80. finalize
  81. ensureNativeChromeDestroyedOnUIThread
  82. getShortcutToBookmark
  83. SuppressLint
  84. notifyChange
  85. nativeInit
  86. nativeDestroy
  87. nativeAddBookmark
  88. nativeRemoveBookmark
  89. nativeUpdateBookmark
  90. nativeAddBookmarkFromAPI
  91. nativeQueryBookmarkFromAPI
  92. nativeUpdateBookmarkFromAPI
  93. nativeRemoveBookmarkFromAPI
  94. nativeRemoveHistoryFromAPI
  95. nativeAddSearchTermFromAPI
  96. nativeQuerySearchTermFromAPI
  97. nativeUpdateSearchTermFromAPI
  98. nativeRemoveSearchTermFromAPI
  99. nativeBookmarkNodeExists
  100. nativeCreateBookmarksFolderOnce
  101. nativeGetAllBookmarkFolders
  102. nativeRemoveAllBookmarks
  103. nativeGetBookmarkNode
  104. nativeGetMobileBookmarksFolder
  105. nativeIsBookmarkInMobileBookmarksBranch
  106. nativeGetFaviconOrTouchIcon
  107. nativeGetThumbnail

// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser;

import android.annotation.SuppressLint;
import android.app.SearchManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.Browser;
import android.provider.Browser.BookmarkColumns;
import android.provider.Browser.SearchColumns;
import android.text.TextUtils;
import android.util.Log;

import com.google.common.annotations.VisibleForTesting;

import org.chromium.base.CalledByNative;
import org.chromium.base.CalledByNativeUnchecked;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.database.SQLiteCursor;
import org.chromium.sync.notifier.SyncStatusHelper;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This class provides access to user data stored in Chrome, such as bookmarks, most visited pages,
 * etc. It is used to support android.provider.Browser.
 */
public class ChromeBrowserProvider extends ContentProvider {
    private static final String TAG = "ChromeBrowserProvider";

    // The permission required for using the bookmark folders API. Android build system does
    // not generate Manifest.java for java libraries, hence use the permission name string. When
    // making changes to this permission, also update the permission in AndroidManifest.xml.
    private static final String PERMISSION_READ_WRITE_BOOKMARKS = "READ_WRITE_BOOKMARK_FOLDERS";

    // Defines the API methods that the Client can call by name.
    static final String CLIENT_API_BOOKMARK_NODE_EXISTS = "BOOKMARK_NODE_EXISTS";
    static final String CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE = "CREATE_BOOKMARKS_FOLDER_ONCE";
    static final String CLIENT_API_GET_BOOKMARK_FOLDER_HIERARCHY = "GET_BOOKMARK_FOLDER_HIERARCHY";
    static final String CLIENT_API_GET_BOOKMARK_NODE = "GET_BOOKMARK_NODE";
    static final String CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER = "GET_DEFAULT_BOOKMARK_FOLDER";
    static final String CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID =
            "GET_MOBILE_BOOKMARKS_FOLDER_ID";
    static final String CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH =
            "IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH";
    static final String CLIENT_API_DELETE_ALL_BOOKMARKS = "DELETE_ALL_BOOKMARKS";
    static final String CLIENT_API_RESULT_KEY = "result";


    // Defines Chrome's API authority, so it can be run and tested
    // independently.
    private static final String API_AUTHORITY_SUFFIX = ".browser";

    private static final String BROWSER_CONTRACT_API_AUTHORITY =
        "com.google.android.apps.chrome.browser-contract";

    // These values are taken from android.provider.BrowserContract.java since
    // that class is hidden from the SDK.
    private static final String BROWSER_CONTRACT_AUTHORITY = "com.android.browser";
    private static final String BROWSER_CONTRACT_HISTORY_CONTENT_TYPE =
        "vnd.android.cursor.dir/browser-history";
    private static final String BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE =
        "vnd.android.cursor.item/browser-history";

    // This Authority is for internal interface. It's concatenated with
    // Context.getPackageName() so that we can install different channels
    // SxS and have different authorities.
    private static final String AUTHORITY_SUFFIX = ".ChromeBrowserProvider";
    private static final String BOOKMARKS_PATH = "bookmarks";
    private static final String SEARCHES_PATH = "searches";
    private static final String HISTORY_PATH = "history";
    private static final String COMBINED_PATH = "combined";
    private static final String BOOKMARK_FOLDER_PATH = "hierarchy";

    public static final Uri BROWSER_CONTRACTS_BOOKMAKRS_API_URI = buildContentUri(
            BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH);

    public static final Uri BROWSER_CONTRACTS_SEARCHES_API_URI = buildContentUri(
            BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH);

    public static final Uri BROWSER_CONTRACTS_HISTORY_API_URI = buildContentUri(
            BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH);

    public static final Uri BROWSER_CONTRACTS_COMBINED_API_URI = buildContentUri(
            BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH);

    /** The parameter used to specify a bookmark parent ID in ContentValues. */
    public static final String BOOKMARK_PARENT_ID_PARAM = "parentId";

    /** The parameter used to specify whether this is a bookmark folder. */
    public static final String BOOKMARK_IS_FOLDER_PARAM = "isFolder";

    /**
     * Invalid ID value for the Android ContentProvider API calls.
     * The value 0 is intentional: if the ID represents a bookmark node then it's the root node
     * and not accessible. Otherwise it represents a SQLite row id, so 0 is also invalid.
     */
    public static final long INVALID_CONTENT_PROVIDER_ID = 0;

    // ID used to indicate an invalid id for bookmark nodes.
    // Client API queries should use ChromeBrowserProviderClient.INVALID_BOOKMARK_ID.
    static final long INVALID_BOOKMARK_ID = -1;

    private static final String LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY = "last_bookmark_folder_id";

    private static final int URI_MATCH_BOOKMARKS = 0;
    private static final int URI_MATCH_BOOKMARKS_ID = 1;
    private static final int URL_MATCH_API_BOOKMARK = 2;
    private static final int URL_MATCH_API_BOOKMARK_ID = 3;
    private static final int URL_MATCH_API_SEARCHES = 4;
    private static final int URL_MATCH_API_SEARCHES_ID = 5;
    private static final int URL_MATCH_API_HISTORY_CONTENT = 6;
    private static final int URL_MATCH_API_HISTORY_CONTENT_ID = 7;
    private static final int URL_MATCH_API_BOOKMARK_CONTENT = 8;
    private static final int URL_MATCH_API_BOOKMARK_CONTENT_ID = 9;
    private static final int URL_MATCH_BOOKMARK_SUGGESTIONS_ID = 10;
    private static final int URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID = 11;

    // TODO : Using Android.provider.Browser.HISTORY_PROJECTION once THUMBNAIL,
    // TOUCH_ICON, and USER_ENTERED fields are supported.
    private static final String[] BOOKMARK_DEFAULT_PROJECTION = new String[] {
        BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS,
        BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE,
        BookmarkColumns.FAVICON, BookmarkColumns.CREATED
    };

    private static final String[] SUGGEST_PROJECTION = new String[] {
        BookmarkColumns._ID,
        BookmarkColumns.TITLE,
        BookmarkColumns.URL,
        BookmarkColumns.DATE,
        BookmarkColumns.BOOKMARK
    };

    private final Object mInitializeUriMatcherLock = new Object();
    private final Object mLoadNativeLock = new Object();
    private UriMatcher mUriMatcher;
    private long mLastModifiedBookmarkFolderId = INVALID_BOOKMARK_ID;
    private long mNativeChromeBrowserProvider;
    private BookmarkNode mMobileBookmarksFolder;

    /**
     * Records whether we've received a call to one of the public ContentProvider APIs.
     */
    protected boolean mContentProviderApiCalled;

    private void ensureUriMatcherInitialized() {
        synchronized (mInitializeUriMatcherLock) {
            if (mUriMatcher != null) return;

            mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
            // The internal URIs
            String authority = getContext().getPackageName() + AUTHORITY_SUFFIX;
            mUriMatcher.addURI(authority, BOOKMARKS_PATH, URI_MATCH_BOOKMARKS);
            mUriMatcher.addURI(authority, BOOKMARKS_PATH + "/#", URI_MATCH_BOOKMARKS_ID);
            // The internal authority for public APIs
            String apiAuthority = getContext().getPackageName() + API_AUTHORITY_SUFFIX;
            mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK);
            mUriMatcher.addURI(apiAuthority, BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID);
            mUriMatcher.addURI(apiAuthority, SEARCHES_PATH, URL_MATCH_API_SEARCHES);
            mUriMatcher.addURI(apiAuthority, SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID);
            mUriMatcher.addURI(apiAuthority, HISTORY_PATH, URL_MATCH_API_HISTORY_CONTENT);
            mUriMatcher.addURI(apiAuthority, HISTORY_PATH + "/#", URL_MATCH_API_HISTORY_CONTENT_ID);
            mUriMatcher.addURI(apiAuthority, COMBINED_PATH, URL_MATCH_API_BOOKMARK);
            mUriMatcher.addURI(apiAuthority, COMBINED_PATH + "/#", URL_MATCH_API_BOOKMARK_ID);
            // The internal authority for BrowserContracts
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH,
                               URL_MATCH_API_HISTORY_CONTENT);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, HISTORY_PATH + "/#",
                               URL_MATCH_API_HISTORY_CONTENT_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH,
                               URL_MATCH_API_BOOKMARK);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, COMBINED_PATH + "/#",
                               URL_MATCH_API_BOOKMARK_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH,
                               URL_MATCH_API_SEARCHES);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, SEARCHES_PATH + "/#",
                               URL_MATCH_API_SEARCHES_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH,
                               URL_MATCH_API_BOOKMARK_CONTENT);
            mUriMatcher.addURI(BROWSER_CONTRACT_API_AUTHORITY, BOOKMARKS_PATH + "/#",
                               URL_MATCH_API_BOOKMARK_CONTENT_ID);
            // Added the Android Framework URIs, so the provider can easily switched
            // by adding 'browser' and 'com.android.browser' in manifest.
            // The Android's BrowserContract
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH,
                               URL_MATCH_API_HISTORY_CONTENT);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, HISTORY_PATH + "/#",
                               URL_MATCH_API_HISTORY_CONTENT_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined", URL_MATCH_API_BOOKMARK);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, "combined/#", URL_MATCH_API_BOOKMARK_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH, URL_MATCH_API_SEARCHES);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, SEARCHES_PATH + "/#",
                               URL_MATCH_API_SEARCHES_ID);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH,
                               URL_MATCH_API_BOOKMARK_CONTENT);
            mUriMatcher.addURI(BROWSER_CONTRACT_AUTHORITY, BOOKMARKS_PATH + "/#",
                               URL_MATCH_API_BOOKMARK_CONTENT_ID);
            // For supporting android.provider.browser.BookmarkColumns and
            // SearchColumns
            mUriMatcher.addURI("browser", BOOKMARKS_PATH, URL_MATCH_API_BOOKMARK);
            mUriMatcher.addURI("browser", BOOKMARKS_PATH + "/#", URL_MATCH_API_BOOKMARK_ID);
            mUriMatcher.addURI("browser", SEARCHES_PATH, URL_MATCH_API_SEARCHES);
            mUriMatcher.addURI("browser", SEARCHES_PATH + "/#", URL_MATCH_API_SEARCHES_ID);

            mUriMatcher.addURI(apiAuthority,
                               BOOKMARKS_PATH + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
                               URL_MATCH_BOOKMARK_SUGGESTIONS_ID);
            mUriMatcher.addURI(apiAuthority,
                               SearchManager.SUGGEST_URI_PATH_QUERY,
                               URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID);
        }
    }

    @Override
    public boolean onCreate() {
        // Pre-load shared preferences object, this happens on a separate thread
        PreferenceManager.getDefaultSharedPreferences(getContext());
        return true;
    }

    /**
     * Lazily fetches the last modified bookmark folder id.
     */
    private long getLastModifiedBookmarkFolderId() {
        if (mLastModifiedBookmarkFolderId == INVALID_BOOKMARK_ID) {
            SharedPreferences sharedPreferences =
                    PreferenceManager.getDefaultSharedPreferences(getContext());
            mLastModifiedBookmarkFolderId = sharedPreferences.getLong(
                    LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, INVALID_BOOKMARK_ID);
        }
        return mLastModifiedBookmarkFolderId;
    }

    private String buildSuggestWhere(String selection, int argc) {
        StringBuilder sb = new StringBuilder(selection);
        for (int i = 0; i < argc - 1; i++) {
            sb.append(" OR ");
            sb.append(selection);
        }
        return sb.toString();
    }

    private String getReadWritePermissionNameForBookmarkFolders() {
        return getContext().getApplicationContext().getPackageName() + ".permission."
                + PERMISSION_READ_WRITE_BOOKMARKS;
    }

    private Cursor getBookmarkHistorySuggestions(String selection, String[] selectionArgs,
            String sortOrder, boolean excludeHistory) {
        boolean matchTitles = false;
        Vector<String> args = new Vector<String>();
        String like = selectionArgs[0] + "%";
        if (selectionArgs[0].startsWith("http") || selectionArgs[0].startsWith("file")) {
            args.add(like);
        } else {
            // Match against common URL prefixes.
            args.add("http://" + like);
            args.add("https://" + like);
            args.add("http://www." + like);
            args.add("https://www." + like);
            args.add("file://" + like);
            matchTitles = true;
        }

        StringBuilder urlWhere = new StringBuilder("(");
        urlWhere.append(buildSuggestWhere(selection, args.size()));
        if (matchTitles) {
            args.add(like);
            urlWhere.append(" OR title LIKE ?");
        }
        urlWhere.append(")");

        if (excludeHistory) {
            urlWhere.append(" AND bookmark=?");
            args.add("1");
        }

        selectionArgs = args.toArray(selectionArgs);
        Cursor cursor = queryBookmarkFromAPI(SUGGEST_PROJECTION, urlWhere.toString(),
                selectionArgs, sortOrder);
        return new ChromeBrowserProviderSuggestionsCursor(cursor);
    }

    /**
     * @see android.content.ContentUris#parseId(Uri)
     * @return The id from a content URI or -1 if the URI has no id or is malformed.
     */
    private static long getContentUriId(Uri uri) {
        try {
            return ContentUris.parseId(uri);
        } catch (UnsupportedOperationException e) {
            return -1;
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        if (!canHandleContentProviderApiCall()) return null;

        // Check for invalid id values if provided.
        long bookmarkId = getContentUriId(uri);
        if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return null;

        int match = mUriMatcher.match(uri);
        Cursor cursor = null;
        switch (match) {
            case URL_MATCH_BOOKMARK_SUGGESTIONS_ID:
                cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, true);
                break;
            case URL_MATCH_BOOKMARK_HISTORY_SUGGESTIONS_ID:
                cursor = getBookmarkHistorySuggestions(selection, selectionArgs, sortOrder, false);
                break;
            case URL_MATCH_API_BOOKMARK:
                cursor = queryBookmarkFromAPI(projection, selection, selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_BOOKMARK_ID:
                cursor = queryBookmarkFromAPI(projection, buildWhereClause(bookmarkId, selection),
                        selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_SEARCHES:
                cursor = querySearchTermFromAPI(projection, selection, selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_SEARCHES_ID:
                cursor = querySearchTermFromAPI(projection, buildWhereClause(bookmarkId, selection),
                        selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_HISTORY_CONTENT:
                cursor = queryBookmarkFromAPI(projection, buildHistoryWhereClause(selection),
                        selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_HISTORY_CONTENT_ID:
                cursor = queryBookmarkFromAPI(projection,
                        buildHistoryWhereClause(bookmarkId, selection), selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT:
                cursor = queryBookmarkFromAPI(projection, buildBookmarkWhereClause(selection),
                        selectionArgs, sortOrder);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT_ID:
                cursor = queryBookmarkFromAPI(projection,
                        buildBookmarkWhereClause(bookmarkId, selection), selectionArgs, sortOrder);
                break;
            default:
                throw new IllegalArgumentException(TAG + ": query - unknown URL uri = " + uri);
        }
        if (cursor == null) {
            cursor = new MatrixCursor(new String[] { });
        }
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (!canHandleContentProviderApiCall()) return null;

        int match = mUriMatcher.match(uri);
        Uri res = null;
        long id;
        switch (match) {
            case URI_MATCH_BOOKMARKS:
                id = addBookmark(values);
                if (id == INVALID_BOOKMARK_ID) return null;
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT:
                values.put(BookmarkColumns.BOOKMARK, 1);
                //$FALL-THROUGH$
            case URL_MATCH_API_BOOKMARK:
            case URL_MATCH_API_HISTORY_CONTENT:
                id = addBookmarkFromAPI(values);
                if (id == INVALID_CONTENT_PROVIDER_ID) return null;
                break;
            case URL_MATCH_API_SEARCHES:
                id = addSearchTermFromAPI(values);
                if (id == INVALID_CONTENT_PROVIDER_ID) return null;
                break;
            default:
                throw new IllegalArgumentException(TAG + ": insert - unknown URL " + uri);
        }

        res = ContentUris.withAppendedId(uri, id);
        notifyChange(res);
        return res;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (!canHandleContentProviderApiCall()) return 0;

        // Check for invalid id values if provided.
        long bookmarkId = getContentUriId(uri);
        if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0;

        int match = mUriMatcher.match(uri);
        int result;
        switch (match) {
            case URI_MATCH_BOOKMARKS_ID :
                result = nativeRemoveBookmark(mNativeChromeBrowserProvider, bookmarkId);
                break;
            case URL_MATCH_API_BOOKMARK_ID:
                result = removeBookmarkFromAPI(
                        buildWhereClause(bookmarkId, selection), selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK:
                result = removeBookmarkFromAPI(selection, selectionArgs);
                break;
            case URL_MATCH_API_SEARCHES_ID:
                result = removeSearchFromAPI(buildWhereClause(bookmarkId, selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_SEARCHES:
                result = removeSearchFromAPI(selection, selectionArgs);
                break;
            case URL_MATCH_API_HISTORY_CONTENT:
                result = removeHistoryFromAPI(selection, selectionArgs);
                break;
            case URL_MATCH_API_HISTORY_CONTENT_ID:
                result = removeHistoryFromAPI(buildWhereClause(bookmarkId, selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT:
                result = removeBookmarkFromAPI(buildBookmarkWhereClause(selection), selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT_ID:
                result = removeBookmarkFromAPI(buildBookmarkWhereClause(bookmarkId, selection),
                        selectionArgs);
                break;
            default:
                throw new IllegalArgumentException(TAG + ": delete - unknown URL " + uri);
        }
        if (result != 0) notifyChange(uri);
        return result;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        if (!canHandleContentProviderApiCall()) return 0;

        // Check for invalid id values if provided.
        long bookmarkId = getContentUriId(uri);
        if (bookmarkId == INVALID_CONTENT_PROVIDER_ID) return 0;

        int match = mUriMatcher.match(uri);
        int result;
        switch (match) {
            case URI_MATCH_BOOKMARKS_ID:
                String url = null;
                if (values.containsKey(Browser.BookmarkColumns.URL)) {
                    url = values.getAsString(Browser.BookmarkColumns.URL);
                }
                String title = values.getAsString(Browser.BookmarkColumns.TITLE);
                long parentId = INVALID_BOOKMARK_ID;
                if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) {
                    parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM);
                }
                result = nativeUpdateBookmark(mNativeChromeBrowserProvider, bookmarkId, url, title,
                        parentId);
                updateLastModifiedBookmarkFolder(parentId);
                break;
            case URL_MATCH_API_BOOKMARK_ID:
                result = updateBookmarkFromAPI(values, buildWhereClause(bookmarkId, selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK:
                result = updateBookmarkFromAPI(values, selection, selectionArgs);
                break;
            case URL_MATCH_API_SEARCHES_ID:
                result = updateSearchTermFromAPI(values, buildWhereClause(bookmarkId, selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_SEARCHES:
                result = updateSearchTermFromAPI(values, selection, selectionArgs);
                break;
            case URL_MATCH_API_HISTORY_CONTENT:
                result = updateBookmarkFromAPI(values, buildHistoryWhereClause(selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_HISTORY_CONTENT_ID:
                result = updateBookmarkFromAPI(values,
                        buildHistoryWhereClause(bookmarkId, selection), selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT:
                result = updateBookmarkFromAPI(values, buildBookmarkWhereClause(selection),
                        selectionArgs);
                break;
            case URL_MATCH_API_BOOKMARK_CONTENT_ID:
                result = updateBookmarkFromAPI(values,
                        buildBookmarkWhereClause(bookmarkId, selection), selectionArgs);
                break;
            default:
                throw new IllegalArgumentException(TAG + ": update - unknown URL " + uri);
        }
        if (result != 0) notifyChange(uri);
        return result;
    }

    @Override
    public String getType(Uri uri) {
        ensureUriMatcherInitialized();
        int match = mUriMatcher.match(uri);
        switch (match) {
            case URI_MATCH_BOOKMARKS:
            case URL_MATCH_API_BOOKMARK:
                return "vnd.android.cursor.dir/bookmark";
            case URI_MATCH_BOOKMARKS_ID:
            case URL_MATCH_API_BOOKMARK_ID:
                return "vnd.android.cursor.item/bookmark";
            case URL_MATCH_API_SEARCHES:
                return "vnd.android.cursor.dir/searches";
            case URL_MATCH_API_SEARCHES_ID:
                return "vnd.android.cursor.item/searches";
            case URL_MATCH_API_HISTORY_CONTENT:
                return BROWSER_CONTRACT_HISTORY_CONTENT_TYPE;
            case URL_MATCH_API_HISTORY_CONTENT_ID:
                return BROWSER_CONTRACT_HISTORY_CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException(TAG + ": getType - unknown URL " + uri);
        }
    }

    private long addBookmark(ContentValues values) {
        String url = values.getAsString(Browser.BookmarkColumns.URL);
        String title = values.getAsString(Browser.BookmarkColumns.TITLE);
        boolean isFolder = false;
        if (values.containsKey(BOOKMARK_IS_FOLDER_PARAM)) {
            isFolder = values.getAsBoolean(BOOKMARK_IS_FOLDER_PARAM);
        }
        long parentId = INVALID_BOOKMARK_ID;
        if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) {
            parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM);
        }
        long id = nativeAddBookmark(mNativeChromeBrowserProvider, url, title, isFolder, parentId);
        if (id == INVALID_BOOKMARK_ID) return id;

        if (isFolder) {
            updateLastModifiedBookmarkFolder(id);
        } else {
            updateLastModifiedBookmarkFolder(parentId);
        }
        return id;
    }

    private void updateLastModifiedBookmarkFolder(long id) {
        if (getLastModifiedBookmarkFolderId() == id) return;

        mLastModifiedBookmarkFolderId = id;
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getContext());
        sharedPreferences.edit()
                .putLong(LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, mLastModifiedBookmarkFolderId)
                .apply();
    }

    public static String getApiAuthority(Context context) {
        return context.getPackageName() + API_AUTHORITY_SUFFIX;
    }

    public static String getInternalAuthority(Context context) {
        return context.getPackageName() + AUTHORITY_SUFFIX;
    }

    public static Uri getBookmarksUri(Context context) {
        return buildContentUri(getInternalAuthority(context), BOOKMARKS_PATH);
    }

    public static Uri getBookmarkFolderUri(Context context) {
        return buildContentUri(getInternalAuthority(context), BOOKMARK_FOLDER_PATH);
    }

    public static Uri getBookmarksApiUri(Context context) {
        return buildContentUri(getApiAuthority(context), BOOKMARKS_PATH);
    }

    public static Uri getSearchesApiUri(Context context) {
        return buildContentUri(getApiAuthority(context), SEARCHES_PATH);
    }

    private boolean bookmarkNodeExists(long nodeId) {
        if (nodeId < 0) return false;
        return nativeBookmarkNodeExists(mNativeChromeBrowserProvider, nodeId);
    }

    private long createBookmarksFolderOnce(String title, long parentId) {
        return nativeCreateBookmarksFolderOnce(mNativeChromeBrowserProvider, title, parentId);
    }

    private BookmarkNode getBookmarkFolderHierarchy() {
        return nativeGetAllBookmarkFolders(mNativeChromeBrowserProvider);
    }

    protected BookmarkNode getBookmarkNode(long nodeId, boolean getParent, boolean getChildren,
            boolean getFavicons, boolean getThumbnails) {
        // Don't allow going up the hierarchy if sync is disabled and the requested node
        // is the Mobile Bookmarks folder.
        if (getParent && nodeId == getMobileBookmarksFolderId()
                && !SyncStatusHelper.get(getContext()).isSyncEnabled()) {
            getParent = false;
        }

        BookmarkNode node = nativeGetBookmarkNode(mNativeChromeBrowserProvider, nodeId, getParent,
                getChildren);
        if (!getFavicons && !getThumbnails) return node;

        // Favicons and thumbnails need to be populated separately as they are provided
        // asynchronously by Chromium services other than the bookmark model.
        if (node.parent() != null) populateNodeImages(node.parent(), getFavicons, getThumbnails);
        for (BookmarkNode child : node.children()) {
            populateNodeImages(child, getFavicons, getThumbnails);
        }

        return node;
    }

    private BookmarkNode getDefaultBookmarkFolder() {
        // Try to access the bookmark folder last modified by us. If it doesn't exist anymore
        // then use the synced node (Mobile Bookmarks).
        BookmarkNode lastModified = getBookmarkNode(getLastModifiedBookmarkFolderId(), false, false,
                false, false);
        if (lastModified == null) {
            lastModified = getMobileBookmarksFolder();
            mLastModifiedBookmarkFolderId = lastModified != null ? lastModified.id() :
                    INVALID_BOOKMARK_ID;
        }
        return lastModified;
    }

    private void populateNodeImages(BookmarkNode node, boolean favicon, boolean thumbnail) {
        if (node == null || node.type() != Type.URL) return;

        if (favicon) {
            node.setFavicon(nativeGetFaviconOrTouchIcon(mNativeChromeBrowserProvider, node.url()));
        }

        if (thumbnail) {
            node.setThumbnail(nativeGetThumbnail(mNativeChromeBrowserProvider, node.url()));
        }
    }

    private BookmarkNode getMobileBookmarksFolder() {
        if (mMobileBookmarksFolder == null) {
            mMobileBookmarksFolder = nativeGetMobileBookmarksFolder(mNativeChromeBrowserProvider);
        }
        return mMobileBookmarksFolder;
    }

    protected long getMobileBookmarksFolderId() {
        BookmarkNode mobileBookmarks = getMobileBookmarksFolder();
        return mobileBookmarks != null ? mobileBookmarks.id() : INVALID_BOOKMARK_ID;
    }

    private boolean isBookmarkInMobileBookmarksBranch(long nodeId) {
        if (nodeId <= 0) return false;
        return nativeIsBookmarkInMobileBookmarksBranch(mNativeChromeBrowserProvider, nodeId);
    }

    static String argKey(int i) {
        return "arg" + i;
    }

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        // TODO(shashishekhar): Refactor this code into a separate class.
        // Caller must have the READ_WRITE_BOOKMARK_FOLDERS permission.
        getContext().enforcePermission(getReadWritePermissionNameForBookmarkFolders(),
                                       Binder.getCallingPid(), Binder.getCallingUid(), TAG);
        if (!canHandleContentProviderApiCall()) return null;
        if (method == null || extras == null) return null;

        Bundle result = new Bundle();
        if (CLIENT_API_BOOKMARK_NODE_EXISTS.equals(method)) {
            result.putBoolean(CLIENT_API_RESULT_KEY,
                    bookmarkNodeExists(extras.getLong(argKey(0))));
        } else if (CLIENT_API_CREATE_BOOKMARKS_FOLDER_ONCE.equals(method)) {
            result.putLong(CLIENT_API_RESULT_KEY,
                    createBookmarksFolderOnce(extras.getString(argKey(0)),
                                              extras.getLong(argKey(1))));
        } else if (CLIENT_API_GET_BOOKMARK_FOLDER_HIERARCHY.equals(method)) {
            result.putParcelable(CLIENT_API_RESULT_KEY, getBookmarkFolderHierarchy());
        } else if (CLIENT_API_GET_BOOKMARK_NODE.equals(method)) {
            result.putParcelable(CLIENT_API_RESULT_KEY,
                    getBookmarkNode(extras.getLong(argKey(0)),
                                    extras.getBoolean(argKey(1)),
                                    extras.getBoolean(argKey(2)),
                                    extras.getBoolean(argKey(3)),
                                    extras.getBoolean(argKey(4))));
        } else if (CLIENT_API_GET_DEFAULT_BOOKMARK_FOLDER.equals(method)) {
            result.putParcelable(CLIENT_API_RESULT_KEY, getDefaultBookmarkFolder());
        } else if (method.equals(CLIENT_API_GET_MOBILE_BOOKMARKS_FOLDER_ID)) {
            result.putLong(CLIENT_API_RESULT_KEY, getMobileBookmarksFolderId());
        } else if (CLIENT_API_IS_BOOKMARK_IN_MOBILE_BOOKMARKS_BRANCH.equals(method)) {
            result.putBoolean(CLIENT_API_RESULT_KEY,
                    isBookmarkInMobileBookmarksBranch(extras.getLong(argKey(0))));
        } else if (CLIENT_API_DELETE_ALL_BOOKMARKS.equals(method)) {
            nativeRemoveAllBookmarks(mNativeChromeBrowserProvider);
        } else {
            Log.w(TAG, "Received invalid method " + method);
            return null;
        }

        return result;
    }

    /**
     * Checks whether Chrome is sufficiently initialized to handle a call to the
     * ChromeBrowserProvider.
     */
    private boolean canHandleContentProviderApiCall() {
        mContentProviderApiCalled = true;

        if (isInUiThread()) return false;
        if (!ensureNativeChromeLoaded()) return false;
        return true;
    }

    /**
     * The type of a BookmarkNode.
     */
    public enum Type {
        URL,
        FOLDER,
        BOOKMARK_BAR,
        OTHER_NODE,
        MOBILE
    }

    /**
     * Simple Data Object representing the chrome bookmark node.
     */
    public static class BookmarkNode implements Parcelable {
        private final long mId;
        private final String mName;
        private final String mUrl;
        private final Type mType;
        private final BookmarkNode mParent;
        private final List<BookmarkNode> mChildren = new ArrayList<BookmarkNode>();

        // Favicon and thumbnail optionally set in a 2-step procedure.
        private byte[] mFavicon;
        private byte[] mThumbnail;

        /** Used to pass structured data back from the native code. */
        @VisibleForTesting
        public BookmarkNode(long id, Type type, String name, String url, BookmarkNode parent) {
            mId = id;
            mName = name;
            mUrl = url;
            mType = type;
            mParent = parent;
        }

        /**
         * @return The id of this bookmark entry.
         */
        public long id() {
            return mId;
        }

        /**
         * @return The name of this bookmark entry.
         */
        public String name() {
            return mName;
        }

        /**
         * @return The URL of this bookmark entry.
         */
        public String url() {
            return mUrl;
        }

        /**
         * @return The type of this bookmark entry.
         */
        public Type type() {
            return mType;
        }

        /**
         * @return The bookmark favicon, if any.
         */
        public byte[] favicon() {
            return mFavicon;
        }

        /**
         * @return The bookmark thumbnail, if any.
         */
        public byte[] thumbnail() {
            return mThumbnail;
        }

        /**
         * @return The parent folder of this bookmark entry.
         */
        public BookmarkNode parent() {
            return mParent;
        }

        /**
         * Adds a child to this node.
         *
         * <p>
         * Used solely by the native code.
         */
        @VisibleForTesting
        @CalledByNativeUnchecked("BookmarkNode")
        public void addChild(BookmarkNode child) {
            mChildren.add(child);
        }

        /**
         * @return The child bookmark nodes of this node.
         */
        public List<BookmarkNode> children() {
            return mChildren;
        }

        /**
         * @return Whether this node represents a bookmarked URL or not.
         */
        public boolean isUrl() {
            return mUrl != null;
        }

        /**
         * @return true if the two individual nodes contain the same information.
         * The existence of parent and children nodes is checked, but their contents are not.
         */
        public boolean equalContents(BookmarkNode node) {
            return node != null &&
                    mId == node.mId &&
                    !(mName == null ^ node.mName == null) &&
                    (mName == null || mName.equals(node.mName)) &&
                    !(mUrl == null ^ node.mUrl == null) &&
                    (mUrl == null || mUrl.equals(node.mUrl)) &&
                    mType == node.mType &&
                    byteArrayEqual(mFavicon, node.mFavicon) &&
                    byteArrayEqual(mThumbnail, node.mThumbnail) &&
                    !(mParent == null ^ node.mParent == null) &&
                    children().size() == node.children().size();
        }

        private static boolean byteArrayEqual(byte[] byte1, byte[] byte2) {
            if (byte1 == null && byte2 != null) return byte2.length == 0;
            if (byte2 == null && byte1 != null) return byte1.length == 0;
            return Arrays.equals(byte1, byte2);
        }

        @CalledByNative("BookmarkNode")
        private static BookmarkNode create(
                long id, int type, String name, String url, BookmarkNode parent) {
            return new BookmarkNode(id, Type.values()[type], name, url, parent);
        }

        @VisibleForTesting
        public void setFavicon(byte[] favicon) {
            mFavicon = favicon;
        }

        @VisibleForTesting
        public void setThumbnail(byte[] thumbnail) {
            mThumbnail = thumbnail;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            // Write the current node id.
            dest.writeLong(mId);

            // Serialize the full hierarchy from the root.
            getHierarchyRoot().writeNodeContentsRecursive(dest);
        }

        @VisibleForTesting
        public BookmarkNode getHierarchyRoot() {
            BookmarkNode root = this;
            while (root.parent() != null) {
                root = root.parent();
            }
            return root;
        }

        private void writeNodeContentsRecursive(Parcel dest) {
            writeNodeContents(dest);
            dest.writeInt(mChildren.size());
            for (BookmarkNode child : mChildren) {
                child.writeNodeContentsRecursive(dest);
            }
        }

        private void writeNodeContents(Parcel dest) {
            dest.writeLong(mId);
            dest.writeString(mName);
            dest.writeString(mUrl);
            dest.writeInt(mType.ordinal());
            dest.writeByteArray(mFavicon);
            dest.writeByteArray(mThumbnail);
            dest.writeLong(mParent != null ? mParent.mId : INVALID_BOOKMARK_ID);
        }

        public static final Creator<BookmarkNode> CREATOR = new Creator<BookmarkNode>() {
            private HashMap<Long, BookmarkNode> mNodeMap;

            @Override
            public BookmarkNode createFromParcel(Parcel source) {
                mNodeMap = new HashMap<Long, BookmarkNode>();
                long currentNodeId = source.readLong();
                readNodeContentsRecursive(source);
                BookmarkNode node = getNode(currentNodeId);
                mNodeMap.clear();
                return node;
            }

            @Override
            public BookmarkNode[] newArray(int size) {
                return new BookmarkNode[size];
            }

            private BookmarkNode getNode(long id) {
                if (id == INVALID_BOOKMARK_ID) return null;
                Long nodeId = Long.valueOf(id);
                if (!mNodeMap.containsKey(nodeId)) {
                    Log.e(TAG, "Invalid BookmarkNode hierarchy. Unknown id " + id);
                    return null;
                }
                return mNodeMap.get(nodeId);
            }

            private BookmarkNode readNodeContents(Parcel source) {
                long id = source.readLong();
                String name = source.readString();
                String url = source.readString();
                int type = source.readInt();
                byte[] favicon = source.createByteArray();
                byte[] thumbnail = source.createByteArray();
                long parentId = source.readLong();
                if (type < 0 || type >= Type.values().length) {
                    Log.w(TAG, "Invalid node type ordinal value.");
                    return null;
                }

                BookmarkNode node = new BookmarkNode(id, Type.values()[type], name, url,
                        getNode(parentId));
                node.setFavicon(favicon);
                node.setThumbnail(thumbnail);
                return node;
            }

            private BookmarkNode readNodeContentsRecursive(Parcel source) {
                BookmarkNode node = readNodeContents(source);
                if (node == null) return null;

                Long nodeId = Long.valueOf(node.id());
                if (mNodeMap.containsKey(nodeId)) {
                    Log.e(TAG, "Invalid BookmarkNode hierarchy. Duplicate id " + node.id());
                    return null;
                }
                mNodeMap.put(nodeId, node);

                int numChildren = source.readInt();
                for (int i = 0; i < numChildren; ++i) {
                    node.addChild(readNodeContentsRecursive(source));
                }

                return node;
            }
        };
    }

    private long addBookmarkFromAPI(ContentValues values) {
        BookmarkRow row = BookmarkRow.fromContentValues(values);
        if (row.url == null) {
            throw new IllegalArgumentException("Must have a bookmark URL");
        }
        return nativeAddBookmarkFromAPI(mNativeChromeBrowserProvider,
                row.url, row.created, row.isBookmark, row.date, row.favicon,
                row.title, row.visits, row.parentId);
    }

    private Cursor queryBookmarkFromAPI(String[] projectionIn, String selection,
            String[] selectionArgs, String sortOrder) {
        String[] projection = null;
        if (projectionIn == null || projectionIn.length == 0) {
            projection = BOOKMARK_DEFAULT_PROJECTION;
        } else {
            projection = projectionIn;
        }

        return nativeQueryBookmarkFromAPI(mNativeChromeBrowserProvider, projection, selection,
                selectionArgs, sortOrder);
    }

    private int updateBookmarkFromAPI(ContentValues values, String selection,
            String[] selectionArgs) {
        BookmarkRow row = BookmarkRow.fromContentValues(values);
        return nativeUpdateBookmarkFromAPI(mNativeChromeBrowserProvider,
                row.url, row.created, row.isBookmark, row.date,
                row.favicon, row.title, row.visits, row.parentId, selection, selectionArgs);
    }

    private int removeBookmarkFromAPI(String selection, String[] selectionArgs) {
        return nativeRemoveBookmarkFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs);
    }

    private int removeHistoryFromAPI(String selection, String[] selectionArgs) {
        return nativeRemoveHistoryFromAPI(mNativeChromeBrowserProvider, selection, selectionArgs);
    }

    @CalledByNative
    private void onBookmarkChanged() {
        notifyChange(buildAPIContentUri(getContext(), BOOKMARKS_PATH));
    }

    @CalledByNative
    private void onSearchTermChanged() {
        notifyChange(buildAPIContentUri(getContext(), SEARCHES_PATH));
    }

    private long addSearchTermFromAPI(ContentValues values) {
        SearchRow row = SearchRow.fromContentValues(values);
        if (row.term == null) {
            throw new IllegalArgumentException("Must have a search term");
        }
        return nativeAddSearchTermFromAPI(mNativeChromeBrowserProvider, row.term, row.date);
    }

    private int updateSearchTermFromAPI(ContentValues values, String selection,
            String[] selectionArgs) {
        SearchRow row = SearchRow.fromContentValues(values);
        return nativeUpdateSearchTermFromAPI(mNativeChromeBrowserProvider,
                row.term, row.date, selection, selectionArgs);
    }

    private Cursor querySearchTermFromAPI(String[] projectionIn, String selection,
            String[] selectionArgs, String sortOrder) {
        String[] projection = null;
        if (projectionIn == null || projectionIn.length == 0) {
            projection = android.provider.Browser.SEARCHES_PROJECTION;
        } else {
            projection = projectionIn;
        }
        return nativeQuerySearchTermFromAPI(mNativeChromeBrowserProvider, projection, selection,
                selectionArgs, sortOrder);
    }

    private int removeSearchFromAPI(String selection, String[] selectionArgs) {
        return nativeRemoveSearchTermFromAPI(mNativeChromeBrowserProvider,
                selection, selectionArgs);
    }

    private static boolean isInUiThread() {
        if (!ThreadUtils.runningOnUiThread()) return false;

        if (!"REL".equals(Build.VERSION.CODENAME)) {
            throw new IllegalStateException("Shouldn't run in the UI thread");
        }

        Log.w(TAG, "ChromeBrowserProvider methods cannot be called from the UI thread.");
        return true;
    }

    private static Uri buildContentUri(String authority, String path) {
        return Uri.parse("content://" + authority + "/" + path);
    }

    private static Uri buildAPIContentUri(Context context, String path) {
        return buildContentUri(context.getPackageName() + API_AUTHORITY_SUFFIX, path);
    }

    private static String buildWhereClause(long id, String selection) {
        StringBuffer sb = new StringBuffer();
        sb.append(BaseColumns._ID);
        sb.append(" = ");
        sb.append(id);
        if (!TextUtils.isEmpty(selection)) {
            sb.append(" AND (");
            sb.append(selection);
            sb.append(")");
        }
        return sb.toString();
    }

    private static String buildHistoryWhereClause(long id, String selection) {
        return buildWhereClause(id, buildBookmarkWhereClause(selection, false));
    }

    private static String buildHistoryWhereClause(String selection) {
        return buildBookmarkWhereClause(selection, false);
    }

    /**
     * @return a SQL where class which is inserted the bookmark condition.
     */
    private static String buildBookmarkWhereClause(String selection, boolean isBookmark) {
        StringBuffer sb = new StringBuffer();
        sb.append(BookmarkColumns.BOOKMARK);
        sb.append(isBookmark ? " = 1 " : " = 0");
        if (!TextUtils.isEmpty(selection)) {
            sb.append(" AND (");
            sb.append(selection);
            sb.append(")");
        }
        return sb.toString();
    }

    private static String buildBookmarkWhereClause(long id, String selection) {
        return buildWhereClause(id, buildBookmarkWhereClause(selection, true));
    }

    private static String buildBookmarkWhereClause(String selection) {
        return buildBookmarkWhereClause(selection, true);
    }

    // Wrap the value of BookmarkColumn.
    private static class BookmarkRow {
        Boolean isBookmark;
        Long created;
        String url;
        Long date;
        byte[] favicon;
        String title;
        Integer visits;
        long parentId;

        static BookmarkRow fromContentValues(ContentValues values) {
            BookmarkRow row = new BookmarkRow();
            if (values.containsKey(BookmarkColumns.URL)) {
                row.url = values.getAsString(BookmarkColumns.URL);
            }
            if (values.containsKey(BookmarkColumns.BOOKMARK)) {
                row.isBookmark = values.getAsInteger(BookmarkColumns.BOOKMARK) != 0;
            }
            if (values.containsKey(BookmarkColumns.CREATED)) {
                row.created = values.getAsLong(BookmarkColumns.CREATED);
            }
            if (values.containsKey(BookmarkColumns.DATE)) {
                row.date = values.getAsLong(BookmarkColumns.DATE);
            }
            if (values.containsKey(BookmarkColumns.FAVICON)) {
                row.favicon = values.getAsByteArray(BookmarkColumns.FAVICON);
                // We need to know that the caller set the favicon column.
                if (row.favicon == null) {
                    row.favicon = new byte[0];
                }
            }
            if (values.containsKey(BookmarkColumns.TITLE)) {
                row.title = values.getAsString(BookmarkColumns.TITLE);
            }
            if (values.containsKey(BookmarkColumns.VISITS)) {
                row.visits = values.getAsInteger(BookmarkColumns.VISITS);
            }
            if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) {
                row.parentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM);
            }
            return row;
        }
    }

    // Wrap the value of SearchColumn.
    private static class SearchRow {
        String term;
        Long date;

        static SearchRow fromContentValues(ContentValues values) {
            SearchRow row = new SearchRow();
            if (values.containsKey(SearchColumns.SEARCH)) {
                row.term = values.getAsString(SearchColumns.SEARCH);
            }
            if (values.containsKey(SearchColumns.DATE)) {
                row.date = values.getAsLong(SearchColumns.DATE);
            }
            return row;
        }
    }

    /**
     * Returns true if the native side of the class is initialized.
     */
    protected boolean isNativeSideInitialized() {
        return mNativeChromeBrowserProvider != 0;
    }

    /**
     * Make sure chrome is running. This method mustn't run on UI thread.
     *
     * @return Whether the native chrome process is running successfully once this has returned.
     */
    private boolean ensureNativeChromeLoaded() {
        ensureUriMatcherInitialized();

        synchronized (mLoadNativeLock) {
            if (mNativeChromeBrowserProvider != 0) return true;

            final AtomicBoolean retVal = new AtomicBoolean(true);
            ThreadUtils.runOnUiThreadBlocking(new Runnable() {
                @Override
                public void run() {
                    retVal.set(ensureNativeChromeLoadedOnUIThread());
                }
            });
            return retVal.get();
        }
    }

    /**
     * This method should only run on UI thread.
     */
    protected boolean ensureNativeChromeLoadedOnUIThread() {
        if (isNativeSideInitialized()) return true;
        mNativeChromeBrowserProvider = nativeInit();
        return isNativeSideInitialized();
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            // Tests might try to destroy this in the wrong thread.
            ThreadUtils.runOnUiThreadBlocking(new Runnable() {
                @Override
                public void run() {
                    ensureNativeChromeDestroyedOnUIThread();
                }
            });
        } finally {
            super.finalize();
        }
    }

    /**
     * This method should only run on UI thread.
     */
    private void ensureNativeChromeDestroyedOnUIThread() {
        if (isNativeSideInitialized()) {
            nativeDestroy(mNativeChromeBrowserProvider);
            mNativeChromeBrowserProvider = 0;
        }
    }

    /**
     * Call to get the intent to create a bookmark shortcut on homescreen.
     */
    public static Intent getShortcutToBookmark(String url, String title, Bitmap favicon, int rValue,
            int gValue, int bValue, Context context) {
        return BookmarkUtils.createAddToHomeIntent(
                context, url, title, favicon, rValue, gValue, bValue);
    }

    @SuppressLint("NewApi")
    private void notifyChange(final Uri uri) {
        // If the calling user is different than current one, we need to post a
        // task to notify change, otherwise, a system level hidden permission
        // INTERACT_ACROSS_USERS_FULL is needed.
        // The related APIs were added in API 17, it should be safe to fallback to
        // normal way for notifying change, because caller can't be other users in
        // devices whose API level is less than API 17.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            UserHandle callingUserHandle = Binder.getCallingUserHandle();
            if (callingUserHandle != null &&
                    !callingUserHandle.equals(android.os.Process.myUserHandle())) {
                ThreadUtils.postOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        getContext().getContentResolver().notifyChange(uri, null);
                    }
                });
                return;
            }
        }
        getContext().getContentResolver().notifyChange(uri, null);
    }

    private native long nativeInit();
    private native void nativeDestroy(long nativeChromeBrowserProvider);

    // Public API native methods.
    private native long nativeAddBookmark(long nativeChromeBrowserProvider,
            String url, String title, boolean isFolder, long parentId);

    private native int nativeRemoveBookmark(long nativeChromeBrowserProvider, long id);

    private native int nativeUpdateBookmark(long nativeChromeBrowserProvider,
            long id, String url, String title, long parentId);

    private native long nativeAddBookmarkFromAPI(long nativeChromeBrowserProvider,
            String url, Long created, Boolean isBookmark, Long date, byte[] favicon,
            String title, Integer visits, long parentId);

    private native SQLiteCursor nativeQueryBookmarkFromAPI(long nativeChromeBrowserProvider,
            String[] projection, String selection, String[] selectionArgs, String sortOrder);

    private native int nativeUpdateBookmarkFromAPI(long nativeChromeBrowserProvider,
            String url, Long created, Boolean isBookmark, Long date, byte[] favicon,
            String title, Integer visits, long parentId, String selection, String[] selectionArgs);

    private native int nativeRemoveBookmarkFromAPI(long nativeChromeBrowserProvider,
            String selection, String[] selectionArgs);

    private native int nativeRemoveHistoryFromAPI(long nativeChromeBrowserProvider,
            String selection, String[] selectionArgs);

    private native long nativeAddSearchTermFromAPI(long nativeChromeBrowserProvider,
            String term, Long date);

    private native SQLiteCursor nativeQuerySearchTermFromAPI(long nativeChromeBrowserProvider,
            String[] projection, String selection, String[] selectionArgs, String sortOrder);

    private native int nativeUpdateSearchTermFromAPI(long nativeChromeBrowserProvider,
            String search, Long date, String selection, String[] selectionArgs);

    private native int nativeRemoveSearchTermFromAPI(long nativeChromeBrowserProvider,
            String selection, String[] selectionArgs);

    // Client API native methods.
    private native boolean nativeBookmarkNodeExists(long nativeChromeBrowserProvider, long id);

    private native long nativeCreateBookmarksFolderOnce(long nativeChromeBrowserProvider,
            String title, long parentId);

    private native BookmarkNode nativeGetAllBookmarkFolders(long nativeChromeBrowserProvider);

    private native void nativeRemoveAllBookmarks(long nativeChromeBrowserProvider);

    private native BookmarkNode nativeGetBookmarkNode(long nativeChromeBrowserProvider,
            long id, boolean getParent, boolean getChildren);

    private native BookmarkNode nativeGetMobileBookmarksFolder(long nativeChromeBrowserProvider);

    private native boolean nativeIsBookmarkInMobileBookmarksBranch(long nativeChromeBrowserProvider,
            long id);

    private native byte[] nativeGetFaviconOrTouchIcon(long nativeChromeBrowserProvider, String url);

    private native byte[] nativeGetThumbnail(long nativeChromeBrowserProvider, String url);
}

/* [<][>][^][v][top][bottom][index][help] */