// Copyright 2015 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.partnerbookmarks;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
/**
* Reads bookmarks from the partner content provider (if any).
*/
public class PartnerBookmarksReader {
private static final String TAG = "PartnerBookmarksReader";
private static boolean sInitialized = false;
private static boolean sForceDisableEditing = false;
/** Root bookmark id reserved for the implied root of the bookmarks */
static final long ROOT_FOLDER_ID = 0;
/** ID used to indicate an invalid bookmark node. */
static final long INVALID_BOOKMARK_ID = -1;
// JNI c++ pointer
private long mNativePartnerBookmarksReader = 0;
/** The context (used to get a ContentResolver) */
protected Context mContext;
// TODO(aruslan): Move it out to a separate class that defines
// a partner bookmarks provider contract, see http://b/6399404
/** Object defining a partner bookmark. For this package only. */
static class Bookmark {
// To be provided by the bookmark extractors.
/** Local id of the read bookmark */
long mId;
/** Read id of the parent node */
long mParentId;
/** True if it's folder */
boolean mIsFolder;
/** URL of the bookmark. Required for non-folders. */
String mUrl;
/** Title of the bookmark. */
String mTitle;
/** .PNG Favicon of the bookmark. Optional. Not used for folders. */
byte[] mFavicon;
/** .PNG TouchIcon of the bookmark. Optional. Not used for folders. */
byte[] mTouchicon;
// For auxiliary use while reading.
/** Native id of the C++-processed bookmark */
long mNativeId = INVALID_BOOKMARK_ID;
/** The parent node if any */
Bookmark mParent;
/** Children nodes for the perfect garbage collection disaster */
ArrayList<Bookmark> mEntries = new ArrayList<Bookmark>();
}
/** Closable iterator for available bookmarks. */
protected interface BookmarkIterator extends Iterator<Bookmark> {
public void close();
}
/** Returns an iterator to the available bookmarks. Called by async task. */
protected BookmarkIterator getAvailableBookmarks() {
return PartnerBookmarksProviderIterator.createIfAvailable(
mContext.getContentResolver());
}
/**
* Creates the instance of the reader.
* @param context A Context object.
*/
public PartnerBookmarksReader(Context context) {
mContext = context;
mNativePartnerBookmarksReader = nativeInit();
initializeAndDisableEditingIfNecessary();
}
/**
* Asynchronously read bookmarks from the partner content provider
*/
public void readBookmarks() {
if (mNativePartnerBookmarksReader == 0) {
assert false : "readBookmarks called after nativeDestroy.";
return;
}
new ReadBookmarksTask().execute();
}
/**
* Called when the partner bookmark needs to be pushed.
* @param url The URL.
* @param title The title.
* @param isFolder True if it's a folder.
* @param parentId NATIVE parent folder id.
* @param favicon .PNG blob for icon; used if no touchicon is set.
* @param touchicon .PNG blob for icon.
* @return NATIVE id of a bookmark
*/
private long onBookmarkPush(String url, String title, boolean isFolder, long parentId,
byte[] favicon, byte[] touchicon) {
return nativeAddPartnerBookmark(mNativePartnerBookmarksReader, url, title,
isFolder, parentId, favicon, touchicon);
}
/** Notifies the reader is complete and partner bookmarks should be submitted to the shim. */
protected void onBookmarksRead() {
nativePartnerBookmarksCreationComplete(mNativePartnerBookmarksReader);
nativeDestroy(mNativePartnerBookmarksReader);
mNativePartnerBookmarksReader = 0;
}
/** Handles fetching partner bookmarks in a background thread. */
private class ReadBookmarksTask extends AsyncTask<Void, Void, Void> {
private final Object mRootSync = new Object();
@Override
protected Void doInBackground(Void... params) {
BookmarkIterator bookmarkIterator = getAvailableBookmarks();
if (bookmarkIterator == null) return null;
// Get a snapshot of the bookmarks.
LinkedHashMap<Long, Bookmark> idMap = new LinkedHashMap<Long, Bookmark>();
HashSet<String> urlSet = new HashSet<String>();
Bookmark rootBookmarksFolder = createRootBookmarksFolderBookmark();
idMap.put(ROOT_FOLDER_ID, rootBookmarksFolder);
while (bookmarkIterator.hasNext()) {
Bookmark bookmark = bookmarkIterator.next();
if (bookmark == null) continue;
// Check for duplicate ids.
if (idMap.containsKey(bookmark.mId)) {
Log.i(TAG, "Duplicate bookmark id: "
+ bookmark.mId + ". Dropping bookmark.");
continue;
}
// Check for duplicate URLs.
if (!bookmark.mIsFolder && urlSet.contains(bookmark.mUrl)) {
Log.i(TAG, "More than one bookmark pointing to "
+ bookmark.mUrl
+ ". Keeping only the first one for consistency with Chromium.");
continue;
}
idMap.put(bookmark.mId, bookmark);
urlSet.add(bookmark.mUrl);
}
bookmarkIterator.close();
// Recreate the folder hierarchy and read it.
recreateFolderHierarchy(idMap);
if (rootBookmarksFolder.mEntries.size() == 0) {
Log.e(TAG, "ATTENTION: not using partner bookmarks as none were provided");
return null;
}
if (rootBookmarksFolder.mEntries.size() != 1) {
Log.e(TAG, "ATTENTION: more than one top-level partner bookmarks, ignored");
return null;
}
readBookmarkHierarchy(
rootBookmarksFolder,
new HashSet<PartnerBookmarksReader.Bookmark>());
return null;
}
@Override
protected void onPostExecute(Void v) {
synchronized (mRootSync) {
onBookmarksRead();
}
}
private void recreateFolderHierarchy(LinkedHashMap<Long, Bookmark> idMap) {
for (Bookmark bookmark : idMap.values()) {
if (bookmark.mId == ROOT_FOLDER_ID) continue;
// Look for invalid parent ids and self-cycles.
if (!idMap.containsKey(bookmark.mParentId) || bookmark.mParentId == bookmark.mId) {
bookmark.mParent = idMap.get(ROOT_FOLDER_ID);
bookmark.mParent.mEntries.add(bookmark);
continue;
}
bookmark.mParent = idMap.get(bookmark.mParentId);
bookmark.mParent.mEntries.add(bookmark);
}
}
private Bookmark createRootBookmarksFolderBookmark() {
Bookmark root = new Bookmark();
root.mId = ROOT_FOLDER_ID;
root.mTitle = "[IMPLIED_ROOT]";
root.mNativeId = INVALID_BOOKMARK_ID;
root.mParentId = ROOT_FOLDER_ID;
root.mIsFolder = true;
return root;
}
private void readBookmarkHierarchy(
Bookmark bookmark, HashSet<Bookmark> processedNodes) {
// Avoid cycles in the hierarchy that could lead to infinite loops.
if (processedNodes.contains(bookmark)) return;
processedNodes.add(bookmark);
if (bookmark.mId != ROOT_FOLDER_ID) {
try {
synchronized (mRootSync) {
bookmark.mNativeId =
onBookmarkPush(
bookmark.mUrl, bookmark.mTitle,
bookmark.mIsFolder, bookmark.mParentId,
bookmark.mFavicon, bookmark.mTouchicon);
}
} catch (IllegalArgumentException e) {
Log.w(TAG, "Error inserting bookmark " + bookmark.mTitle, e);
}
if (bookmark.mNativeId == INVALID_BOOKMARK_ID) {
Log.e(TAG, "Error creating bookmark '" + bookmark.mTitle + "'.");
return;
}
}
if (bookmark.mIsFolder) {
for (Bookmark entry : bookmark.mEntries) {
if (entry.mParent != bookmark) {
Log.w(TAG, "Hierarchy error in bookmark '"
+ bookmark.mTitle + "'. Skipping.");
continue;
}
entry.mParentId = bookmark.mNativeId;
readBookmarkHierarchy(entry, processedNodes);
}
}
}
}
/**
* Disables partner bookmarks editing.
*/
public static void disablePartnerBookmarksEditing() {
sForceDisableEditing = true;
if (sInitialized) nativeDisablePartnerBookmarksEditing();
}
private static void initializeAndDisableEditingIfNecessary() {
sInitialized = true;
if (sForceDisableEditing) disablePartnerBookmarksEditing();
}
// JNI
private native long nativeInit();
private native void nativeReset(long nativePartnerBookmarksReader);
private native void nativeDestroy(long nativePartnerBookmarksReader);
private native long nativeAddPartnerBookmark(long nativePartnerBookmarksReader,
String url, String title, boolean isFolder, long parentId,
byte[] favicon, byte[] touchicon);
private native void nativePartnerBookmarksCreationComplete(long nativePartnerBookmarksReader);
private static native void nativeDisablePartnerBookmarksEditing();
}