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