blob: e5e033130dd2706a136a2f018044ce5fd070d840 [file] [log] [blame]
// 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.provider;
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.content.pm.PackageManager;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.provider.BaseColumns;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import android.util.Log;
import android.util.LongSparseArray;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.database.SQLiteCursor;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 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";
/**
* A projection of {@link #SEARCHES_URI} that contains {@link SearchColumns#ID},
* {@link SearchColumns#SEARCH}, and {@link SearchColumns#DATE}.
*/
@VisibleForTesting
public static final String[] SEARCHES_PROJECTION = new String[] {
// if you change column order you must also change indices below
SearchColumns.ID, // 0
SearchColumns.SEARCH, // 1
SearchColumns.DATE, // 2
};
/* these indices dependent on SEARCHES_PROJECTION */
@VisibleForTesting
public static final int SEARCHES_PROJECTION_SEARCH_INDEX = 1;
@VisibleForTesting
public static final int SEARCHES_PROJECTION_DATE_INDEX = 2;
// 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 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";
private static final String BROWSER_CONTRACT_BOOKMARK_CONTENT_TYPE =
"vnd.android.cursor.dir/bookmark";
private static final String BROWSER_CONTRACT_BOOKMARK_CONTENT_ITEM_TYPE =
"vnd.android.cursor.item/bookmark";
private static final String BROWSER_CONTRACT_SEARCH_CONTENT_TYPE =
"vnd.android.cursor.dir/searches";
private static final String BROWSER_CONTRACT_SEARCH_CONTENT_ITEM_TYPE =
"vnd.android.cursor.item/searches";
// 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";
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.
private 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
};
// These must be kept in sync with internal histograms.xml
private static final String READ_HISTORY_BOOKMARKS_PERMISSION = "READ_HISTORY_BOOKMARKS";
private static final String WRITE_HISTORY_BOOKMARKS_PERMISSION = "WRITE_HISTORY_BOOKMARKS";
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 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() {
// Work around for broken Android versions that break the Android contract and initialize
// ContentProviders on non-UI threads. crbug.com/705442
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.addStartupCompletedObserver(
new BrowserStartupController.StartupCallback() {
@Override
public void onSuccess() {
ensureNativeSideInitialized();
}
@Override
public void onFailure() {
}
});
}
});
return true;
}
/**
* Lazily fetches the last modified bookmark folder id.
*/
private long getLastModifiedBookmarkFolderId() {
if (mLastModifiedBookmarkFolderId == INVALID_BOOKMARK_ID) {
SharedPreferences sharedPreferences =
ContextUtils.getAppSharedPreferences();
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;
List<String> args = new ArrayList<String>();
String like = selectionArgs[0] + "%";
if (selectionArgs[0].startsWith(UrlConstants.HTTP_SCHEME)
|| selectionArgs[0].startsWith(UrlConstants.FILE_SCHEME)) {
args.add(like);
} else {
// Match against common URL prefixes.
args.add(UrlConstants.HTTP_URL_PREFIX + like);
args.add(UrlConstants.HTTPS_URL_PREFIX + like);
args.add(UrlConstants.HTTP_URL_PREFIX + "www." + like);
args.add(UrlConstants.HTTPS_URL_PREFIX + "www." + like);
args.add(UrlConstants.FILE_URL_PREFIX + 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;
// Starting with M, other apps are no longer allowed to access bookmarks. But returning null
// might break old apps, so return an empty Cursor instead.
if (!hasReadAccess()) return new MatrixCursor(BOOKMARK_DEFAULT_PROJECTION, 0);
// 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() || !hasWriteAccess()) 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() || !hasWriteAccess()) 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() || !hasWriteAccess()) 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(BookmarkColumns.URL)) {
url = values.getAsString(BookmarkColumns.URL);
}
String title = values.getAsString(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 BROWSER_CONTRACT_BOOKMARK_CONTENT_TYPE;
case URI_MATCH_BOOKMARKS_ID:
case URL_MATCH_API_BOOKMARK_ID:
return BROWSER_CONTRACT_BOOKMARK_CONTENT_ITEM_TYPE;
case URL_MATCH_API_SEARCHES:
return BROWSER_CONTRACT_SEARCH_CONTENT_TYPE;
case URL_MATCH_API_SEARCHES_ID:
return BROWSER_CONTRACT_SEARCH_CONTENT_ITEM_TYPE;
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(BookmarkColumns.URL);
String title = values.getAsString(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 =
ContextUtils.getAppSharedPreferences();
sharedPreferences.edit()
.putLong(LAST_MODIFIED_BOOKMARK_FOLDER_ID_KEY, mLastModifiedBookmarkFolderId)
.apply();
}
@VisibleForTesting
public static String getApiAuthority(Context context) {
return context.getPackageName() + API_AUTHORITY_SUFFIX;
}
@VisibleForTesting
public static Uri getBookmarksApiUri(Context context) {
return buildContentUri(getApiAuthority(context), BOOKMARKS_PATH);
}
@VisibleForTesting
public static Uri getSearchesApiUri(Context context) {
return buildContentUri(getApiAuthority(context), SEARCHES_PATH);
}
/**
* Checks whether Chrome is sufficiently initialized to handle a call to the
* ChromeBrowserProvider.
*/
private boolean canHandleContentProviderApiCall() {
if (isInUiThread()) return false;
ensureUriMatcherInitialized();
if (mNativeChromeBrowserProvider != 0) return true;
synchronized (mLoadNativeLock) {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
if (mNativeChromeBrowserProvider != 0) return;
try {
ChromeBrowserInitializer.getInstance(getContext())
.handleSynchronousStartup();
} catch (ProcessInitException e) {
// Chrome browser runs in the background, so exit silently; but do exit,
// since otherwise the next attempt to use Chrome will find a broken JNI.
System.exit(-1);
}
ensureNativeSideInitialized();
}
});
}
return true;
}
/**
* @return Whether the caller has read access to history and bookmarks information.
*/
private boolean hasReadAccess() {
return hasPermission(READ_HISTORY_BOOKMARKS_PERMISSION);
}
/**
* @return Whether the caller has write access to history and bookmarks information.
*/
private boolean hasWriteAccess() {
return hasPermission(WRITE_HISTORY_BOOKMARKS_PERMISSION);
}
/**
* The type of a BookmarkNode.
*/
@IntDef({Type.URL, Type.FOLDER, Type.BOOKMARK_BAR, Type.OTHER_NODE, Type.MOBILE})
@Retention(RetentionPolicy.SOURCE)
public @interface Type {
// Values should be numerated from 0 and can't have gaps.
int URL = 0;
int FOLDER = 1;
int BOOKMARK_BAR = 2;
int OTHER_NODE = 3;
int MOBILE = 4;
int NUM_ENTRIES = 5;
}
/**
* 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 int 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 int 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 int 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
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.
*/
@VisibleForTesting
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);
}
@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);
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 LongSparseArray<BookmarkNode> mNodeMap;
@Override
public BookmarkNode createFromParcel(Parcel source) {
mNodeMap = new LongSparseArray<>();
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.indexOfKey(nodeId) < 0) {
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.NUM_ENTRIES) {
Log.w(TAG, "Invalid node type ordinal value.");
return null;
}
BookmarkNode node = new BookmarkNode(id, 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.indexOfKey(nodeId) >= 0) {
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 = 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) {
StringBuilder sb = new StringBuilder();
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) {
StringBuilder sb = new StringBuilder();
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;
}
}
/**
* Initialize native side if it hasn't been already initialized.
* This is called from BrowserStartupCallback during normal startup except when called
* through one of the public ContentProvider APIs.
*/
private void ensureNativeSideInitialized() {
ThreadUtils.assertOnUiThread();
if (mNativeChromeBrowserProvider == 0) mNativeChromeBrowserProvider = nativeInit();
}
@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())) {
PostTask.postTask(UiThreadTaskTraits.DEFAULT, new Runnable() {
@Override
public void run() {
getContext().getContentResolver().notifyChange(uri, null);
}
});
return;
}
}
getContext().getContentResolver().notifyChange(uri, null);
}
private boolean hasPermission(String permission) {
boolean isSystemOrGoogleCaller = ExternalAuthUtils.getInstance().isCallerValid(
getContext(), ExternalAuthUtils.FLAG_SHOULD_BE_GOOGLE_SIGNED
| ExternalAuthUtils.FLAG_SHOULD_BE_SYSTEM);
if (isSystemOrGoogleCaller) {
recordPermissionWasGranted("SignaturePassed", permission);
return true;
}
boolean hasPermission = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
hasPermission = getContext().checkCallingOrSelfPermission(
getReadWritePermissionNameForBookmarkFolders())
== PackageManager.PERMISSION_GRANTED;
} else {
final String fullyQualifiedPermission = "com.android.browser.permission." + permission;
hasPermission = getContext().checkCallingOrSelfPermission(fullyQualifiedPermission)
== PackageManager.PERMISSION_GRANTED;
}
if (hasPermission) {
recordPermissionWasGranted("CallerHasPermission", permission);
}
return hasPermission;
}
private void recordPermissionWasGranted(String permissionCheckType, String permission) {
int callingUid = Binder.getCallingUid();
PackageManager pm = getContext().getPackageManager();
String[] packages = pm.getPackagesForUid(callingUid);
if (packages.length == 0) return;
@IntentHandler.ExternalAppId
int externalId = IntentHandler.mapPackageToExternalAppId(packages[0]);
RecordHistogram.recordEnumeratedHistogram(
"Android.ChromeBrowserProvider." + permissionCheckType + "." + permission,
externalId, IntentHandler.ExternalAppId.NUM_ENTRIES);
}
private native long nativeInit();
// 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);
}