Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 package org.mozilla.gecko.db;
8 import java.io.ByteArrayOutputStream;
9 import java.util.Collection;
10 import java.util.HashMap;
11 import java.util.List;
13 import org.mozilla.gecko.AboutPages;
14 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
15 import org.mozilla.gecko.db.BrowserContract.Combined;
16 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
17 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
18 import org.mozilla.gecko.db.BrowserContract.Favicons;
19 import org.mozilla.gecko.db.BrowserContract.History;
20 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
21 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
22 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
23 import org.mozilla.gecko.db.BrowserContract.URLColumns;
24 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
25 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
27 import android.content.ContentProviderOperation;
28 import android.content.ContentResolver;
29 import android.content.ContentValues;
30 import android.database.ContentObserver;
31 import android.database.Cursor;
32 import android.database.CursorWrapper;
33 import android.graphics.Bitmap;
34 import android.graphics.drawable.BitmapDrawable;
35 import android.net.Uri;
36 import android.provider.Browser;
37 import android.text.TextUtils;
38 import android.util.Log;
40 public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
41 // Calculate these once, at initialization. isLoggable is too expensive to
42 // have in-line in each log call.
43 private static final String LOGTAG = "GeckoLocalBrowserDB";
44 private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
45 protected static void debug(String message) {
46 if (logDebug) {
47 Log.d(LOGTAG, message);
48 }
49 }
51 private final String mProfile;
53 // Map of folder GUIDs to IDs. Used for caching.
54 private HashMap<String, Long> mFolderIdMap;
56 // Use wrapped Boolean so that we can have a null state
57 private Boolean mDesktopBookmarksExist;
59 private final Uri mBookmarksUriWithProfile;
60 private final Uri mParentsUriWithProfile;
61 private final Uri mFlagsUriWithProfile;
62 private final Uri mHistoryUriWithProfile;
63 private final Uri mHistoryExpireUriWithProfile;
64 private final Uri mCombinedUriWithProfile;
65 private final Uri mDeletedHistoryUriWithProfile;
66 private final Uri mUpdateHistoryUriWithProfile;
67 private final Uri mFaviconsUriWithProfile;
68 private final Uri mThumbnailsUriWithProfile;
69 private final Uri mReadingListUriWithProfile;
71 private static final String[] DEFAULT_BOOKMARK_COLUMNS =
72 new String[] { Bookmarks._ID,
73 Bookmarks.GUID,
74 Bookmarks.URL,
75 Bookmarks.TITLE,
76 Bookmarks.TYPE,
77 Bookmarks.PARENT };
79 public LocalBrowserDB(String profile) {
80 mProfile = profile;
81 mFolderIdMap = new HashMap<String, Long>();
82 mDesktopBookmarksExist = null;
84 mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI);
85 mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI);
86 mFlagsUriWithProfile = appendProfile(Bookmarks.FLAGS_URI);
87 mHistoryUriWithProfile = appendProfile(History.CONTENT_URI);
88 mHistoryExpireUriWithProfile = appendProfile(History.CONTENT_OLD_URI);
89 mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI);
90 mFaviconsUriWithProfile = appendProfile(Favicons.CONTENT_URI);
91 mThumbnailsUriWithProfile = appendProfile(Thumbnails.CONTENT_URI);
92 mReadingListUriWithProfile = appendProfile(ReadingListItems.CONTENT_URI);
94 mDeletedHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon().
95 appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1").build();
97 mUpdateHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon().
98 appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").
99 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
100 }
102 // Invalidate cached data
103 @Override
104 public void invalidateCachedState() {
105 mDesktopBookmarksExist = null;
106 }
108 private Uri historyUriWithLimit(int limit) {
109 return mHistoryUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT,
110 String.valueOf(limit)).build();
111 }
113 private Uri bookmarksUriWithLimit(int limit) {
114 return mBookmarksUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT,
115 String.valueOf(limit)).build();
116 }
118 private Uri combinedUriWithLimit(int limit) {
119 return mCombinedUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT,
120 String.valueOf(limit)).build();
121 }
123 private Uri appendProfile(Uri uri) {
124 return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).build();
125 }
127 private Uri getAllBookmarksUri() {
128 Uri.Builder uriBuilder = mBookmarksUriWithProfile.buildUpon()
129 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
130 return uriBuilder.build();
131 }
133 private Uri getAllHistoryUri() {
134 Uri.Builder uriBuilder = mHistoryUriWithProfile.buildUpon()
135 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
136 return uriBuilder.build();
137 }
139 private Uri getAllFaviconsUri() {
140 Uri.Builder uriBuilder = mFaviconsUriWithProfile.buildUpon()
141 .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
142 return uriBuilder.build();
143 }
145 private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint,
146 int limit, CharSequence urlFilter) {
147 return filterAllSites(cr, projection, constraint, limit, urlFilter, "", null);
148 }
150 private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint,
151 int limit, CharSequence urlFilter, String selection, String[] selectionArgs) {
152 // The combined history/bookmarks selection queries for sites with a url or title containing
153 // the constraint string(s), treating space-separated words as separate constraints
154 if (!TextUtils.isEmpty(constraint)) {
155 String[] constraintWords = constraint.toString().split(" ");
156 // Only create a filter query with a maximum of 10 constraint words
157 int constraintCount = Math.min(constraintWords.length, 10);
158 for (int i = 0; i < constraintCount; i++) {
159 selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
160 Combined.TITLE + " LIKE ?)");
161 String constraintWord = "%" + constraintWords[i] + "%";
162 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
163 new String[] { constraintWord, constraintWord });
164 }
165 }
167 if (urlFilter != null) {
168 selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)");
169 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() });
170 }
172 // Our version of frecency is computed by scaling the number of visits by a multiplier
173 // that approximates Gaussian decay, based on how long ago the entry was last visited.
174 // Since we're limited by the math we can do with sqlite, we're calculating this
175 // approximation using the Cauchy distribution: multiplier = 15^2 / (age^2 + 15^2).
176 // Using 15 as our scale parameter, we get a constant 15^2 = 225. Following this math,
177 // frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977)
178 // We also give bookmarks an extra bonus boost by adding 100 points to their frecency score.
179 final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
181 Cursor c = cr.query(combinedUriWithLimit(limit),
182 projection,
183 selection,
184 selectionArgs,
185 sortOrder);
187 return new LocalDBCursor(c);
188 }
190 @Override
191 public int getCount(ContentResolver cr, String database) {
192 int count = 0;
193 String[] columns = null;
194 String constraint = null;
195 Uri uri = null;
196 if ("history".equals(database)) {
197 uri = mHistoryUriWithProfile;
198 columns = new String[] { History._ID };
199 constraint = Combined.VISITS + " > 0";
200 } else if ("bookmarks".equals(database)) {
201 uri = mBookmarksUriWithProfile;
202 columns = new String[] { Bookmarks._ID };
203 // ignore folders, tags, keywords, separators, etc.
204 constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK;
205 } else if ("thumbnails".equals(database)) {
206 uri = mThumbnailsUriWithProfile;
207 columns = new String[] { Thumbnails._ID };
208 } else if ("favicons".equals(database)) {
209 uri = mFaviconsUriWithProfile;
210 columns = new String[] { Favicons._ID };
211 }
212 if (uri != null) {
213 Cursor cursor = null;
215 try {
216 cursor = cr.query(uri, columns, constraint, null, null);
217 count = cursor.getCount();
218 } finally {
219 if (cursor != null)
220 cursor.close();
221 }
222 }
223 debug("Got count " + count + " for " + database);
224 return count;
225 }
227 @Override
228 public Cursor filter(ContentResolver cr, CharSequence constraint, int limit) {
229 return filterAllSites(cr,
230 new String[] { Combined._ID,
231 Combined.URL,
232 Combined.TITLE,
233 Combined.DISPLAY,
234 Combined.BOOKMARK_ID,
235 Combined.HISTORY_ID },
236 constraint,
237 limit,
238 null);
239 }
241 @Override
242 public Cursor getTopSites(ContentResolver cr, int limit) {
243 // Filter out bookmarks that don't have real parents (e.g. pinned sites or reading list items)
244 String selection = DBUtils.concatenateWhere("", Combined.URL + " NOT IN (SELECT " +
245 Bookmarks.URL + " FROM bookmarks WHERE " +
246 DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < ? AND " +
247 DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)");
248 String[] selectionArgs = new String[] { String.valueOf(Bookmarks.FIXED_ROOT_ID) };
250 return filterAllSites(cr,
251 new String[] { Combined._ID,
252 Combined.URL,
253 Combined.TITLE,
254 Combined.DISPLAY,
255 Combined.BOOKMARK_ID,
256 Combined.HISTORY_ID },
257 "",
258 limit,
259 AboutPages.URL_FILTER,
260 selection,
261 selectionArgs);
262 }
264 @Override
265 public void updateVisitedHistory(ContentResolver cr, String uri) {
266 ContentValues values = new ContentValues();
268 values.put(History.URL, uri);
269 values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
270 values.put(History.IS_DELETED, 0);
272 // This will insert a new history entry if one for this URL
273 // doesn't already exist
274 cr.update(mUpdateHistoryUriWithProfile,
275 values,
276 History.URL + " = ?",
277 new String[] { uri });
278 }
280 @Override
281 public void updateHistoryTitle(ContentResolver cr, String uri, String title) {
282 ContentValues values = new ContentValues();
283 values.put(History.TITLE, title);
285 cr.update(mHistoryUriWithProfile,
286 values,
287 History.URL + " = ?",
288 new String[] { uri });
289 }
291 @Override
292 public void updateHistoryEntry(ContentResolver cr, String uri, String title,
293 long date, int visits) {
294 int oldVisits = 0;
295 Cursor cursor = null;
296 try {
297 cursor = cr.query(mHistoryUriWithProfile,
298 new String[] { History.VISITS },
299 History.URL + " = ?",
300 new String[] { uri },
301 null);
303 if (cursor.moveToFirst()) {
304 oldVisits = cursor.getInt(0);
305 }
306 } finally {
307 if (cursor != null)
308 cursor.close();
309 }
311 ContentValues values = new ContentValues();
312 values.put(History.DATE_LAST_VISITED, date);
313 values.put(History.VISITS, oldVisits + visits);
314 if (title != null) {
315 values.put(History.TITLE, title);
316 }
318 cr.update(mHistoryUriWithProfile,
319 values,
320 History.URL + " = ?",
321 new String[] { uri });
322 }
324 @Override
325 public Cursor getAllVisitedHistory(ContentResolver cr) {
326 Cursor c = cr.query(mHistoryUriWithProfile,
327 new String[] { History.URL },
328 History.VISITS + " > 0",
329 null,
330 null);
332 return new LocalDBCursor(c);
333 }
335 @Override
336 public Cursor getRecentHistory(ContentResolver cr, int limit) {
337 Cursor c = cr.query(combinedUriWithLimit(limit),
338 new String[] { Combined._ID,
339 Combined.BOOKMARK_ID,
340 Combined.HISTORY_ID,
341 Combined.URL,
342 Combined.TITLE,
343 Combined.DISPLAY,
344 Combined.DATE_LAST_VISITED,
345 Combined.VISITS },
346 History.DATE_LAST_VISITED + " > 0",
347 null,
348 History.DATE_LAST_VISITED + " DESC");
350 return new LocalDBCursor(c);
351 }
353 @Override
354 public void expireHistory(ContentResolver cr, ExpirePriority priority) {
355 Uri url = mHistoryExpireUriWithProfile;
356 url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build();
357 cr.delete(url, null, null);
358 }
360 @Override
361 public void removeHistoryEntry(ContentResolver cr, int id) {
362 cr.delete(mHistoryUriWithProfile,
363 History._ID + " = ?",
364 new String[] { String.valueOf(id) });
365 }
367 @Override
368 public void removeHistoryEntry(ContentResolver cr, String url) {
369 int deleted = cr.delete(mHistoryUriWithProfile,
370 History.URL + " = ?",
371 new String[] { url });
372 }
374 @Override
375 public void clearHistory(ContentResolver cr) {
376 cr.delete(mHistoryUriWithProfile, null, null);
377 }
379 @Override
380 public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
381 Cursor c = null;
382 boolean addDesktopFolder = false;
384 // We always want to show mobile bookmarks in the root view.
385 if (folderId == Bookmarks.FIXED_ROOT_ID) {
386 folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
388 // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop
389 // bookmarks exist, so that the user can still access non-mobile bookmarks.
390 addDesktopFolder = desktopBookmarksExist(cr);
391 }
393 if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) {
394 // Since the "Desktop Bookmarks" folder doesn't actually exist, we
395 // just fake it by querying specifically certain known desktop folders.
396 c = cr.query(mBookmarksUriWithProfile,
397 DEFAULT_BOOKMARK_COLUMNS,
398 Bookmarks.GUID + " = ? OR " +
399 Bookmarks.GUID + " = ? OR " +
400 Bookmarks.GUID + " = ?",
401 new String[] { Bookmarks.TOOLBAR_FOLDER_GUID,
402 Bookmarks.MENU_FOLDER_GUID,
403 Bookmarks.UNFILED_FOLDER_GUID },
404 null);
405 } else {
406 // Right now, we only support showing folder and bookmark type of
407 // entries. We should add support for other types though (bug 737024)
408 c = cr.query(mBookmarksUriWithProfile,
409 DEFAULT_BOOKMARK_COLUMNS,
410 Bookmarks.PARENT + " = ? AND " +
411 "(" + Bookmarks.TYPE + " = ? OR " +
412 "(" + Bookmarks.TYPE + " = ? AND " + Bookmarks.URL + " IS NOT NULL))",
413 new String[] { String.valueOf(folderId),
414 String.valueOf(Bookmarks.TYPE_FOLDER),
415 String.valueOf(Bookmarks.TYPE_BOOKMARK) },
416 null);
417 }
419 if (addDesktopFolder) {
420 // Wrap cursor to add fake desktop bookmarks and reading list folders
421 c = new SpecialFoldersCursorWrapper(c, addDesktopFolder);
422 }
424 return new LocalDBCursor(c);
425 }
427 @Override
428 public Cursor getReadingList(ContentResolver cr) {
429 return cr.query(mReadingListUriWithProfile,
430 ReadingListItems.DEFAULT_PROJECTION,
431 null,
432 null,
433 null);
434 }
437 // Returns true if any desktop bookmarks exist, which will be true if the user
438 // has set up sync at one point, or done a profile migration from XUL fennec.
439 private boolean desktopBookmarksExist(ContentResolver cr) {
440 if (mDesktopBookmarksExist != null)
441 return mDesktopBookmarksExist;
443 Cursor c = null;
444 int count = 0;
445 try {
446 // Check to see if there are any bookmarks in one of our three
447 // fixed "Desktop Boomarks" folders.
448 c = cr.query(bookmarksUriWithLimit(1),
449 new String[] { Bookmarks._ID },
450 Bookmarks.PARENT + " = ? OR " +
451 Bookmarks.PARENT + " = ? OR " +
452 Bookmarks.PARENT + " = ?",
453 new String[] { String.valueOf(getFolderIdFromGuid(cr, Bookmarks.TOOLBAR_FOLDER_GUID)),
454 String.valueOf(getFolderIdFromGuid(cr, Bookmarks.MENU_FOLDER_GUID)),
455 String.valueOf(getFolderIdFromGuid(cr, Bookmarks.UNFILED_FOLDER_GUID)) },
456 null);
457 count = c.getCount();
458 } finally {
459 if (c != null)
460 c.close();
461 }
463 // Cache result for future queries
464 mDesktopBookmarksExist = (count > 0);
465 return mDesktopBookmarksExist;
466 }
468 @Override
469 public int getReadingListCount(ContentResolver cr) {
470 Cursor c = null;
471 try {
472 c = cr.query(mReadingListUriWithProfile,
473 new String[] { ReadingListItems._ID },
474 null,
475 null,
476 null);
477 return c.getCount();
478 } finally {
479 if (c != null) {
480 c.close();
481 }
482 }
483 }
485 @Override
486 public boolean isBookmark(ContentResolver cr, String uri) {
487 // This method is about normal bookmarks, not the Reading List.
488 Cursor c = null;
489 try {
490 c = cr.query(bookmarksUriWithLimit(1),
491 new String[] { Bookmarks._ID },
492 Bookmarks.URL + " = ? AND " +
493 Bookmarks.PARENT + " != ? AND " +
494 Bookmarks.PARENT + " != ?",
495 new String[] { uri,
496 String.valueOf(Bookmarks.FIXED_READING_LIST_ID),
497 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) },
498 Bookmarks.URL);
499 return c.getCount() > 0;
500 } catch (NullPointerException e) {
501 Log.e(LOGTAG, "NullPointerException in isBookmark");
502 } finally {
503 if (c != null)
504 c.close();
505 }
507 return false;
508 }
510 @Override
511 public boolean isReadingListItem(ContentResolver cr, String uri) {
512 Cursor c = null;
513 try {
514 c = cr.query(mReadingListUriWithProfile,
515 new String[] { ReadingListItems._ID },
516 ReadingListItems.URL + " = ? ",
517 new String[] { uri },
518 null);
519 return c.getCount() > 0;
520 } catch (NullPointerException e) {
521 Log.e(LOGTAG, "NullPointerException in isReadingListItem");
522 } finally {
523 if (c != null)
524 c.close();
525 }
527 return false;
528 }
530 /**
531 * For a given URI, we want to return a number of things:
532 *
533 * * Is this URI the URI of a bookmark?
534 * * ... a reading list item?
535 *
536 * This will expand as necessary to eliminate multiple consecutive queries.
537 */
538 @Override
539 public int getItemFlags(ContentResolver cr, String uri) {
540 final Cursor c = cr.query(mFlagsUriWithProfile,
541 null,
542 null,
543 new String[] { uri },
544 null);
545 if (c == null) {
546 return 0;
547 }
549 try {
550 // This should never fail: it returns a single `flags` row.
551 c.moveToFirst();
552 return Bookmarks.FLAG_SUCCESS | c.getInt(0);
553 } finally {
554 c.close();
555 }
556 }
558 @Override
559 public String getUrlForKeyword(ContentResolver cr, String keyword) {
560 Cursor c = null;
561 try {
562 c = cr.query(mBookmarksUriWithProfile,
563 new String[] { Bookmarks.URL },
564 Bookmarks.KEYWORD + " = ?",
565 new String[] { keyword },
566 null);
568 if (c.moveToFirst())
569 return c.getString(c.getColumnIndexOrThrow(Bookmarks.URL));
570 } finally {
571 if (c != null)
572 c.close();
573 }
575 return null;
576 }
578 private synchronized long getFolderIdFromGuid(ContentResolver cr, String guid) {
579 if (mFolderIdMap.containsKey(guid))
580 return mFolderIdMap.get(guid);
582 long folderId = -1;
583 Cursor c = null;
585 try {
586 c = cr.query(mBookmarksUriWithProfile,
587 new String[] { Bookmarks._ID },
588 Bookmarks.GUID + " = ?",
589 new String[] { guid },
590 null);
592 if (c.moveToFirst())
593 folderId = c.getLong(c.getColumnIndexOrThrow(Bookmarks._ID));
594 } finally {
595 if (c != null)
596 c.close();
597 }
599 mFolderIdMap.put(guid, folderId);
600 return folderId;
601 }
603 /**
604 * Find parents of records that match the provided criteria, and bump their
605 * modified timestamp.
606 */
607 protected void bumpParents(ContentResolver cr, String param, String value) {
608 ContentValues values = new ContentValues();
609 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
611 String where = param + " = ?";
612 String[] args = new String[] { value };
613 int updated = cr.update(mParentsUriWithProfile, values, where, args);
614 debug("Updated " + updated + " rows to new modified time.");
615 }
617 private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) {
618 final long now = System.currentTimeMillis();
619 ContentValues values = new ContentValues();
620 values.put(Browser.BookmarkColumns.TITLE, title);
621 values.put(Bookmarks.URL, uri);
622 values.put(Bookmarks.PARENT, folderId);
623 values.put(Bookmarks.DATE_MODIFIED, now);
625 // Get the page's favicon ID from the history table
626 Cursor c = null;
627 try {
628 c = cr.query(mHistoryUriWithProfile,
629 new String[] { History.FAVICON_ID },
630 History.URL + " = ?",
631 new String[] { uri },
632 null);
634 if (c.moveToFirst()) {
635 int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_ID);
636 if (!c.isNull(columnIndex))
637 values.put(Bookmarks.FAVICON_ID, c.getLong(columnIndex));
638 }
639 } finally {
640 if (c != null)
641 c.close();
642 }
644 // Restore deleted record if possible
645 values.put(Bookmarks.IS_DELETED, 0);
647 final Uri bookmarksWithInsert = mBookmarksUriWithProfile.buildUpon()
648 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
649 .build();
650 cr.update(bookmarksWithInsert,
651 values,
652 Bookmarks.URL + " = ? AND " +
653 Bookmarks.PARENT + " = " + folderId,
654 new String[] { uri });
656 // Bump parent modified time using its ID.
657 debug("Bumping parent modified time for addition to: " + folderId);
658 final String where = Bookmarks._ID + " = ?";
659 final String[] args = new String[] { String.valueOf(folderId) };
661 ContentValues bumped = new ContentValues();
662 bumped.put(Bookmarks.DATE_MODIFIED, now);
664 final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args);
665 debug("Updated " + updated + " rows to new modified time.");
666 }
668 @Override
669 public void addBookmark(ContentResolver cr, String title, String uri) {
670 long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
671 addBookmarkItem(cr, title, uri, folderId);
672 }
674 @Override
675 public void removeBookmark(ContentResolver cr, int id) {
676 Uri contentUri = mBookmarksUriWithProfile;
678 // Do this now so that the item still exists!
679 final String idString = String.valueOf(id);
680 bumpParents(cr, Bookmarks._ID, idString);
682 final String[] idArgs = new String[] { idString };
683 final String idEquals = Bookmarks._ID + " = ?";
684 cr.delete(contentUri, idEquals, idArgs);
685 }
687 @Override
688 public void removeBookmarksWithURL(ContentResolver cr, String uri) {
689 Uri contentUri = mBookmarksUriWithProfile;
691 // Do this now so that the items still exist!
692 bumpParents(cr, Bookmarks.URL, uri);
694 // Toggling bookmark on an URL should not affect the items in the reading list or pinned sites.
695 final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
696 final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? AND " + Bookmarks.PARENT + " != ? ";
698 cr.delete(contentUri, urlEquals, urlArgs);
699 }
701 @Override
702 public void addReadingListItem(ContentResolver cr, ContentValues values) {
703 // Check that required fields are present.
704 for (String field: ReadingListItems.REQUIRED_FIELDS) {
705 if (!values.containsKey(field)) {
706 throw new IllegalArgumentException("Missing required field for reading list item: " + field);
707 }
708 }
710 // Clear delete flag if necessary
711 values.put(ReadingListItems.IS_DELETED, 0);
713 // Restore deleted record if possible
714 final Uri insertUri = mReadingListUriWithProfile
715 .buildUpon()
716 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
717 .build();
719 final int updated = cr.update(insertUri,
720 values,
721 ReadingListItems.URL + " = ? ",
722 new String[] { values.getAsString(ReadingListItems.URL) });
724 debug("Updated " + updated + " rows to new modified time.");
725 }
727 @Override
728 public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
729 cr.delete(mReadingListUriWithProfile, ReadingListItems.URL + " = ? ", new String[] { uri });
730 }
732 @Override
733 public void removeReadingListItem(ContentResolver cr, int id) {
734 cr.delete(mReadingListUriWithProfile, ReadingListItems._ID + " = ? ", new String[] { String.valueOf(id) });
735 }
737 @Override
738 public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
739 cr.registerContentObserver(mBookmarksUriWithProfile, false, observer);
740 }
742 @Override
743 public void registerHistoryObserver(ContentResolver cr, ContentObserver observer) {
744 cr.registerContentObserver(mHistoryUriWithProfile, false, observer);
745 }
747 @Override
748 public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
749 ContentValues values = new ContentValues();
750 values.put(Browser.BookmarkColumns.TITLE, title);
751 values.put(Bookmarks.URL, uri);
752 values.put(Bookmarks.KEYWORD, keyword);
753 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
755 cr.update(mBookmarksUriWithProfile,
756 values,
757 Bookmarks._ID + " = ?",
758 new String[] { String.valueOf(id) });
759 }
761 /**
762 * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
763 * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
764 * @param cr The ContentResolver to use.
765 * @param faviconURL The URL of the favicon to fetch from the database.
766 * @return The decoded Bitmap from the database, if any. null if none is stored.
767 */
768 @Override
769 public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
770 Cursor c = null;
771 byte[] b = null;
773 try {
774 c = cr.query(mFaviconsUriWithProfile,
775 new String[] { Favicons.DATA },
776 Favicons.URL + " = ? AND " + Favicons.DATA + " IS NOT NULL",
777 new String[] { faviconURL },
778 null);
780 if (!c.moveToFirst()) {
781 return null;
782 }
784 final int faviconIndex = c.getColumnIndexOrThrow(Favicons.DATA);
785 b = c.getBlob(faviconIndex);
786 } finally {
787 if (c != null) {
788 c.close();
789 }
790 }
792 if (b == null) {
793 return null;
794 }
796 return FaviconDecoder.decodeFavicon(b);
797 }
799 @Override
800 public String getFaviconUrlForHistoryUrl(ContentResolver cr, String uri) {
801 Cursor c = null;
803 try {
804 c = cr.query(mHistoryUriWithProfile,
805 new String[] { History.FAVICON_URL },
806 Combined.URL + " = ?",
807 new String[] { uri },
808 null);
810 if (c.moveToFirst())
811 return c.getString(c.getColumnIndexOrThrow(History.FAVICON_URL));
812 } finally {
813 if (c != null)
814 c.close();
815 }
817 return null;
818 }
820 @Override
821 public void updateFaviconForUrl(ContentResolver cr, String pageUri,
822 byte[] encodedFavicon, String faviconUri) {
823 ContentValues values = new ContentValues();
824 values.put(Favicons.URL, faviconUri);
825 values.put(Favicons.PAGE_URL, pageUri);
826 values.put(Favicons.DATA, encodedFavicon);
828 // Update or insert
829 Uri faviconsUri = getAllFaviconsUri().buildUpon().
830 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
832 cr.update(faviconsUri,
833 values,
834 Favicons.URL + " = ?",
835 new String[] { faviconUri });
836 }
838 @Override
839 public void updateThumbnailForUrl(ContentResolver cr, String uri,
840 BitmapDrawable thumbnail) {
841 Bitmap bitmap = thumbnail.getBitmap();
843 byte[] data = null;
844 ByteArrayOutputStream stream = new ByteArrayOutputStream();
845 if (bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)) {
846 data = stream.toByteArray();
847 } else {
848 Log.w(LOGTAG, "Favicon compression failed.");
849 }
851 ContentValues values = new ContentValues();
852 values.put(Thumbnails.DATA, data);
853 values.put(Thumbnails.URL, uri);
855 Uri thumbnailsUri = mThumbnailsUriWithProfile.buildUpon().
856 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
857 cr.update(thumbnailsUri,
858 values,
859 Thumbnails.URL + " = ?",
860 new String[] { uri });
861 }
863 @Override
864 public byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
865 Cursor c = null;
866 byte[] b = null;
867 try {
868 c = cr.query(mThumbnailsUriWithProfile,
869 new String[]{ Thumbnails.DATA },
870 Thumbnails.URL + " = ? AND " + Thumbnails.DATA + " IS NOT NULL",
871 new String[]{ uri },
872 null);
874 if (!c.moveToFirst()) {
875 return null;
876 }
878 int thumbnailIndex = c.getColumnIndexOrThrow(Thumbnails.DATA);
879 b = c.getBlob(thumbnailIndex);
880 } finally {
881 if (c != null) {
882 c.close();
883 }
884 }
886 return b;
887 }
889 /**
890 * Query for non-null thumbnails matching the provided <code>urls</code>.
891 * The returned cursor will have no more than, but possibly fewer than,
892 * the requested number of thumbnails.
893 *
894 * Returns null if the provided list of URLs is empty or null.
895 */
896 @Override
897 public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
898 if (urls == null) {
899 return null;
900 }
902 int urlCount = urls.size();
903 if (urlCount == 0) {
904 return null;
905 }
907 // Don't match against null thumbnails.
908 StringBuilder selection = new StringBuilder(
909 Thumbnails.DATA + " IS NOT NULL AND " +
910 Thumbnails.URL + " IN ("
911 );
913 // Compute a (?, ?, ?) sequence to match the provided URLs.
914 int i = 1;
915 while (i++ < urlCount) {
916 selection.append("?, ");
917 }
918 selection.append("?)");
920 String[] selectionArgs = urls.toArray(new String[urlCount]);
922 return cr.query(mThumbnailsUriWithProfile,
923 new String[] { Thumbnails.URL, Thumbnails.DATA },
924 selection.toString(),
925 selectionArgs,
926 null);
927 }
929 @Override
930 public void removeThumbnails(ContentResolver cr) {
931 cr.delete(mThumbnailsUriWithProfile, null, null);
932 }
934 // Utility function for updating existing history using batch operations
935 public void updateHistoryInBatch(ContentResolver cr,
936 Collection<ContentProviderOperation> operations,
937 String url, String title,
938 long date, int visits) {
939 Cursor cursor = null;
941 try {
942 final String[] projection = new String[] {
943 History._ID,
944 History.VISITS,
945 History.DATE_LAST_VISITED
946 };
948 // We need to get the old visit count.
949 cursor = cr.query(getAllHistoryUri(),
950 projection,
951 History.URL + " = ?",
952 new String[] { url },
953 null);
955 ContentValues values = new ContentValues();
957 // Restore deleted record if possible
958 values.put(History.IS_DELETED, 0);
960 if (cursor.moveToFirst()) {
961 int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS);
962 int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED);
963 int oldVisits = cursor.getInt(visitsCol);
964 long oldDate = cursor.getLong(dateCol);
965 values.put(History.VISITS, oldVisits + visits);
966 // Only update last visited if newer.
967 if (date > oldDate) {
968 values.put(History.DATE_LAST_VISITED, date);
969 }
970 } else {
971 values.put(History.VISITS, visits);
972 values.put(History.DATE_LAST_VISITED, date);
973 }
974 if (title != null) {
975 values.put(History.TITLE, title);
976 }
977 values.put(History.URL, url);
979 Uri historyUri = getAllHistoryUri().buildUpon().
980 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
982 // Update or insert
983 ContentProviderOperation.Builder builder =
984 ContentProviderOperation.newUpdate(historyUri);
985 builder.withSelection(History.URL + " = ?", new String[] { url });
986 builder.withValues(values);
988 // Queue the operation
989 operations.add(builder.build());
990 } finally {
991 if (cursor != null)
992 cursor.close();
993 }
994 }
996 public void updateBookmarkInBatch(ContentResolver cr,
997 Collection<ContentProviderOperation> operations,
998 String url, String title, String guid,
999 long parent, long added,
1000 long modified, long position,
1001 String keyword, int type) {
1002 ContentValues values = new ContentValues();
1003 if (title == null && url != null) {
1004 title = url;
1005 }
1006 if (title != null) {
1007 values.put(Bookmarks.TITLE, title);
1008 }
1009 if (url != null) {
1010 values.put(Bookmarks.URL, url);
1011 }
1012 if (guid != null) {
1013 values.put(SyncColumns.GUID, guid);
1014 }
1015 if (keyword != null) {
1016 values.put(Bookmarks.KEYWORD, keyword);
1017 }
1018 if (added > 0) {
1019 values.put(SyncColumns.DATE_CREATED, added);
1020 }
1021 if (modified > 0) {
1022 values.put(SyncColumns.DATE_MODIFIED, modified);
1023 }
1024 values.put(Bookmarks.POSITION, position);
1025 // Restore deleted record if possible
1026 values.put(Bookmarks.IS_DELETED, 0);
1028 // This assumes no "real" folder has a negative ID. Only
1029 // things like the reading list folder do.
1030 if (parent < 0) {
1031 parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
1032 }
1033 values.put(Bookmarks.PARENT, parent);
1034 values.put(Bookmarks.TYPE, type);
1036 Uri bookmarkUri = getAllBookmarksUri().buildUpon().
1037 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
1038 // Update or insert
1039 ContentProviderOperation.Builder builder =
1040 ContentProviderOperation.newUpdate(bookmarkUri);
1041 if (url != null) {
1042 // Bookmarks are defined by their URL and Folder.
1043 builder.withSelection(Bookmarks.URL + " = ? AND "
1044 + Bookmarks.PARENT + " = ? AND "
1045 + Bookmarks.PARENT + " != ?",
1046 new String[] { url,
1047 Long.toString(parent),
1048 String.valueOf(Bookmarks.FIXED_READING_LIST_ID)
1049 });
1050 } else if (title != null) {
1051 // Or their title and parent folder. (Folders!)
1052 builder.withSelection(Bookmarks.TITLE + " = ? AND "
1053 + Bookmarks.PARENT + " = ? AND "
1054 + Bookmarks.PARENT + " != ?",
1055 new String[] { title,
1056 Long.toString(parent),
1057 String.valueOf(Bookmarks.FIXED_READING_LIST_ID)
1058 });
1059 } else if (type == Bookmarks.TYPE_SEPARATOR) {
1060 // Or their their position (seperators)
1061 builder.withSelection(Bookmarks.POSITION + " = ? AND "
1062 + Bookmarks.PARENT + " = ? AND "
1063 + Bookmarks.PARENT + " != ?",
1064 new String[] { Long.toString(position),
1065 Long.toString(parent),
1066 String.valueOf(Bookmarks.FIXED_READING_LIST_ID)
1067 });
1068 } else {
1069 Log.e(LOGTAG, "Bookmark entry without url or title and not a seperator, not added.");
1070 }
1071 builder.withValues(values);
1073 // Queue the operation
1074 operations.add(builder.build());
1075 }
1077 public void updateFaviconInBatch(ContentResolver cr,
1078 Collection<ContentProviderOperation> operations,
1079 String url, String faviconUrl,
1080 String faviconGuid, byte[] data) {
1081 ContentValues values = new ContentValues();
1082 values.put(Favicons.DATA, data);
1083 values.put(Favicons.PAGE_URL, url);
1084 if (faviconUrl != null) {
1085 values.put(Favicons.URL, faviconUrl);
1086 }
1088 // Update or insert
1089 Uri faviconsUri = getAllFaviconsUri().buildUpon().
1090 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
1091 // Update or insert
1092 ContentProviderOperation.Builder builder =
1093 ContentProviderOperation.newUpdate(faviconsUri);
1094 builder.withValues(values);
1095 builder.withSelection(Favicons.PAGE_URL + " = ?", new String[] { url });
1096 // Queue the operation
1097 operations.add(builder.build());
1098 }
1100 // This wrapper adds a fake "Desktop Bookmarks" folder entry to the
1101 // beginning of the cursor's data set.
1102 private class SpecialFoldersCursorWrapper extends CursorWrapper {
1103 private int mIndexOffset;
1105 private int mDesktopBookmarksIndex = -1;
1107 private boolean mAtDesktopBookmarksPosition = false;
1109 public SpecialFoldersCursorWrapper(Cursor c, boolean showDesktopBookmarks) {
1110 super(c);
1112 mIndexOffset = 0;
1114 if (showDesktopBookmarks) {
1115 mDesktopBookmarksIndex = mIndexOffset;
1116 mIndexOffset++;
1117 }
1118 }
1120 @Override
1121 public int getCount() {
1122 return super.getCount() + mIndexOffset;
1123 }
1125 @Override
1126 public boolean moveToPosition(int position) {
1127 mAtDesktopBookmarksPosition = (mDesktopBookmarksIndex == position);
1129 if (mAtDesktopBookmarksPosition)
1130 return true;
1132 return super.moveToPosition(position - mIndexOffset);
1133 }
1135 @Override
1136 public long getLong(int columnIndex) {
1137 if (!mAtDesktopBookmarksPosition)
1138 return super.getLong(columnIndex);
1140 if (columnIndex == getColumnIndex(Bookmarks.PARENT)) {
1141 return Bookmarks.FIXED_ROOT_ID;
1142 }
1144 return -1;
1145 }
1147 @Override
1148 public int getInt(int columnIndex) {
1149 if (!mAtDesktopBookmarksPosition)
1150 return super.getInt(columnIndex);
1152 if (columnIndex == getColumnIndex(Bookmarks._ID) && mAtDesktopBookmarksPosition)
1153 return Bookmarks.FAKE_DESKTOP_FOLDER_ID;
1155 if (columnIndex == getColumnIndex(Bookmarks.TYPE))
1156 return Bookmarks.TYPE_FOLDER;
1158 return -1;
1159 }
1161 @Override
1162 public String getString(int columnIndex) {
1163 if (!mAtDesktopBookmarksPosition)
1164 return super.getString(columnIndex);
1166 if (columnIndex == getColumnIndex(Bookmarks.GUID) && mAtDesktopBookmarksPosition)
1167 return Bookmarks.FAKE_DESKTOP_FOLDER_GUID;
1169 return "";
1170 }
1171 }
1173 private static class LocalDBCursor extends CursorWrapper {
1174 public LocalDBCursor(Cursor c) {
1175 super(c);
1176 }
1178 private String translateColumnName(String columnName) {
1179 if (columnName.equals(BrowserDB.URLColumns.URL)) {
1180 columnName = URLColumns.URL;
1181 } else if (columnName.equals(BrowserDB.URLColumns.TITLE)) {
1182 columnName = URLColumns.TITLE;
1183 } else if (columnName.equals(BrowserDB.URLColumns.FAVICON)) {
1184 columnName = FaviconColumns.FAVICON;
1185 } else if (columnName.equals(BrowserDB.URLColumns.DATE_LAST_VISITED)) {
1186 columnName = History.DATE_LAST_VISITED;
1187 } else if (columnName.equals(BrowserDB.URLColumns.VISITS)) {
1188 columnName = History.VISITS;
1189 }
1191 return columnName;
1192 }
1194 @Override
1195 public int getColumnIndex(String columnName) {
1196 return super.getColumnIndex(translateColumnName(columnName));
1197 }
1199 @Override
1200 public int getColumnIndexOrThrow(String columnName) {
1201 return super.getColumnIndexOrThrow(translateColumnName(columnName));
1202 }
1203 }
1206 @Override
1207 public void pinSite(ContentResolver cr, String url, String title, int position) {
1208 ContentValues values = new ContentValues();
1209 final long now = System.currentTimeMillis();
1210 values.put(Bookmarks.TITLE, title);
1211 values.put(Bookmarks.URL, url);
1212 values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
1213 values.put(Bookmarks.DATE_MODIFIED, now);
1214 values.put(Bookmarks.POSITION, position);
1215 values.put(Bookmarks.IS_DELETED, 0);
1217 // We do an update-and-replace here without deleting any existing pins for the given URL.
1218 // That means if the user pins a URL, then edits another thumbnail to use the same URL,
1219 // we'll end up with two pins for that site. This is the intended behavior, which
1220 // incidentally saves us a delete query.
1221 Uri uri = mBookmarksUriWithProfile.buildUpon()
1222 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
1223 cr.update(uri,
1224 values,
1225 Bookmarks.POSITION + " = ? AND " +
1226 Bookmarks.PARENT + " = ?",
1227 new String[] { Integer.toString(position),
1228 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
1229 }
1231 @Override
1232 public Cursor getPinnedSites(ContentResolver cr, int limit) {
1233 return cr.query(bookmarksUriWithLimit(limit),
1234 new String[] { Bookmarks._ID,
1235 Bookmarks.URL,
1236 Bookmarks.TITLE,
1237 Bookmarks.POSITION },
1238 Bookmarks.PARENT + " == ?",
1239 new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) },
1240 Bookmarks.POSITION + " ASC");
1241 }
1243 @Override
1244 public void unpinSite(ContentResolver cr, int position) {
1245 cr.delete(mBookmarksUriWithProfile,
1246 Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?",
1247 new String[] {
1248 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
1249 Integer.toString(position)
1250 });
1251 }
1253 @Override
1254 public void unpinAllSites(ContentResolver cr) {
1255 cr.delete(mBookmarksUriWithProfile,
1256 Bookmarks.PARENT + " == ?",
1257 new String[] {
1258 String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID)
1259 });
1260 }
1262 @Override
1263 public boolean isVisited(ContentResolver cr, String uri) {
1264 int count = 0;
1265 Cursor c = null;
1267 try {
1268 c = cr.query(historyUriWithLimit(1),
1269 new String[] { History._ID },
1270 History.URL + " = ?",
1271 new String[] { uri },
1272 History.URL);
1273 count = c.getCount();
1274 } catch (NullPointerException e) {
1275 Log.e(LOGTAG, "NullPointerException in isVisited");
1276 } finally {
1277 if (c != null)
1278 c.close();
1279 }
1281 return (count > 0);
1282 }
1284 public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
1285 Cursor c = cr.query(bookmarksUriWithLimit(1),
1286 new String[] { Bookmarks._ID,
1287 Bookmarks.URL,
1288 Bookmarks.TITLE,
1289 Bookmarks.KEYWORD },
1290 Bookmarks.URL + " = ?",
1291 new String[] { url },
1292 null);
1294 if (c != null && c.getCount() == 0) {
1295 c.close();
1296 c = null;
1297 }
1299 return c;
1300 }
1301 }