Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1339)

Unified Diff: chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkimport/BookmarkImporter.java

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkimport/BookmarkImporter.java
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkimport/BookmarkImporter.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkimport/BookmarkImporter.java
new file mode 100644
index 0000000000000000000000000000000000000000..cc851130921fdb6b0e67fa2b43ccd16bbf80c2f0
--- /dev/null
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/bookmarkimport/BookmarkImporter.java
@@ -0,0 +1,364 @@
+// 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.bookmarkimport;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.Browser;
+import android.provider.Browser.BookmarkColumns;
+import android.util.Log;
+
+import org.chromium.chrome.browser.ChromeBrowserProvider;
+import org.chromium.chrome.browser.ChromeBrowserProviderClient;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+
+/**
+ * Imports bookmarks from another browser into Chrome.
+ */
+public abstract class BookmarkImporter {
+ private static final String TAG = "BookmarkImporter";
+
+ /** Class containing the results of a bookmark import operation */
+ public static class ImportResults {
+ public int newBookmarks; // Number of new bookmarks that could be imported.
+ public int numImported; // Number of bookmarks that were successfully imported.
+ public long rootFolderId; // ID of the folder where the bookmarks were imported.
+ }
+
+ /** Listener for asynchronous import events. */
+ public interface OnBookmarksImportedListener {
+ /**
+ * Triggered after finishing the bookmark importing operation.
+ * @param results Results of the importing operation. Will be null in case of failure.
+ */
+ public void onBookmarksImported(ImportResults results);
+ }
+
+ /** Object defining an imported bookmark. */
+ static class Bookmark {
+ // To be provided by the bookmark extractors.
+ public long id; // Local id of the imported bookmark. Value ROOT_FOLDER_ID is reserved.
+ public long parentId; // Import id of the parent node.
+ public boolean isFolder; // True if the object describes a bookmark folder.
+ public String url; // URL of the bookmark. Required for non-folders.
+ public String title; // Title of the bookmark.
+ public Long created; // Creation date (timestamp) of the bookmark. Optional.
+ public Long lastVisit; // Date (timestamp) of the last visit. Optional.
+ public Long visits; // Number of visits to the page. Optional.
+ public byte[] favicon; // Favicon of the bookmark. Optional.
+
+ // For auxiliary use while importing. Not to be set by the bookmark extractors.
+ public long nativeId;
+ public Bookmark parent;
+ public ArrayList<Bookmark> entries = new ArrayList<Bookmark>();
+ public boolean processed;
+ }
+
+ /** Closable iterator for available bookmarks. */
+ public interface BookmarkIterator extends Iterator<Bookmark> {
+ public void close();
+ }
+
+ /**
+ * Returns an array of iterators to the available bookmarks.
+ * The first one is tried and in case of complete importing failure the second one is then used
+ * and so on until the array is exhausted. Note that no new bookmarks is not a failure.
+ *
+ * Called by an async task.
+ */
+ protected abstract BookmarkIterator[] availableBookmarks();
+
+ /** Imported bookmark id reserved for the root folder. */
+ static final long ROOT_FOLDER_ID = 0;
+
+ // Auxiliary query constants.
+ private static final Integer VALUE_IS_BOOKMARK = 1;
+ private static final String SELECT_IS_BOOKMARK = Browser.BookmarkColumns.BOOKMARK + "="
+ + VALUE_IS_BOOKMARK.toString();
+ private static final String HAS_URL = Browser.BookmarkColumns.URL + "=?";
+ private static final String[] EXISTS_PROJECTION = new String[]{ BookmarkColumns.URL };
+
+ protected final Context mContext;
+
+ private ImportBookmarksTask mTask;
+
+ protected BookmarkImporter(Context context) {
+ mContext = context;
+ }
+
+ /** Asynchronously import bookmarks from another browser */
+ public void importBookmarks(OnBookmarksImportedListener listener) {
+ mTask = new ImportBookmarksTask(listener);
+ mTask.execute();
+ }
+
+ public void cancel() {
+ mTask.cancel(true);
+ }
+
+ /**
+ * Handles loading Android Browser bookmarks in a background thread.
+ */
+ private class ImportBookmarksTask extends AsyncTask<Void, Void, ImportResults> {
+ private final OnBookmarksImportedListener mBookmarksImportedListener;
+
+ ImportBookmarksTask(OnBookmarksImportedListener listener) {
+ mBookmarksImportedListener = listener;
+ }
+
+ @Override
+ protected ImportResults doInBackground(Void... params) {
+ BookmarkIterator[] iterators = null;
+ try {
+ iterators = availableBookmarks();
+ } catch (Exception e) {
+ Log.w(TAG, "Unexpected exception while requesting available bookmarks: "
+ + e.getMessage());
+ return null;
+ }
+
+ if (iterators == null) {
+ Log.e(TAG, "No bookmark iterators found.");
+ return null;
+ }
+
+ for (BookmarkIterator iterator : iterators) {
+ ImportResults results = importFromIterator(iterator);
+ if (results != null) return results;
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(ImportResults results) {
+ if (mBookmarksImportedListener != null) {
+ mBookmarksImportedListener.onBookmarksImported(results);
+ }
+ }
+
+ private ImportResults importFromIterator(BookmarkIterator bookmarkIterator) {
+ try {
+ 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>();
+
+ // The root folder is used for hierarchy reconstruction purposes only.
+ // Bookmarks are directly imported into the Mobile Bookmarks folder.
+ Bookmark rootFolder = createRootFolderBookmark();
+ idMap.put(ROOT_FOLDER_ID, rootFolder);
+
+ int failedImports = 0;
+ while (bookmarkIterator.hasNext()) {
+ Bookmark bookmark = bookmarkIterator.next();
+ if (bookmark == null) {
+ ++failedImports;
+ continue;
+ }
+
+ // Check for duplicate ids.
+ if (idMap.containsKey(bookmark.id)) {
+ Log.e(TAG, "Duplicate bookmark id: " + bookmark.id
+ + ". Dropping bookmark.");
+ ++failedImports;
+ continue;
+ }
+
+ // Check for duplicate URLs.
+ if (!bookmark.isFolder && urlSet.contains(bookmark.url)) {
+ Log.i(TAG, "More than one bookmark pointing to " + bookmark.url
+ + ". Keeping only the first one for consistency with Chromium.");
+ continue;
+ }
+
+ // Reject bookmarks that already exist in the native model.
+ if (alreadyExists(bookmark)) continue;
+
+ idMap.put(bookmark.id, bookmark);
+ urlSet.add(bookmark.url);
+ }
+ bookmarkIterator.close();
+
+ // Abort if no new bookmarks to import.
+ ImportResults results = new ImportResults();
+ results.rootFolderId = rootFolder.nativeId;
+ results.newBookmarks = idMap.size() + failedImports - 1;
+ if (results.newBookmarks == 0) return results;
+
+ // Check if all imports failed.
+ if (idMap.size() == 1 && failedImports > 0) return null;
+
+ // Recreate the folder hierarchy and import it.
+ recreateFolderHierarchy(idMap);
+ importBookmarkHierarchy(rootFolder, results);
+
+ return results;
+ } catch (Exception e) {
+ Log.w(TAG, "Unexpected exception while importing bookmarks: " + e.getMessage());
+ return null;
+ }
+ }
+
+ private ContentValues getBookmarkValues(Bookmark bookmark) {
+ ContentValues values = new ContentValues();
+ values.put(BookmarkColumns.BOOKMARK, VALUE_IS_BOOKMARK);
+ values.put(BookmarkColumns.URL, bookmark.url);
+ values.put(BookmarkColumns.TITLE, bookmark.title);
+ values.put(ChromeBrowserProvider.BOOKMARK_PARENT_ID_PARAM, bookmark.parent.nativeId);
+ if (bookmark.created != null) values.put(BookmarkColumns.CREATED, bookmark.created);
+ if (bookmark.lastVisit != null) values.put(BookmarkColumns.DATE, bookmark.lastVisit);
+ if (bookmark.visits != null) {
+ // TODO(michaelbai) http://crbug.com/149376, http://b/6362473
+ // See android_provider_backend.cc IsHistoryAndBookmarkRowValid().
+ if (bookmark.created != null && bookmark.lastVisit != null
+ && bookmark.visits.longValue() > 2
+ && bookmark.lastVisit.longValue() - bookmark.created.longValue()
+ > bookmark.visits.longValue()) {
+ values.put(BookmarkColumns.VISITS, bookmark.visits);
+ }
+ }
+ if (bookmark.favicon != null) values.put(BookmarkColumns.FAVICON, bookmark.favicon);
+ return values;
+ }
+
+ private boolean alreadyExists(Bookmark bookmark) {
+ // Folders are re-used if they already exist. No need to filter them out.
+ if (bookmark.isFolder) return false;
+
+ Cursor cursor = mContext.getContentResolver().query(
+ ChromeBrowserProvider.getBookmarksApiUri(mContext), EXISTS_PROJECTION,
+ SELECT_IS_BOOKMARK + " AND " + HAS_URL, new String[]{ bookmark.url }, null);
+ if (cursor != null) {
+ boolean exists = cursor.getCount() > 0;
+ cursor.close();
+ return exists;
+ }
+ return false;
+ }
+
+ private void recreateFolderHierarchy(LinkedHashMap<Long, Bookmark> idMap) {
+ for (Bookmark bookmark : idMap.values()) {
+ if (bookmark.id == ROOT_FOLDER_ID) continue;
+
+ // Look for invalid parent ids and self-cycles.
+ if (!idMap.containsKey(bookmark.parentId) || bookmark.parentId == bookmark.id) {
+ bookmark.parent = idMap.get(ROOT_FOLDER_ID);
+ bookmark.parent.entries.add(bookmark);
+ continue;
+ }
+
+ bookmark.parent = idMap.get(bookmark.parentId);
+ bookmark.parent.entries.add(bookmark);
+ }
+ }
+
+ private Bookmark createRootFolderBookmark() {
+ Bookmark root = new Bookmark();
+ root.id = ROOT_FOLDER_ID;
+ root.nativeId = ChromeBrowserProviderClient.getMobileBookmarksFolderId(mContext);
+ root.parentId = ROOT_FOLDER_ID;
+ root.parent = root;
+ root.isFolder = true;
+ return root;
+ }
+
+ private void importBookmarkHierarchy(Bookmark bookmark, ImportResults results) {
+ // Avoid cycles in the hierarchy that could lead to infinite loops.
+ if (bookmark.processed) return;
+ bookmark.processed = true;
+
+ if (bookmark.isFolder) {
+ if (bookmark.id != ROOT_FOLDER_ID) {
+ bookmark.nativeId = ChromeBrowserProviderClient.createBookmarksFolderOnce(
+ mContext, bookmark.title, bookmark.parent.nativeId);
+ ++results.numImported;
+ }
+
+ if (bookmark.nativeId == ChromeBrowserProviderClient.INVALID_BOOKMARK_ID
+ && bookmark.id != ROOT_FOLDER_ID) {
+ Log.e(TAG, "Error creating the folder '" + bookmark.title
+ + "'. Skipping entries.");
+ return;
+ }
+
+ for (Bookmark entry : bookmark.entries) {
+ if (entry.parent != bookmark) {
+ Log.w(TAG, "Hierarchy error in bookmark '" + bookmark.title
+ + "'. Skipping.");
+ continue;
+ }
+ importBookmarkHierarchy(entry, results);
+ }
+ } else {
+ sanitizeBookmarkDates(bookmark);
+ ContentValues values = getBookmarkValues(bookmark);
+ try {
+ // Check if the URL already exists in the database.
+ String[] urlArgs = new String[]{ bookmark.url };
+ Uri bookmarksApiUri = ChromeBrowserProvider.getBookmarksApiUri(mContext);
+ Cursor history = mContext.getContentResolver().query(
+ bookmarksApiUri, null, HAS_URL, urlArgs, null);
+ boolean alreadyExists = history != null && history.getCount() > 0;
+ if (history != null) history.close();
+
+ if (alreadyExists) {
+ // If so, update the existing information.
+ if (mContext.getContentResolver().update(
+ bookmarksApiUri, values, HAS_URL, urlArgs) == 0) {
+ throw new IllegalArgumentException(
+ "Couldn't update the existing history information");
+ }
+ } else {
+ // Otherwise insert the new information.
+ if (mContext.getContentResolver().insert(
+ bookmarksApiUri, values) == null) {
+ throw new IllegalArgumentException(
+ "Couldn't insert the bookmark");
+ }
+ }
+ ++results.numImported;
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Error inserting bookmark " + bookmark.title + ": "
+ + e.getMessage());
+ }
+ }
+ }
+
+ // Sanitize timestamp inputs as the provider backend might reject some of the bookmarks
+ // if the values are inconsistent.
+ private void sanitizeBookmarkDates(Bookmark bookmark) {
+ final long now = System.currentTimeMillis();
+ if (bookmark.created != null && bookmark.created.longValue() > now) {
+ bookmark.created = Long.valueOf(now);
+ }
+
+ if (bookmark.lastVisit != null && bookmark.lastVisit.longValue() > now) {
+ bookmark.lastVisit = Long.valueOf(now);
+ }
+
+ if (bookmark.created != null && bookmark.lastVisit != null
+ && bookmark.created.longValue() > bookmark.lastVisit.longValue()) {
+ bookmark.created = bookmark.lastVisit;
+ }
+
+ // The provider backend assumes one visit per timestamp and actually checks this.
+ if (bookmark.lastVisit != null && bookmark.created != null && bookmark.visits != null) {
+ long maxVisits = bookmark.lastVisit.longValue() - bookmark.created.longValue() + 1;
+ if (bookmark.visits.longValue() > maxVisits) {
+ bookmark.visits = Long.valueOf(maxVisits);
+ }
+ }
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698