OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.bookmarkimport; |
| 6 |
| 7 import android.content.ContentValues; |
| 8 import android.content.Context; |
| 9 import android.database.Cursor; |
| 10 import android.net.Uri; |
| 11 import android.os.AsyncTask; |
| 12 import android.provider.Browser; |
| 13 import android.provider.Browser.BookmarkColumns; |
| 14 import android.util.Log; |
| 15 |
| 16 import org.chromium.chrome.browser.ChromeBrowserProvider; |
| 17 import org.chromium.chrome.browser.ChromeBrowserProviderClient; |
| 18 |
| 19 import java.util.ArrayList; |
| 20 import java.util.HashSet; |
| 21 import java.util.Iterator; |
| 22 import java.util.LinkedHashMap; |
| 23 |
| 24 /** |
| 25 * Imports bookmarks from another browser into Chrome. |
| 26 */ |
| 27 public abstract class BookmarkImporter { |
| 28 private static final String TAG = "BookmarkImporter"; |
| 29 |
| 30 /** Class containing the results of a bookmark import operation */ |
| 31 public static class ImportResults { |
| 32 public int newBookmarks; // Number of new bookmarks that could be import
ed. |
| 33 public int numImported; // Number of bookmarks that were successfully im
ported. |
| 34 public long rootFolderId; // ID of the folder where the bookmarks were i
mported. |
| 35 } |
| 36 |
| 37 /** Listener for asynchronous import events. */ |
| 38 public interface OnBookmarksImportedListener { |
| 39 /** |
| 40 * Triggered after finishing the bookmark importing operation. |
| 41 * @param results Results of the importing operation. Will be null in ca
se of failure. |
| 42 */ |
| 43 public void onBookmarksImported(ImportResults results); |
| 44 } |
| 45 |
| 46 /** Object defining an imported bookmark. */ |
| 47 static class Bookmark { |
| 48 // To be provided by the bookmark extractors. |
| 49 public long id; // Local id of the imported bookmark. Value ROOT_FOLDER_
ID is reserved. |
| 50 public long parentId; // Import id of the parent node. |
| 51 public boolean isFolder; // True if the object describes a bookmark fold
er. |
| 52 public String url; // URL of the bookmark. Required for non-folders. |
| 53 public String title; // Title of the bookmark. |
| 54 public Long created; // Creation date (timestamp) of the bookmark. Optio
nal. |
| 55 public Long lastVisit; // Date (timestamp) of the last visit. Optional. |
| 56 public Long visits; // Number of visits to the page. Optional. |
| 57 public byte[] favicon; // Favicon of the bookmark. Optional. |
| 58 |
| 59 // For auxiliary use while importing. Not to be set by the bookmark extr
actors. |
| 60 public long nativeId; |
| 61 public Bookmark parent; |
| 62 public ArrayList<Bookmark> entries = new ArrayList<Bookmark>(); |
| 63 public boolean processed; |
| 64 } |
| 65 |
| 66 /** Closable iterator for available bookmarks. */ |
| 67 public interface BookmarkIterator extends Iterator<Bookmark> { |
| 68 public void close(); |
| 69 } |
| 70 |
| 71 /** |
| 72 * Returns an array of iterators to the available bookmarks. |
| 73 * The first one is tried and in case of complete importing failure the seco
nd one is then used |
| 74 * and so on until the array is exhausted. Note that no new bookmarks is not
a failure. |
| 75 * |
| 76 * Called by an async task. |
| 77 */ |
| 78 protected abstract BookmarkIterator[] availableBookmarks(); |
| 79 |
| 80 /** Imported bookmark id reserved for the root folder. */ |
| 81 static final long ROOT_FOLDER_ID = 0; |
| 82 |
| 83 // Auxiliary query constants. |
| 84 private static final Integer VALUE_IS_BOOKMARK = 1; |
| 85 private static final String SELECT_IS_BOOKMARK = Browser.BookmarkColumns.BOO
KMARK + "=" |
| 86 + VALUE_IS_BOOKMARK.toString(); |
| 87 private static final String HAS_URL = Browser.BookmarkColumns.URL + "=?"; |
| 88 private static final String[] EXISTS_PROJECTION = new String[]{ BookmarkColu
mns.URL }; |
| 89 |
| 90 protected final Context mContext; |
| 91 |
| 92 private ImportBookmarksTask mTask; |
| 93 |
| 94 protected BookmarkImporter(Context context) { |
| 95 mContext = context; |
| 96 } |
| 97 |
| 98 /** Asynchronously import bookmarks from another browser */ |
| 99 public void importBookmarks(OnBookmarksImportedListener listener) { |
| 100 mTask = new ImportBookmarksTask(listener); |
| 101 mTask.execute(); |
| 102 } |
| 103 |
| 104 public void cancel() { |
| 105 mTask.cancel(true); |
| 106 } |
| 107 |
| 108 /** |
| 109 * Handles loading Android Browser bookmarks in a background thread. |
| 110 */ |
| 111 private class ImportBookmarksTask extends AsyncTask<Void, Void, ImportResult
s> { |
| 112 private final OnBookmarksImportedListener mBookmarksImportedListener; |
| 113 |
| 114 ImportBookmarksTask(OnBookmarksImportedListener listener) { |
| 115 mBookmarksImportedListener = listener; |
| 116 } |
| 117 |
| 118 @Override |
| 119 protected ImportResults doInBackground(Void... params) { |
| 120 BookmarkIterator[] iterators = null; |
| 121 try { |
| 122 iterators = availableBookmarks(); |
| 123 } catch (Exception e) { |
| 124 Log.w(TAG, "Unexpected exception while requesting available book
marks: " |
| 125 + e.getMessage()); |
| 126 return null; |
| 127 } |
| 128 |
| 129 if (iterators == null) { |
| 130 Log.e(TAG, "No bookmark iterators found."); |
| 131 return null; |
| 132 } |
| 133 |
| 134 for (BookmarkIterator iterator : iterators) { |
| 135 ImportResults results = importFromIterator(iterator); |
| 136 if (results != null) return results; |
| 137 } |
| 138 |
| 139 return null; |
| 140 } |
| 141 |
| 142 @Override |
| 143 protected void onPostExecute(ImportResults results) { |
| 144 if (mBookmarksImportedListener != null) { |
| 145 mBookmarksImportedListener.onBookmarksImported(results); |
| 146 } |
| 147 } |
| 148 |
| 149 private ImportResults importFromIterator(BookmarkIterator bookmarkIterat
or) { |
| 150 try { |
| 151 if (bookmarkIterator == null) return null; |
| 152 |
| 153 // Get a snapshot of the bookmarks. |
| 154 LinkedHashMap<Long, Bookmark> idMap = new LinkedHashMap<Long, Bo
okmark>(); |
| 155 HashSet<String> urlSet = new HashSet<String>(); |
| 156 |
| 157 // The root folder is used for hierarchy reconstruction purposes
only. |
| 158 // Bookmarks are directly imported into the Mobile Bookmarks fol
der. |
| 159 Bookmark rootFolder = createRootFolderBookmark(); |
| 160 idMap.put(ROOT_FOLDER_ID, rootFolder); |
| 161 |
| 162 int failedImports = 0; |
| 163 while (bookmarkIterator.hasNext()) { |
| 164 Bookmark bookmark = bookmarkIterator.next(); |
| 165 if (bookmark == null) { |
| 166 ++failedImports; |
| 167 continue; |
| 168 } |
| 169 |
| 170 // Check for duplicate ids. |
| 171 if (idMap.containsKey(bookmark.id)) { |
| 172 Log.e(TAG, "Duplicate bookmark id: " + bookmark.id |
| 173 + ". Dropping bookmark."); |
| 174 ++failedImports; |
| 175 continue; |
| 176 } |
| 177 |
| 178 // Check for duplicate URLs. |
| 179 if (!bookmark.isFolder && urlSet.contains(bookmark.url)) { |
| 180 Log.i(TAG, "More than one bookmark pointing to " + bookm
ark.url |
| 181 + ". Keeping only the first one for consistency
with Chromium."); |
| 182 continue; |
| 183 } |
| 184 |
| 185 // Reject bookmarks that already exist in the native model. |
| 186 if (alreadyExists(bookmark)) continue; |
| 187 |
| 188 idMap.put(bookmark.id, bookmark); |
| 189 urlSet.add(bookmark.url); |
| 190 } |
| 191 bookmarkIterator.close(); |
| 192 |
| 193 // Abort if no new bookmarks to import. |
| 194 ImportResults results = new ImportResults(); |
| 195 results.rootFolderId = rootFolder.nativeId; |
| 196 results.newBookmarks = idMap.size() + failedImports - 1; |
| 197 if (results.newBookmarks == 0) return results; |
| 198 |
| 199 // Check if all imports failed. |
| 200 if (idMap.size() == 1 && failedImports > 0) return null; |
| 201 |
| 202 // Recreate the folder hierarchy and import it. |
| 203 recreateFolderHierarchy(idMap); |
| 204 importBookmarkHierarchy(rootFolder, results); |
| 205 |
| 206 return results; |
| 207 } catch (Exception e) { |
| 208 Log.w(TAG, "Unexpected exception while importing bookmarks: " +
e.getMessage()); |
| 209 return null; |
| 210 } |
| 211 } |
| 212 |
| 213 private ContentValues getBookmarkValues(Bookmark bookmark) { |
| 214 ContentValues values = new ContentValues(); |
| 215 values.put(BookmarkColumns.BOOKMARK, VALUE_IS_BOOKMARK); |
| 216 values.put(BookmarkColumns.URL, bookmark.url); |
| 217 values.put(BookmarkColumns.TITLE, bookmark.title); |
| 218 values.put(ChromeBrowserProvider.BOOKMARK_PARENT_ID_PARAM, bookmark.
parent.nativeId); |
| 219 if (bookmark.created != null) values.put(BookmarkColumns.CREATED, bo
okmark.created); |
| 220 if (bookmark.lastVisit != null) values.put(BookmarkColumns.DATE, boo
kmark.lastVisit); |
| 221 if (bookmark.visits != null) { |
| 222 // TODO(michaelbai) http://crbug.com/149376, http://b/6362473 |
| 223 // See android_provider_backend.cc IsHistoryAndBookmarkRowValid(
). |
| 224 if (bookmark.created != null && bookmark.lastVisit != null |
| 225 && bookmark.visits.longValue() > 2 |
| 226 && bookmark.lastVisit.longValue() - bookmark.created.lon
gValue() |
| 227 > bookmark.visits.longValue()) { |
| 228 values.put(BookmarkColumns.VISITS, bookmark.visits); |
| 229 } |
| 230 } |
| 231 if (bookmark.favicon != null) values.put(BookmarkColumns.FAVICON, bo
okmark.favicon); |
| 232 return values; |
| 233 } |
| 234 |
| 235 private boolean alreadyExists(Bookmark bookmark) { |
| 236 // Folders are re-used if they already exist. No need to filter them
out. |
| 237 if (bookmark.isFolder) return false; |
| 238 |
| 239 Cursor cursor = mContext.getContentResolver().query( |
| 240 ChromeBrowserProvider.getBookmarksApiUri(mContext), EXISTS_P
ROJECTION, |
| 241 SELECT_IS_BOOKMARK + " AND " + HAS_URL, new String[]{ bookma
rk.url }, null); |
| 242 if (cursor != null) { |
| 243 boolean exists = cursor.getCount() > 0; |
| 244 cursor.close(); |
| 245 return exists; |
| 246 } |
| 247 return false; |
| 248 } |
| 249 |
| 250 private void recreateFolderHierarchy(LinkedHashMap<Long, Bookmark> idMap
) { |
| 251 for (Bookmark bookmark : idMap.values()) { |
| 252 if (bookmark.id == ROOT_FOLDER_ID) continue; |
| 253 |
| 254 // Look for invalid parent ids and self-cycles. |
| 255 if (!idMap.containsKey(bookmark.parentId) || bookmark.parentId =
= bookmark.id) { |
| 256 bookmark.parent = idMap.get(ROOT_FOLDER_ID); |
| 257 bookmark.parent.entries.add(bookmark); |
| 258 continue; |
| 259 } |
| 260 |
| 261 bookmark.parent = idMap.get(bookmark.parentId); |
| 262 bookmark.parent.entries.add(bookmark); |
| 263 } |
| 264 } |
| 265 |
| 266 private Bookmark createRootFolderBookmark() { |
| 267 Bookmark root = new Bookmark(); |
| 268 root.id = ROOT_FOLDER_ID; |
| 269 root.nativeId = ChromeBrowserProviderClient.getMobileBookmarksFolder
Id(mContext); |
| 270 root.parentId = ROOT_FOLDER_ID; |
| 271 root.parent = root; |
| 272 root.isFolder = true; |
| 273 return root; |
| 274 } |
| 275 |
| 276 private void importBookmarkHierarchy(Bookmark bookmark, ImportResults re
sults) { |
| 277 // Avoid cycles in the hierarchy that could lead to infinite loops. |
| 278 if (bookmark.processed) return; |
| 279 bookmark.processed = true; |
| 280 |
| 281 if (bookmark.isFolder) { |
| 282 if (bookmark.id != ROOT_FOLDER_ID) { |
| 283 bookmark.nativeId = ChromeBrowserProviderClient.createBookma
rksFolderOnce( |
| 284 mContext, bookmark.title, bookmark.parent.nativeId); |
| 285 ++results.numImported; |
| 286 } |
| 287 |
| 288 if (bookmark.nativeId == ChromeBrowserProviderClient.INVALID_BOO
KMARK_ID |
| 289 && bookmark.id != ROOT_FOLDER_ID) { |
| 290 Log.e(TAG, "Error creating the folder '" + bookmark.title |
| 291 + "'. Skipping entries."); |
| 292 return; |
| 293 } |
| 294 |
| 295 for (Bookmark entry : bookmark.entries) { |
| 296 if (entry.parent != bookmark) { |
| 297 Log.w(TAG, "Hierarchy error in bookmark '" + bookmark.ti
tle |
| 298 + "'. Skipping."); |
| 299 continue; |
| 300 } |
| 301 importBookmarkHierarchy(entry, results); |
| 302 } |
| 303 } else { |
| 304 sanitizeBookmarkDates(bookmark); |
| 305 ContentValues values = getBookmarkValues(bookmark); |
| 306 try { |
| 307 // Check if the URL already exists in the database. |
| 308 String[] urlArgs = new String[]{ bookmark.url }; |
| 309 Uri bookmarksApiUri = ChromeBrowserProvider.getBookmarksApiU
ri(mContext); |
| 310 Cursor history = mContext.getContentResolver().query( |
| 311 bookmarksApiUri, null, HAS_URL, urlArgs, null); |
| 312 boolean alreadyExists = history != null && history.getCount(
) > 0; |
| 313 if (history != null) history.close(); |
| 314 |
| 315 if (alreadyExists) { |
| 316 // If so, update the existing information. |
| 317 if (mContext.getContentResolver().update( |
| 318 bookmarksApiUri, values, HAS_URL, urlArgs) == 0)
{ |
| 319 throw new IllegalArgumentException( |
| 320 "Couldn't update the existing history inform
ation"); |
| 321 } |
| 322 } else { |
| 323 // Otherwise insert the new information. |
| 324 if (mContext.getContentResolver().insert( |
| 325 bookmarksApiUri, values) == null) { |
| 326 throw new IllegalArgumentException( |
| 327 "Couldn't insert the bookmark"); |
| 328 } |
| 329 } |
| 330 ++results.numImported; |
| 331 } catch (IllegalArgumentException e) { |
| 332 Log.w(TAG, "Error inserting bookmark " + bookmark.title + ":
" |
| 333 + e.getMessage()); |
| 334 } |
| 335 } |
| 336 } |
| 337 |
| 338 // Sanitize timestamp inputs as the provider backend might reject some o
f the bookmarks |
| 339 // if the values are inconsistent. |
| 340 private void sanitizeBookmarkDates(Bookmark bookmark) { |
| 341 final long now = System.currentTimeMillis(); |
| 342 if (bookmark.created != null && bookmark.created.longValue() > now)
{ |
| 343 bookmark.created = Long.valueOf(now); |
| 344 } |
| 345 |
| 346 if (bookmark.lastVisit != null && bookmark.lastVisit.longValue() > n
ow) { |
| 347 bookmark.lastVisit = Long.valueOf(now); |
| 348 } |
| 349 |
| 350 if (bookmark.created != null && bookmark.lastVisit != null |
| 351 && bookmark.created.longValue() > bookmark.lastVisit.longVal
ue()) { |
| 352 bookmark.created = bookmark.lastVisit; |
| 353 } |
| 354 |
| 355 // The provider backend assumes one visit per timestamp and actually
checks this. |
| 356 if (bookmark.lastVisit != null && bookmark.created != null && bookma
rk.visits != null) { |
| 357 long maxVisits = bookmark.lastVisit.longValue() - bookmark.creat
ed.longValue() + 1; |
| 358 if (bookmark.visits.longValue() > maxVisits) { |
| 359 bookmark.visits = Long.valueOf(maxVisits); |
| 360 } |
| 361 } |
| 362 } |
| 363 } |
| 364 } |
OLD | NEW |