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); |
+ } |
+ } |
+ } |
+ } |
+} |