/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.db; import org.mozilla.gecko.db.BrowserContract.Bookmarks; import org.mozilla.gecko.db.BrowserContract.Combined; import org.mozilla.gecko.db.BrowserContract.ExpirePriority; import org.mozilla.gecko.db.BrowserContract.FaviconColumns; import org.mozilla.gecko.db.BrowserContract.Favicons; import org.mozilla.gecko.db.BrowserContract.History; import org.mozilla.gecko.db.BrowserContract.SyncColumns; import org.mozilla.gecko.db.BrowserContract.Thumbnails; import org.mozilla.gecko.db.BrowserContract.URLColumns; import org.mozilla.gecko.gfx.BitmapUtils; import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentValues; import android.database.ContentObserver; import android.database.Cursor; import android.database.CursorWrapper; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.provider.Browser; import android.text.TextUtils; import android.util.Log; import java.io.ByteArrayOutputStream; import java.util.Collection; import java.util.HashMap; import java.util.List; public class LocalBrowserDB implements BrowserDB.BrowserDBIface { // Calculate these once, at initialization. isLoggable is too expensive to // have in-line in each log call. private static final String LOGTAG = "GeckoLocalBrowserDB"; private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); protected static void debug(String message) { if (logDebug) { Log.d(LOGTAG, message); } } private final String mProfile; // Map of folder GUIDs to IDs. Used for caching. private HashMap mFolderIdMap; // Use wrapped Boolean so that we can have a null state private Boolean mDesktopBookmarksExist; private Boolean mReadingListItemsExist; private final Uri mBookmarksUriWithProfile; private final Uri mParentsUriWithProfile; private final Uri mHistoryUriWithProfile; private final Uri mHistoryExpireUriWithProfile; private final Uri mCombinedUriWithProfile; private final Uri mDeletedHistoryUriWithProfile; private final Uri mUpdateHistoryUriWithProfile; private final Uri mFaviconsUriWithProfile; private final Uri mThumbnailsUriWithProfile; private static final String[] DEFAULT_BOOKMARK_COLUMNS = new String[] { Bookmarks._ID, Bookmarks.GUID, Bookmarks.URL, Bookmarks.TITLE, Bookmarks.TYPE, Bookmarks.PARENT, Bookmarks.KEYWORD, Bookmarks.FAVICON }; public LocalBrowserDB(String profile) { mProfile = profile; mFolderIdMap = new HashMap(); mDesktopBookmarksExist = null; mReadingListItemsExist = null; mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI); mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI); mHistoryUriWithProfile = appendProfile(History.CONTENT_URI); mHistoryExpireUriWithProfile = appendProfile(History.CONTENT_OLD_URI); mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI); mFaviconsUriWithProfile = appendProfile(Favicons.CONTENT_URI); mThumbnailsUriWithProfile = appendProfile(Thumbnails.CONTENT_URI); mDeletedHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon(). appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1").build(); mUpdateHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon(). appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true"). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); } // Invalidate cached data @Override public void invalidateCachedState() { mDesktopBookmarksExist = null; mReadingListItemsExist = null; } private Uri historyUriWithLimit(int limit) { return mHistoryUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)).build(); } private Uri bookmarksUriWithLimit(int limit) { return mBookmarksUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)).build(); } private Uri combinedUriWithLimit(int limit) { return mCombinedUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit)).build(); } private Uri appendProfile(Uri uri) { return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).build(); } private Uri getAllBookmarksUri() { Uri.Builder uriBuilder = mBookmarksUriWithProfile.buildUpon() .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); return uriBuilder.build(); } private Uri getAllHistoryUri() { Uri.Builder uriBuilder = mHistoryUriWithProfile.buildUpon() .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); return uriBuilder.build(); } private Uri getAllFaviconsUri() { Uri.Builder uriBuilder = mFaviconsUriWithProfile.buildUpon() .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1"); return uriBuilder.build(); } private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint, int limit, CharSequence urlFilter) { return filterAllSites(cr, projection, constraint, limit, urlFilter, "", null); } private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint, int limit, CharSequence urlFilter, String selection, String[] selectionArgs) { // The combined history/bookmarks selection queries for sites with a url or title containing // the constraint string(s), treating space-separated words as separate constraints if (!TextUtils.isEmpty(constraint)) { String[] constraintWords = constraint.toString().split(" "); for (int i = 0; i < constraintWords.length; i++) { selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " + Combined.TITLE + " LIKE ?)"); String constraintWord = "%" + constraintWords[i] + "%"; selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { constraintWord, constraintWord }); } } if (urlFilter != null) { selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)"); selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() }); } // Our version of frecency is computed by scaling the number of visits by a multiplier // that approximates Gaussian decay, based on how long ago the entry was last visited. // Since we're limited by the math we can do with sqlite, we're calculating this // approximation using the Cauchy distribution: multiplier = 15^2 / (age^2 + 15^2). // Using 15 as our scale parameter, we get a constant 15^2 = 225. Following this math, // frecencyScore = numVisits * max(1, 100 * 225 / (age*age + 225)). (See bug 704977) // We also give bookmarks an extra bonus boost by adding 100 points to their frecency score. final String sortOrder = BrowserContract.getFrecencySortOrder(true, false); Cursor c = cr.query(combinedUriWithLimit(limit), projection, selection, selectionArgs, sortOrder); return new LocalDBCursor(c); } @Override public int getCount(ContentResolver cr, String database) { Cursor cursor = null; int count = 0; String[] columns = null; String constraint = null; try { Uri uri = null; if ("history".equals(database)) { uri = mHistoryUriWithProfile; columns = new String[] { History._ID }; constraint = Combined.VISITS + " > 0"; } else if ("bookmarks".equals(database)) { uri = mBookmarksUriWithProfile; columns = new String[] { Bookmarks._ID }; // ignore folders, tags, keywords, separators, etc. constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK; } else if ("thumbnails".equals(database)) { uri = mThumbnailsUriWithProfile; columns = new String[] { Thumbnails._ID }; } else if ("favicons".equals(database)) { uri = mFaviconsUriWithProfile; columns = new String[] { Favicons._ID }; } if (uri != null) { cursor = cr.query(uri, columns, constraint, null, null); count = cursor.getCount(); } } finally { if (cursor != null) cursor.close(); } debug("Got count " + count + " for " + database); return count; } @Override public Cursor filter(ContentResolver cr, CharSequence constraint, int limit) { return filterAllSites(cr, new String[] { Combined._ID, Combined.URL, Combined.TITLE, Combined.DISPLAY, Combined.BOOKMARK_ID, Combined.HISTORY_ID }, constraint, limit, null); } @Override public Cursor getTopSites(ContentResolver cr, int limit) { // Filter out sites that are pinned String selection = DBUtils.concatenateWhere("", Combined.URL + " NOT IN (SELECT " + Bookmarks.URL + " FROM bookmarks WHERE " + DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " == ? AND " + DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)"); String[] selectionArgs = DBUtils.appendSelectionArgs(new String[0], new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }); return filterAllSites(cr, new String[] { Combined._ID, Combined.URL, Combined.TITLE }, "", limit, BrowserDB.ABOUT_PAGES_URL_FILTER, selection, selectionArgs); } @Override public void updateVisitedHistory(ContentResolver cr, String uri) { ContentValues values = new ContentValues(); values.put(History.URL, uri); values.put(History.DATE_LAST_VISITED, System.currentTimeMillis()); values.put(History.IS_DELETED, 0); // This will insert a new history entry if one for this URL // doesn't already exist cr.update(mUpdateHistoryUriWithProfile, values, History.URL + " = ?", new String[] { uri }); } @Override public void updateHistoryTitle(ContentResolver cr, String uri, String title) { ContentValues values = new ContentValues(); values.put(History.TITLE, title); cr.update(mHistoryUriWithProfile, values, History.URL + " = ?", new String[] { uri }); } @Override public void updateHistoryEntry(ContentResolver cr, String uri, String title, long date, int visits) { int oldVisits = 0; Cursor cursor = null; try { cursor = cr.query(mHistoryUriWithProfile, new String[] { History.VISITS }, History.URL + " = ?", new String[] { uri }, null); if (cursor.moveToFirst()) { oldVisits = cursor.getInt(0); } } finally { if (cursor != null) cursor.close(); } ContentValues values = new ContentValues(); values.put(History.DATE_LAST_VISITED, date); values.put(History.VISITS, oldVisits + visits); if (title != null) { values.put(History.TITLE, title); } cr.update(mHistoryUriWithProfile, values, History.URL + " = ?", new String[] { uri }); } @Override public Cursor getAllVisitedHistory(ContentResolver cr) { Cursor c = cr.query(mHistoryUriWithProfile, new String[] { History.URL }, History.VISITS + " > 0", null, null); return new LocalDBCursor(c); } @Override public Cursor getRecentHistory(ContentResolver cr, int limit) { Cursor c = cr.query(combinedUriWithLimit(limit), new String[] { Combined._ID, Combined.BOOKMARK_ID, Combined.HISTORY_ID, Combined.URL, Combined.TITLE, Combined.FAVICON, Combined.DISPLAY, Combined.DATE_LAST_VISITED, Combined.VISITS }, History.DATE_LAST_VISITED + " > 0", null, History.DATE_LAST_VISITED + " DESC"); return new LocalDBCursor(c); } @Override public void expireHistory(ContentResolver cr, ExpirePriority priority) { Uri url = mHistoryExpireUriWithProfile; url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build(); cr.delete(url, null, null); } @Override public void removeHistoryEntry(ContentResolver cr, int id) { cr.delete(mHistoryUriWithProfile, History._ID + " = ?", new String[] { String.valueOf(id) }); } @Override public void removeHistoryEntry(ContentResolver cr, String url) { int deleted = cr.delete(mHistoryUriWithProfile, History.URL + " = ?", new String[] { url }); } @Override public void clearHistory(ContentResolver cr) { cr.delete(mHistoryUriWithProfile, null, null); } @Override public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) { Cursor c = null; boolean addDesktopFolder = false; boolean addReadingListFolder = false; // We always want to show mobile bookmarks in the root view. if (folderId == Bookmarks.FIXED_ROOT_ID) { folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); // We'll add a fake "Desktop Bookmarks" folder to the root view if desktop // bookmarks exist, so that the user can still access non-mobile bookmarks. addDesktopFolder = desktopBookmarksExist(cr); // We'll add the Reading List folder to the root view if any reading // list items exist. addReadingListFolder = readingListItemsExist(cr); } if (folderId == Bookmarks.FAKE_DESKTOP_FOLDER_ID) { // Since the "Desktop Bookmarks" folder doesn't actually exist, we // just fake it by querying specifically certain known desktop folders. c = cr.query(mBookmarksUriWithProfile, DEFAULT_BOOKMARK_COLUMNS, Bookmarks.GUID + " = ? OR " + Bookmarks.GUID + " = ? OR " + Bookmarks.GUID + " = ?", new String[] { Bookmarks.TOOLBAR_FOLDER_GUID, Bookmarks.MENU_FOLDER_GUID, Bookmarks.UNFILED_FOLDER_GUID }, null); } else { // Right now, we only support showing folder and bookmark type of // entries. We should add support for other types though (bug 737024) c = cr.query(mBookmarksUriWithProfile, DEFAULT_BOOKMARK_COLUMNS, Bookmarks.PARENT + " = ? AND " + "(" + Bookmarks.TYPE + " = ? OR " + Bookmarks.TYPE + " = ?)", new String[] { String.valueOf(folderId), String.valueOf(Bookmarks.TYPE_BOOKMARK), String.valueOf(Bookmarks.TYPE_FOLDER) }, null); } if (addDesktopFolder || addReadingListFolder) { // Wrap cursor to add fake desktop bookmarks and reading list folders c = new SpecialFoldersCursorWrapper(c, addDesktopFolder, addReadingListFolder); } return new LocalDBCursor(c); } // Returns true if any desktop bookmarks exist, which will be true if the user // has set up sync at one point, or done a profile migration from XUL fennec. private boolean desktopBookmarksExist(ContentResolver cr) { if (mDesktopBookmarksExist != null) return mDesktopBookmarksExist; Cursor c = null; int count = 0; try { // Check to see if there are any bookmarks in one of our three // fixed "Desktop Boomarks" folders. c = cr.query(bookmarksUriWithLimit(1), new String[] { Bookmarks._ID }, Bookmarks.PARENT + " = ? OR " + Bookmarks.PARENT + " = ? OR " + Bookmarks.PARENT + " = ?", new String[] { String.valueOf(getFolderIdFromGuid(cr, Bookmarks.TOOLBAR_FOLDER_GUID)), String.valueOf(getFolderIdFromGuid(cr, Bookmarks.MENU_FOLDER_GUID)), String.valueOf(getFolderIdFromGuid(cr, Bookmarks.UNFILED_FOLDER_GUID)) }, null); count = c.getCount(); } finally { c.close(); } // Cache result for future queries mDesktopBookmarksExist = (count > 0); return mDesktopBookmarksExist; } private boolean readingListItemsExist(ContentResolver cr) { if (mReadingListItemsExist != null) return mReadingListItemsExist; Cursor c = null; int count = 0; try { c = cr.query(bookmarksUriWithLimit(1), new String[] { Bookmarks._ID }, Bookmarks.PARENT + " = ?", new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }, null); count = c.getCount(); } finally { c.close(); } // Cache result for future queries mReadingListItemsExist = (count > 0); return mReadingListItemsExist; } @Override public int getReadingListCount(ContentResolver cr) { // This method is about the Reading List, not normal bookmarks Cursor c = null; int count = 0; try { c = cr.query(mBookmarksUriWithProfile, new String[] { Bookmarks._ID }, Bookmarks.PARENT + " = ?", new String[] { String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }, null); count = c.getCount(); } finally { c.close(); } return count; } @Override public boolean isBookmark(ContentResolver cr, String uri) { // This method is about normal bookmarks, not the Reading List int count = 0; try { Cursor c = cr.query(bookmarksUriWithLimit(1), new String[] { Bookmarks._ID }, Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? AND " + Bookmarks.PARENT + " != ?", new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }, Bookmarks.URL); count = c.getCount(); c.close(); } catch (NullPointerException e) { Log.e(LOGTAG, "NullPointerException in isBookmark"); } return (count > 0); } @Override public boolean isReadingListItem(ContentResolver cr, String uri) { int count = 0; try { Cursor c = cr.query(mBookmarksUriWithProfile, new String[] { Bookmarks._ID }, Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " == ?", new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }, Bookmarks.URL); count = c.getCount(); c.close(); } catch (NullPointerException e) { Log.e(LOGTAG, "NullPointerException in isReadingListItem"); } return (count > 0); } @Override public String getUrlForKeyword(ContentResolver cr, String keyword) { Cursor cursor = cr.query(mBookmarksUriWithProfile, new String[] { Bookmarks.URL }, Bookmarks.KEYWORD + " = ?", new String[] { keyword }, null); if (!cursor.moveToFirst()) { cursor.close(); return null; } String url = cursor.getString(cursor.getColumnIndexOrThrow(Bookmarks.URL)); cursor.close(); return url; } private synchronized long getFolderIdFromGuid(ContentResolver cr, String guid) { if (mFolderIdMap.containsKey(guid)) return mFolderIdMap.get(guid); long folderId = -1; Cursor c = null; try { c = cr.query(mBookmarksUriWithProfile, new String[] { Bookmarks._ID }, Bookmarks.GUID + " = ?", new String[] { guid }, null); if (c.moveToFirst()) folderId = c.getLong(c.getColumnIndexOrThrow(Bookmarks._ID)); } finally { if (c != null) c.close(); } mFolderIdMap.put(guid, folderId); return folderId; } /** * Find parents of records that match the provided criteria, and bump their * modified timestamp. */ protected void bumpParents(ContentResolver cr, String param, String value) { ContentValues values = new ContentValues(); values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); String where = param + " = ?"; String[] args = new String[] { value }; int updated = cr.update(mParentsUriWithProfile, values, where, args); debug("Updated " + updated + " rows to new modified time."); } private void addBookmarkItem(ContentResolver cr, String title, String uri, long folderId) { final long now = System.currentTimeMillis(); ContentValues values = new ContentValues(); values.put(Browser.BookmarkColumns.TITLE, title); values.put(Bookmarks.URL, uri); values.put(Bookmarks.PARENT, folderId); values.put(Bookmarks.DATE_MODIFIED, now); // Get the page's favicon ID from the history table Cursor c = cr.query(mHistoryUriWithProfile, new String[] { History.FAVICON_ID }, History.URL + " = ?", new String[] { uri }, null); if (c.moveToFirst()) { int columnIndex = c.getColumnIndexOrThrow(History.FAVICON_ID); if (!c.isNull(columnIndex)) values.put(Bookmarks.FAVICON_ID, c.getLong(columnIndex)); } c.close(); // Restore deleted record if possible values.put(Bookmarks.IS_DELETED, 0); int updated = cr.update(mBookmarksUriWithProfile, values, Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ?", new String[] { uri, String.valueOf(folderId) }); if (updated == 0) cr.insert(mBookmarksUriWithProfile, values); // Bump parent modified time using its ID. debug("Bumping parent modified time for addition to: " + folderId); final String where = Bookmarks._ID + " = ?"; final String[] args = new String[] { String.valueOf(folderId) }; ContentValues bumped = new ContentValues(); bumped.put(Bookmarks.DATE_MODIFIED, now); updated = cr.update(mBookmarksUriWithProfile, bumped, where, args); debug("Updated " + updated + " rows to new modified time."); } @Override public void addBookmark(ContentResolver cr, String title, String uri) { long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); addBookmarkItem(cr, title, uri, folderId); } @Override public void removeBookmark(ContentResolver cr, int id) { Uri contentUri = mBookmarksUriWithProfile; // Do this now so that the item still exists! final String idString = String.valueOf(id); bumpParents(cr, Bookmarks._ID, idString); final String[] idArgs = new String[] { idString }; final String idEquals = Bookmarks._ID + " = ?"; cr.delete(contentUri, idEquals, idArgs); } @Override public void removeBookmarksWithURL(ContentResolver cr, String uri) { Uri contentUri = mBookmarksUriWithProfile; // Do this now so that the items still exist! bumpParents(cr, Bookmarks.URL, uri); // Toggling bookmark on an URL should not affect the items in the reading list final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }; final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ?"; cr.delete(contentUri, urlEquals, urlArgs); } @Override public void addReadingListItem(ContentResolver cr, String title, String uri) { addBookmarkItem(cr, title, uri, Bookmarks.FIXED_READING_LIST_ID); } @Override public void removeReadingListItemWithURL(ContentResolver cr, String uri) { Uri contentUri = mBookmarksUriWithProfile; // Do this now so that the items still exist! bumpParents(cr, Bookmarks.URL, uri); final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }; final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " == ?"; cr.delete(contentUri, urlEquals, urlArgs); } @Override public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) { cr.registerContentObserver(mBookmarksUriWithProfile, false, observer); } @Override public void registerHistoryObserver(ContentResolver cr, ContentObserver observer) { cr.registerContentObserver(mHistoryUriWithProfile, false, observer); } @Override public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) { ContentValues values = new ContentValues(); values.put(Browser.BookmarkColumns.TITLE, title); values.put(Bookmarks.URL, uri); values.put(Bookmarks.KEYWORD, keyword); values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); cr.update(mBookmarksUriWithProfile, values, Bookmarks._ID + " = ?", new String[] { String.valueOf(id) }); } @Override public Bitmap getFaviconForUrl(ContentResolver cr, String uri) { Cursor c = cr.query(mCombinedUriWithProfile, new String[] { Combined.FAVICON }, Combined.URL + " = ?", new String[] { uri }, null); if (!c.moveToFirst()) { c.close(); return null; } int faviconIndex = c.getColumnIndexOrThrow(Combined.FAVICON); byte[] b = c.getBlob(faviconIndex); c.close(); if (b == null || b.length == 0) return null; return BitmapUtils.decodeByteArray(b); } @Override public String getFaviconUrlForHistoryUrl(ContentResolver cr, String uri) { Cursor c = cr.query(mHistoryUriWithProfile, new String[] { History.FAVICON_URL }, Combined.URL + " = ?", new String[] { uri }, null); if (!c.moveToFirst()) { c.close(); return null; } String faviconUrl = c.getString(c.getColumnIndexOrThrow(History.FAVICON_URL)); c.close(); return faviconUrl; } @Override public Cursor getFaviconsForUrls(ContentResolver cr, List urls) { StringBuffer selection = new StringBuffer(); String[] selectionArgs = new String[urls.size()]; for (int i = 0; i < urls.size(); i++) { final String url = urls.get(i); if (i > 0) selection.append(" OR "); selection.append(Favicons.URL + " = ?"); selectionArgs[i] = url; } return cr.query(mCombinedUriWithProfile, new String[] { Combined.URL, Combined.FAVICON }, selection.toString(), selectionArgs, null); } @Override public void updateFaviconForUrl(ContentResolver cr, String pageUri, Bitmap favicon, String faviconUri) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); favicon.compress(Bitmap.CompressFormat.PNG, 100, stream); ContentValues values = new ContentValues(); values.put(Favicons.URL, faviconUri); values.put(Favicons.DATA, stream.toByteArray()); values.put(Favicons.PAGE_URL, pageUri); // Update or insert Uri faviconsUri = getAllFaviconsUri().buildUpon(). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); int updated = cr.update(faviconsUri, values, Favicons.URL + " = ?", new String[] { faviconUri }); if (updated == 0) cr.insert(mFaviconsUriWithProfile, values); } @Override public void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail) { Bitmap bitmap = thumbnail.getBitmap(); ByteArrayOutputStream stream = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream); ContentValues values = new ContentValues(); values.put(Thumbnails.DATA, stream.toByteArray()); values.put(Thumbnails.URL, uri); int updated = cr.update(mThumbnailsUriWithProfile, values, Thumbnails.URL + " = ?", new String[] { uri }); if (updated == 0) cr.insert(mThumbnailsUriWithProfile, values); } @Override public byte[] getThumbnailForUrl(ContentResolver cr, String uri) { Cursor c = cr.query(mThumbnailsUriWithProfile, new String[] { Thumbnails.DATA }, Thumbnails.URL + " = ?", new String[] { uri }, null); if (!c.moveToFirst()) { c.close(); return null; } int thumbnailIndex = c.getColumnIndexOrThrow(Thumbnails.DATA); byte[] b = c.getBlob(thumbnailIndex); c.close(); return b; } @Override public Cursor getThumbnailsForUrls(ContentResolver cr, List urls) { StringBuffer selection = new StringBuffer(); String[] selectionArgs = new String[urls.size()]; for (int i = 0; i < urls.size(); i++) { final String url = urls.get(i); if (i > 0) selection.append(" OR "); selection.append(Thumbnails.URL + " = ?"); selectionArgs[i] = url; } return cr.query(mThumbnailsUriWithProfile, new String[] { Thumbnails.URL, Thumbnails.DATA }, selection.toString(), selectionArgs, null); } @Override public void removeThumbnails(ContentResolver cr) { cr.delete(mThumbnailsUriWithProfile, null, null); } // Utility function for updating existing history using batch operations public void updateHistoryInBatch(ContentResolver cr, Collection operations, String url, String title, long date, int visits) { Cursor cursor = null; try { final String[] projection = new String[] { History._ID, History.VISITS, History.DATE_LAST_VISITED }; // We need to get the old visit count. cursor = cr.query(getAllHistoryUri(), projection, History.URL + " = ?", new String[] { url }, null); ContentValues values = new ContentValues(); // Restore deleted record if possible values.put(History.IS_DELETED, 0); if (cursor.moveToFirst()) { int visitsCol = cursor.getColumnIndexOrThrow(History.VISITS); int dateCol = cursor.getColumnIndexOrThrow(History.DATE_LAST_VISITED); int oldVisits = cursor.getInt(visitsCol); long oldDate = cursor.getLong(dateCol); values.put(History.VISITS, oldVisits + visits); // Only update last visited if newer. if (date > oldDate) { values.put(History.DATE_LAST_VISITED, date); } } else { values.put(History.VISITS, visits); values.put(History.DATE_LAST_VISITED, date); } if (title != null) { values.put(History.TITLE, title); } values.put(History.URL, url); Uri historyUri = getAllHistoryUri().buildUpon(). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); // Update or insert ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(historyUri); builder.withSelection(History.URL + " = ?", new String[] { url }); builder.withValues(values); // Queue the operation operations.add(builder.build()); } finally { if (cursor != null) cursor.close(); } } public void updateBookmarkInBatch(ContentResolver cr, Collection operations, String url, String title, String guid, long parent, long added, long modified, long position, String keyword, int type) { ContentValues values = new ContentValues(); if (title == null && url != null) { title = url; } if (title != null) { values.put(Bookmarks.TITLE, title); } if (url != null) { values.put(Bookmarks.URL, url); } if (guid != null) { values.put(SyncColumns.GUID, guid); } if (keyword != null) { values.put(Bookmarks.KEYWORD, keyword); } if (added > 0) { values.put(SyncColumns.DATE_CREATED, added); } if (modified > 0) { values.put(SyncColumns.DATE_MODIFIED, modified); } values.put(Bookmarks.POSITION, position); // Restore deleted record if possible values.put(Bookmarks.IS_DELETED, 0); // This assumes no "real" folder has a negative ID. Only // things like the reading list folder do. if (parent < 0) { parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID); } values.put(Bookmarks.PARENT, parent); values.put(Bookmarks.TYPE, type); Uri bookmarkUri = getAllBookmarksUri().buildUpon(). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); // Update or insert ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(bookmarkUri); if (url != null) { // Bookmarks are defined by their URL and Folder. builder.withSelection(Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.PARENT + " != ?", new String[] { url, Long.toString(parent), String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); } else if (title != null) { // Or their title and parent folder. (Folders!) builder.withSelection(Bookmarks.TITLE + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.PARENT + " != ?", new String[] { title, Long.toString(parent), String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); } else if (type == Bookmarks.TYPE_SEPARATOR) { // Or their their position (seperators) builder.withSelection(Bookmarks.POSITION + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.PARENT + " != ?", new String[] { Long.toString(position), Long.toString(parent), String.valueOf(Bookmarks.FIXED_READING_LIST_ID) }); } else { Log.e(LOGTAG, "Bookmark entry without url or title and not a seperator, not added."); } builder.withValues(values); // Queue the operation operations.add(builder.build()); } public void updateFaviconInBatch(ContentResolver cr, Collection operations, String url, String faviconUrl, String faviconGuid, byte[] data) { ContentValues values = new ContentValues(); values.put(Favicons.DATA, data); values.put(Favicons.PAGE_URL, url); if (faviconUrl != null) { values.put(Favicons.URL, faviconUrl); } // Update or insert Uri faviconsUri = getAllFaviconsUri().buildUpon(). appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build(); // Update or insert ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(faviconsUri); builder.withValues(values); builder.withSelection(Favicons.PAGE_URL + " = ?", new String[] { url }); // Queue the operation operations.add(builder.build()); } // This wrapper adds a fake "Desktop Bookmarks" folder entry to the // beginning of the cursor's data set. private class SpecialFoldersCursorWrapper extends CursorWrapper { private int mIndexOffset; private int mDesktopBookmarksIndex = -1; private int mReadingListIndex = -1; private boolean mAtDesktopBookmarksPosition = false; private boolean mAtReadingListPosition = false; public SpecialFoldersCursorWrapper(Cursor c, boolean showDesktopBookmarks, boolean showReadingList) { super(c); mIndexOffset = 0; if (showDesktopBookmarks) { mDesktopBookmarksIndex = mIndexOffset; mIndexOffset++; } if (showReadingList) { mReadingListIndex = mIndexOffset; mIndexOffset++; } } @Override public int getCount() { return super.getCount() + mIndexOffset; } @Override public boolean moveToPosition(int position) { mAtDesktopBookmarksPosition = (mDesktopBookmarksIndex == position); mAtReadingListPosition = (mReadingListIndex == position); if (mAtDesktopBookmarksPosition || mAtReadingListPosition) return true; return super.moveToPosition(position - mIndexOffset); } @Override public long getLong(int columnIndex) { if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) return super.getLong(columnIndex); if (columnIndex == getColumnIndex(Bookmarks.PARENT)) { return Bookmarks.FIXED_ROOT_ID; } return -1; } @Override public int getInt(int columnIndex) { if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) return super.getInt(columnIndex); if (columnIndex == getColumnIndex(Bookmarks._ID)) { if (mAtDesktopBookmarksPosition) { return Bookmarks.FAKE_DESKTOP_FOLDER_ID; } else if (mAtReadingListPosition) { return Bookmarks.FIXED_READING_LIST_ID; } } if (columnIndex == getColumnIndex(Bookmarks.TYPE)) return Bookmarks.TYPE_FOLDER; return -1; } @Override public String getString(int columnIndex) { if (!mAtDesktopBookmarksPosition && !mAtReadingListPosition) return super.getString(columnIndex); if (columnIndex == getColumnIndex(Bookmarks.GUID)) { if (mAtDesktopBookmarksPosition) { return Bookmarks.FAKE_DESKTOP_FOLDER_GUID; } else if (mAtReadingListPosition) { return Bookmarks.READING_LIST_FOLDER_GUID; } } return ""; } } private static class LocalDBCursor extends CursorWrapper { public LocalDBCursor(Cursor c) { super(c); } private String translateColumnName(String columnName) { if (columnName.equals(BrowserDB.URLColumns.URL)) { columnName = URLColumns.URL; } else if (columnName.equals(BrowserDB.URLColumns.TITLE)) { columnName = URLColumns.TITLE; } else if (columnName.equals(BrowserDB.URLColumns.FAVICON)) { columnName = FaviconColumns.FAVICON; } else if (columnName.equals(BrowserDB.URLColumns.DATE_LAST_VISITED)) { columnName = History.DATE_LAST_VISITED; } else if (columnName.equals(BrowserDB.URLColumns.VISITS)) { columnName = History.VISITS; } return columnName; } @Override public int getColumnIndex(String columnName) { return super.getColumnIndex(translateColumnName(columnName)); } @Override public int getColumnIndexOrThrow(String columnName) { return super.getColumnIndexOrThrow(translateColumnName(columnName)); } } @Override public void pinSite(ContentResolver cr, String url, String title, int position) { ContentValues values = new ContentValues(); final long now = System.currentTimeMillis(); values.put(Bookmarks.TITLE, title); values.put(Bookmarks.URL, url); values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID); values.put(Bookmarks.DATE_MODIFIED, now); values.put(Bookmarks.POSITION, position); values.put(Bookmarks.IS_DELETED, 0); // If this site is already pinned, unpin it cr.delete(mBookmarksUriWithProfile, Bookmarks.PARENT + " == ? AND " + Bookmarks.URL + " == ?", new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID), url }); // If something is already pinned in this spot update it int updated = cr.update(mBookmarksUriWithProfile, values, Bookmarks.POSITION + " = ? AND " + Bookmarks.PARENT + " = ?", new String[] { Integer.toString(position), String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }); // Otherwise just insert a new item if (updated == 0) { cr.insert(mBookmarksUriWithProfile, values); } } @Override public Cursor getPinnedSites(ContentResolver cr, int limit) { return cr.query(bookmarksUriWithLimit(limit), new String[] { Bookmarks._ID, Bookmarks.URL, Bookmarks.TITLE, Bookmarks.POSITION }, Bookmarks.PARENT + " == ?", new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }, Bookmarks.POSITION + " ASC"); } @Override public void unpinSite(ContentResolver cr, int position) { cr.delete(mBookmarksUriWithProfile, Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?", new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID), Integer.toString(position) }); } @Override public void unpinAllSites(ContentResolver cr) { cr.delete(mBookmarksUriWithProfile, Bookmarks.PARENT + " == ?", new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) }); } @Override public boolean isVisited(ContentResolver cr, String uri) { int count = 0; try { Cursor c = cr.query(historyUriWithLimit(1), new String[] { History._ID }, History.URL + " = ?", new String[] { uri }, History.URL); count = c.getCount(); c.close(); } catch (NullPointerException e) { Log.e(LOGTAG, "NullPointerException in isVisited"); } return (count > 0); } }