// 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.SharedPreferences; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; 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 org.chromium.base.CalledByNative; import org.chromium.base.CalledByNativeUnchecked; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; 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_EDITABLE_BOOKMARK_FOLDER_HIERARCHY = "GET_EDITABLE_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_USER_BOOKMARKS = "DELETE_ALL_USER_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 args = new Vector(); 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 getEditableBookmarkFolderHierarchy() { return nativeGetEditableBookmarkFolders(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.isUrl()) { 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_EDITABLE_BOOKMARK_FOLDER_HIERARCHY.equals(method)) { result.putParcelable(CLIENT_API_RESULT_KEY, getEditableBookmarkFolderHierarchy()); } 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_USER_BOOKMARKS.equals(method)) { nativeRemoveAllUserBookmarks(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 mChildren = new ArrayList(); // 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. * *

* 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 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 CREATOR = new Creator() { private HashMap mNodeMap; @Override public BookmarkNode createFromParcel(Parcel source) { mNodeMap = new HashMap(); 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.mUrl == null) { throw new IllegalArgumentException("Must have a bookmark URL"); } return nativeAddBookmarkFromAPI(mNativeChromeBrowserProvider, row.mUrl, row.mCreated, row.mIsBookmark, row.mDate, row.mFavicon, row.mTitle, row.mVisits, row.mParentId); } 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.mUrl, row.mCreated, row.mIsBookmark, row.mDate, row.mFavicon, row.mTitle, row.mVisits, row.mParentId, 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 onHistoryChanged() { notifyChange(buildAPIContentUri(getContext(), HISTORY_PATH)); } @CalledByNative private void onSearchTermChanged() { notifyChange(buildAPIContentUri(getContext(), SEARCHES_PATH)); } private long addSearchTermFromAPI(ContentValues values) { SearchRow row = SearchRow.fromContentValues(values); if (row.mTerm == null) { throw new IllegalArgumentException("Must have a search term"); } return nativeAddSearchTermFromAPI(mNativeChromeBrowserProvider, row.mTerm, row.mDate); } private int updateSearchTermFromAPI(ContentValues values, String selection, String[] selectionArgs) { SearchRow row = SearchRow.fromContentValues(values); return nativeUpdateSearchTermFromAPI(mNativeChromeBrowserProvider, row.mTerm, row.mDate, 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 mIsBookmark; Long mCreated; String mUrl; Long mDate; byte[] mFavicon; String mTitle; Integer mVisits; long mParentId; static BookmarkRow fromContentValues(ContentValues values) { BookmarkRow row = new BookmarkRow(); if (values.containsKey(BookmarkColumns.URL)) { row.mUrl = values.getAsString(BookmarkColumns.URL); } if (values.containsKey(BookmarkColumns.BOOKMARK)) { row.mIsBookmark = values.getAsInteger(BookmarkColumns.BOOKMARK) != 0; } if (values.containsKey(BookmarkColumns.CREATED)) { row.mCreated = values.getAsLong(BookmarkColumns.CREATED); } if (values.containsKey(BookmarkColumns.DATE)) { row.mDate = values.getAsLong(BookmarkColumns.DATE); } if (values.containsKey(BookmarkColumns.FAVICON)) { row.mFavicon = values.getAsByteArray(BookmarkColumns.FAVICON); // We need to know that the caller set the favicon column. if (row.mFavicon == null) { row.mFavicon = new byte[0]; } } if (values.containsKey(BookmarkColumns.TITLE)) { row.mTitle = values.getAsString(BookmarkColumns.TITLE); } if (values.containsKey(BookmarkColumns.VISITS)) { row.mVisits = values.getAsInteger(BookmarkColumns.VISITS); } if (values.containsKey(BOOKMARK_PARENT_ID_PARAM)) { row.mParentId = values.getAsLong(BOOKMARK_PARENT_ID_PARAM); } return row; } } // Wrap the value of SearchColumn. private static class SearchRow { String mTerm; Long mDate; static SearchRow fromContentValues(ContentValues values) { SearchRow row = new SearchRow(); if (values.containsKey(SearchColumns.SEARCH)) { row.mTerm = values.getAsString(SearchColumns.SEARCH); } if (values.containsKey(SearchColumns.DATE)) { row.mDate = 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; } } @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 nativeGetEditableBookmarkFolders(long nativeChromeBrowserProvider); private native void nativeRemoveAllUserBookmarks(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); }