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 file, michael@0: * 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.util.ArrayList; michael@0: import java.util.Collections; michael@0: import java.util.HashMap; michael@0: import java.util.Map; michael@0: 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.CommonColumns; 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.Schema; michael@0: import org.mozilla.gecko.db.BrowserContract.SyncColumns; michael@0: import org.mozilla.gecko.db.BrowserContract.Thumbnails; michael@0: import org.mozilla.gecko.sync.Utils; michael@0: michael@0: import android.app.SearchManager; michael@0: import android.content.ContentProviderOperation; michael@0: import android.content.ContentProviderResult; michael@0: import android.content.ContentUris; michael@0: import android.content.ContentValues; michael@0: import android.content.OperationApplicationException; michael@0: import android.content.UriMatcher; michael@0: import android.database.Cursor; michael@0: import android.database.DatabaseUtils; michael@0: import android.database.MatrixCursor; michael@0: import android.database.SQLException; michael@0: import android.database.sqlite.SQLiteDatabase; michael@0: import android.database.sqlite.SQLiteQueryBuilder; michael@0: import android.net.Uri; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: michael@0: public class BrowserProvider extends SharedBrowserDatabaseProvider { michael@0: private static final String LOGTAG = "GeckoBrowserProvider"; michael@0: michael@0: // How many records to reposition in a single query. michael@0: // This should be less than the SQLite maximum number of query variables michael@0: // (currently 999) divided by the number of variables used per positioning michael@0: // query (currently 3). michael@0: static final int MAX_POSITION_UPDATES_PER_QUERY = 100; michael@0: michael@0: // Minimum number of records to keep when expiring history. michael@0: static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000; michael@0: static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500; michael@0: michael@0: // Minimum duration to keep when expiring. michael@0: static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks. michael@0: // Minimum number of thumbnails to keep around. michael@0: static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15; michael@0: michael@0: static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; michael@0: static final String TABLE_HISTORY = History.TABLE_NAME; michael@0: static final String TABLE_FAVICONS = Favicons.TABLE_NAME; michael@0: static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; michael@0: michael@0: static final String VIEW_COMBINED = Combined.VIEW_NAME; michael@0: static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; michael@0: static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; michael@0: static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; michael@0: michael@0: static final String VIEW_FLAGS = "flags"; michael@0: michael@0: // Bookmark matches michael@0: static final int BOOKMARKS = 100; michael@0: static final int BOOKMARKS_ID = 101; michael@0: static final int BOOKMARKS_FOLDER_ID = 102; michael@0: static final int BOOKMARKS_PARENT = 103; michael@0: static final int BOOKMARKS_POSITIONS = 104; michael@0: michael@0: // History matches michael@0: static final int HISTORY = 200; michael@0: static final int HISTORY_ID = 201; michael@0: static final int HISTORY_OLD = 202; michael@0: michael@0: // Favicon matches michael@0: static final int FAVICONS = 300; michael@0: static final int FAVICON_ID = 301; michael@0: michael@0: // Schema matches michael@0: static final int SCHEMA = 400; michael@0: michael@0: // Combined bookmarks and history matches michael@0: static final int COMBINED = 500; michael@0: michael@0: // Control matches michael@0: static final int CONTROL = 600; michael@0: michael@0: // Search Suggest matches michael@0: static final int SEARCH_SUGGEST = 700; michael@0: michael@0: // Thumbnail matches michael@0: static final int THUMBNAILS = 800; michael@0: static final int THUMBNAIL_ID = 801; michael@0: michael@0: static final int FLAGS = 900; michael@0: michael@0: static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE michael@0: + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID michael@0: + " ASC"; michael@0: michael@0: static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC"; michael@0: michael@0: static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); michael@0: michael@0: static final Map BOOKMARKS_PROJECTION_MAP; michael@0: static final Map HISTORY_PROJECTION_MAP; michael@0: static final Map COMBINED_PROJECTION_MAP; michael@0: static final Map SCHEMA_PROJECTION_MAP; michael@0: static final Map SEARCH_SUGGEST_PROJECTION_MAP; michael@0: static final Map FAVICONS_PROJECTION_MAP; michael@0: static final Map THUMBNAILS_PROJECTION_MAP; michael@0: michael@0: static { michael@0: // We will reuse this. michael@0: HashMap map; michael@0: michael@0: // Bookmarks michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); michael@0: michael@0: map = new HashMap(); michael@0: map.put(Bookmarks._ID, Bookmarks._ID); michael@0: map.put(Bookmarks.TITLE, Bookmarks.TITLE); michael@0: map.put(Bookmarks.URL, Bookmarks.URL); michael@0: map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); michael@0: map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID); michael@0: map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL); michael@0: map.put(Bookmarks.TYPE, Bookmarks.TYPE); michael@0: map.put(Bookmarks.PARENT, Bookmarks.PARENT); michael@0: map.put(Bookmarks.POSITION, Bookmarks.POSITION); michael@0: map.put(Bookmarks.TAGS, Bookmarks.TAGS); michael@0: map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION); michael@0: map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD); michael@0: map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); michael@0: map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); michael@0: map.put(Bookmarks.GUID, Bookmarks.GUID); michael@0: map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); michael@0: BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: // History michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD); michael@0: michael@0: map = new HashMap(); michael@0: map.put(History._ID, History._ID); michael@0: map.put(History.TITLE, History.TITLE); michael@0: map.put(History.URL, History.URL); michael@0: map.put(History.FAVICON, History.FAVICON); michael@0: map.put(History.FAVICON_ID, History.FAVICON_ID); michael@0: map.put(History.FAVICON_URL, History.FAVICON_URL); michael@0: map.put(History.VISITS, History.VISITS); michael@0: map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); michael@0: map.put(History.DATE_CREATED, History.DATE_CREATED); michael@0: map.put(History.DATE_MODIFIED, History.DATE_MODIFIED); michael@0: map.put(History.GUID, History.GUID); michael@0: map.put(History.IS_DELETED, History.IS_DELETED); michael@0: HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: // Favicons michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID); michael@0: michael@0: map = new HashMap(); michael@0: map.put(Favicons._ID, Favicons._ID); michael@0: map.put(Favicons.URL, Favicons.URL); michael@0: map.put(Favicons.DATA, Favicons.DATA); michael@0: map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED); michael@0: map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED); michael@0: FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: // Thumbnails michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS); michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID); michael@0: michael@0: map = new HashMap(); michael@0: map.put(Thumbnails._ID, Thumbnails._ID); michael@0: map.put(Thumbnails.URL, Thumbnails.URL); michael@0: map.put(Thumbnails.DATA, Thumbnails.DATA); michael@0: THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: // Combined bookmarks and history michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED); michael@0: michael@0: map = new HashMap(); michael@0: map.put(Combined._ID, Combined._ID); michael@0: map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID); michael@0: map.put(Combined.HISTORY_ID, Combined.HISTORY_ID); michael@0: map.put(Combined.DISPLAY, "MAX(" + Combined.DISPLAY + ") AS " + Combined.DISPLAY); michael@0: map.put(Combined.URL, Combined.URL); michael@0: map.put(Combined.TITLE, Combined.TITLE); michael@0: map.put(Combined.VISITS, Combined.VISITS); michael@0: map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); michael@0: map.put(Combined.FAVICON, Combined.FAVICON); michael@0: map.put(Combined.FAVICON_ID, Combined.FAVICON_ID); michael@0: map.put(Combined.FAVICON_URL, Combined.FAVICON_URL); michael@0: COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: // Schema michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA); michael@0: michael@0: map = new HashMap(); michael@0: map.put(Schema.VERSION, Schema.VERSION); michael@0: SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: michael@0: michael@0: // Control michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL); michael@0: michael@0: // Search Suggest michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST); michael@0: michael@0: URI_MATCHER.addURI(BrowserContract.AUTHORITY, "flags", FLAGS); michael@0: michael@0: map = new HashMap(); michael@0: map.put(SearchManager.SUGGEST_COLUMN_TEXT_1, michael@0: Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); michael@0: map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL, michael@0: Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL); michael@0: map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, michael@0: Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA); michael@0: SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map); michael@0: } michael@0: michael@0: static final String qualifyColumn(String table, String column) { michael@0: return table + "." + column; michael@0: } michael@0: michael@0: private static boolean hasFaviconsInProjection(String[] projection) { michael@0: if (projection == null) return true; michael@0: for (int i = 0; i < projection.length; ++i) { michael@0: if (projection[i].equals(FaviconColumns.FAVICON) || michael@0: projection[i].equals(FaviconColumns.FAVICON_URL)) michael@0: return true; michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: 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 boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); michael@0: private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); michael@0: protected static void trace(String message) { michael@0: if (logVerbose) { michael@0: Log.v(LOGTAG, message); michael@0: } michael@0: } michael@0: 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: /** michael@0: * Remove enough history items to bring the database count below retain, michael@0: * removing no items with a modified time after keepAfter. michael@0: * michael@0: * Provide keepAfter less than or equal to zero to skip that check. michael@0: * michael@0: * Items will be removed according to an approximate frecency calculation. michael@0: */ michael@0: private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) { michael@0: Log.d(LOGTAG, "Expiring history."); michael@0: final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY); michael@0: michael@0: if (retain >= rows) { michael@0: debug("Not expiring history: only have " + rows + " rows."); michael@0: return; michael@0: } michael@0: michael@0: final String sortOrder = BrowserContract.getFrecencySortOrder(false, true); michael@0: final long toRemove = rows - retain; michael@0: debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + "."); michael@0: michael@0: final String sql; michael@0: if (keepAfter > 0) { michael@0: sql = "DELETE FROM " + TABLE_HISTORY + " " + michael@0: "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED +") < " + keepAfter + " " + michael@0: " AND " + History._ID + " IN ( SELECT " + michael@0: History._ID + " FROM " + TABLE_HISTORY + " " + michael@0: "ORDER BY " + sortOrder + " LIMIT " + toRemove + michael@0: ")"; michael@0: } else { michael@0: sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " + michael@0: "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " + michael@0: "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")"; michael@0: } michael@0: trace("Deleting using query: " + sql); michael@0: michael@0: beginWrite(db); michael@0: db.execSQL(sql); michael@0: } michael@0: michael@0: /** michael@0: * Remove any thumbnails that for sites that aren't likely to be ever shown. michael@0: * Items will be removed according to a frecency calculation and only if they are not pinned michael@0: * michael@0: * Call this method within a transaction. michael@0: */ michael@0: private void expireThumbnails(final SQLiteDatabase db) { michael@0: Log.d(LOGTAG, "Expiring thumbnails."); michael@0: final String sortOrder = BrowserContract.getFrecencySortOrder(true, false); michael@0: final String sql = "DELETE FROM " + TABLE_THUMBNAILS + michael@0: " WHERE " + Thumbnails.URL + " NOT IN ( " + michael@0: " SELECT " + Combined.URL + michael@0: " FROM " + Combined.VIEW_NAME + michael@0: " ORDER BY " + sortOrder + michael@0: " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT + michael@0: ") AND " + Thumbnails.URL + " NOT IN ( " + michael@0: " SELECT " + Bookmarks.URL + michael@0: " FROM " + TABLE_BOOKMARKS + michael@0: " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + michael@0: ")"; michael@0: trace("Clear thumbs using query: " + sql); michael@0: db.execSQL(sql); michael@0: } michael@0: michael@0: private boolean shouldIncrementVisits(Uri uri) { michael@0: String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS); michael@0: return Boolean.parseBoolean(incrementVisits); michael@0: } michael@0: michael@0: @Override michael@0: public String getType(Uri uri) { michael@0: final int match = URI_MATCHER.match(uri); michael@0: michael@0: trace("Getting URI type: " + uri); michael@0: michael@0: switch (match) { michael@0: case BOOKMARKS: michael@0: trace("URI is BOOKMARKS: " + uri); michael@0: return Bookmarks.CONTENT_TYPE; michael@0: case BOOKMARKS_ID: michael@0: trace("URI is BOOKMARKS_ID: " + uri); michael@0: return Bookmarks.CONTENT_ITEM_TYPE; michael@0: case HISTORY: michael@0: trace("URI is HISTORY: " + uri); michael@0: return History.CONTENT_TYPE; michael@0: case HISTORY_ID: michael@0: trace("URI is HISTORY_ID: " + uri); michael@0: return History.CONTENT_ITEM_TYPE; michael@0: case SEARCH_SUGGEST: michael@0: trace("URI is SEARCH_SUGGEST: " + uri); michael@0: return SearchManager.SUGGEST_MIME_TYPE; michael@0: case FLAGS: michael@0: trace("URI is FLAGS."); michael@0: return Bookmarks.CONTENT_ITEM_TYPE; michael@0: } michael@0: michael@0: debug("URI has unrecognized type: " + uri); michael@0: michael@0: return null; michael@0: } michael@0: michael@0: @SuppressWarnings("fallthrough") michael@0: @Override michael@0: public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { michael@0: trace("Calling delete in transaction on URI: " + uri); michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: final int match = URI_MATCHER.match(uri); michael@0: int deleted = 0; michael@0: michael@0: switch (match) { michael@0: case BOOKMARKS_ID: michael@0: trace("Delete on BOOKMARKS_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case BOOKMARKS: { michael@0: trace("Deleting bookmarks: " + uri); michael@0: deleted = deleteBookmarks(uri, selection, selectionArgs); michael@0: deleteUnusedImages(uri); michael@0: break; michael@0: } michael@0: michael@0: case HISTORY_ID: michael@0: trace("Delete on HISTORY_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case HISTORY: { michael@0: trace("Deleting history: " + uri); michael@0: beginWrite(db); michael@0: deleted = deleteHistory(uri, selection, selectionArgs); michael@0: deleteUnusedImages(uri); michael@0: break; michael@0: } michael@0: michael@0: case HISTORY_OLD: { michael@0: String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY); michael@0: long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW; michael@0: int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT; michael@0: michael@0: if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) { michael@0: keepAfter = 0; michael@0: retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT; michael@0: } michael@0: expireHistory(db, retainCount, keepAfter); michael@0: expireThumbnails(db); michael@0: deleteUnusedImages(uri); michael@0: break; michael@0: } michael@0: michael@0: case FAVICON_ID: michael@0: debug("Delete on FAVICON_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case FAVICONS: { michael@0: trace("Deleting favicons: " + uri); michael@0: beginWrite(db); michael@0: deleted = deleteFavicons(uri, selection, selectionArgs); michael@0: break; michael@0: } michael@0: michael@0: case THUMBNAIL_ID: michael@0: debug("Delete on THUMBNAIL_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case THUMBNAILS: { michael@0: trace("Deleting thumbnails: " + uri); michael@0: beginWrite(db); michael@0: deleted = deleteThumbnails(uri, selection, selectionArgs); michael@0: break; michael@0: } michael@0: michael@0: default: michael@0: throw new UnsupportedOperationException("Unknown delete URI " + uri); michael@0: } michael@0: michael@0: debug("Deleted " + deleted + " rows for URI: " + uri); michael@0: michael@0: return deleted; michael@0: } michael@0: michael@0: @Override michael@0: public Uri insertInTransaction(Uri uri, ContentValues values) { michael@0: trace("Calling insert in transaction on URI: " + uri); michael@0: michael@0: int match = URI_MATCHER.match(uri); michael@0: long id = -1; michael@0: michael@0: switch (match) { michael@0: case BOOKMARKS: { michael@0: trace("Insert on BOOKMARKS: " + uri); michael@0: id = insertBookmark(uri, values); michael@0: break; michael@0: } michael@0: michael@0: case HISTORY: { michael@0: trace("Insert on HISTORY: " + uri); michael@0: id = insertHistory(uri, values); michael@0: break; michael@0: } michael@0: michael@0: case FAVICONS: { michael@0: trace("Insert on FAVICONS: " + uri); michael@0: id = insertFavicon(uri, values); michael@0: break; michael@0: } michael@0: michael@0: case THUMBNAILS: { michael@0: trace("Insert on THUMBNAILS: " + uri); michael@0: id = insertThumbnail(uri, values); michael@0: break; michael@0: } michael@0: michael@0: default: michael@0: throw new UnsupportedOperationException("Unknown insert URI " + uri); michael@0: } michael@0: michael@0: debug("Inserted ID in database: " + id); michael@0: michael@0: if (id >= 0) michael@0: return ContentUris.withAppendedId(uri, id); michael@0: michael@0: return null; michael@0: } michael@0: michael@0: @SuppressWarnings("fallthrough") michael@0: @Override michael@0: public int updateInTransaction(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: trace("Calling update in transaction on URI: " + uri); michael@0: michael@0: int match = URI_MATCHER.match(uri); michael@0: int updated = 0; michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: switch (match) { michael@0: // We provide a dedicated (hacky) API for callers to bulk-update the positions of michael@0: // folder children by passing an array of GUID strings as `selectionArgs`. michael@0: // Each child will have its position column set to its index in the provided array. michael@0: // michael@0: // This avoids callers having to issue a large number of UPDATE queries through michael@0: // the usual channels. See Bug 728783. michael@0: // michael@0: // Note that this is decidedly not a general-purpose API; use at your own risk. michael@0: // `values` and `selection` are ignored. michael@0: case BOOKMARKS_POSITIONS: { michael@0: debug("Update on BOOKMARKS_POSITIONS: " + uri); michael@0: michael@0: // This already starts and finishes its own transaction. michael@0: updated = updateBookmarkPositions(uri, selectionArgs); michael@0: break; michael@0: } michael@0: michael@0: case BOOKMARKS_PARENT: { michael@0: debug("Update on BOOKMARKS_PARENT: " + uri); michael@0: beginWrite(db); michael@0: updated = updateBookmarkParents(db, values, selection, selectionArgs); michael@0: break; michael@0: } michael@0: michael@0: case BOOKMARKS_ID: michael@0: debug("Update on BOOKMARKS_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case BOOKMARKS: { michael@0: debug("Updating bookmark: " + uri); michael@0: if (shouldUpdateOrInsert(uri)) { michael@0: updated = updateOrInsertBookmark(uri, values, selection, selectionArgs); michael@0: } else { michael@0: updated = updateBookmarks(uri, values, selection, selectionArgs); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case HISTORY_ID: michael@0: debug("Update on HISTORY_ID: " + uri); michael@0: michael@0: selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case HISTORY: { michael@0: debug("Updating history: " + uri); michael@0: if (shouldUpdateOrInsert(uri)) { michael@0: updated = updateOrInsertHistory(uri, values, selection, selectionArgs); michael@0: } else { michael@0: updated = updateHistory(uri, values, selection, selectionArgs); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case FAVICONS: { michael@0: debug("Update on FAVICONS: " + uri); michael@0: michael@0: String url = values.getAsString(Favicons.URL); michael@0: String faviconSelection = null; michael@0: String[] faviconSelectionArgs = null; michael@0: michael@0: if (!TextUtils.isEmpty(url)) { michael@0: faviconSelection = Favicons.URL + " = ?"; michael@0: faviconSelectionArgs = new String[] { url }; michael@0: } michael@0: michael@0: if (shouldUpdateOrInsert(uri)) { michael@0: updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs); michael@0: } else { michael@0: updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case THUMBNAILS: { michael@0: debug("Update on THUMBNAILS: " + uri); michael@0: michael@0: String url = values.getAsString(Thumbnails.URL); michael@0: michael@0: // if no URL is provided, update all of the entries michael@0: if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) { michael@0: updated = updateExistingThumbnail(uri, values, null, null); michael@0: } else if (shouldUpdateOrInsert(uri)) { michael@0: updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?", michael@0: new String[] { url }); michael@0: } else { michael@0: updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?", michael@0: new String[] { url }); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: default: michael@0: throw new UnsupportedOperationException("Unknown update URI " + uri); michael@0: } michael@0: michael@0: debug("Updated " + updated + " rows for URI: " + uri); michael@0: return updated; michael@0: } michael@0: michael@0: @Override michael@0: public Cursor query(Uri uri, String[] projection, String selection, michael@0: String[] selectionArgs, String sortOrder) { michael@0: SQLiteDatabase db = getReadableDatabase(uri); michael@0: final int match = URI_MATCHER.match(uri); michael@0: michael@0: // The first selectionArgs value is the URI for which to query. michael@0: if (match == FLAGS) { michael@0: // We don't need the QB below for this. michael@0: // michael@0: // There are three possible kinds of bookmarks: michael@0: // * Regular bookmarks michael@0: // * Bookmarks whose parent is FIXED_READING_LIST_ID (reading list items) michael@0: // * Bookmarks whose parent is FIXED_PINNED_LIST_ID (pinned items). michael@0: // michael@0: // Although SQLite doesn't have an aggregate operator for bitwise-OR, we're michael@0: // using disjoint flags, so we can simply use SUM and DISTINCT to get the michael@0: // flags we need. michael@0: // We turn parents into flags according to the three kinds, above. michael@0: // michael@0: // When this query is extended to support queries across multiple tables, simply michael@0: // extend it to look like michael@0: // michael@0: // SELECT COALESCE((SELECT ...), 0) | COALESCE(...) | ... michael@0: michael@0: final boolean includeDeleted = shouldShowDeleted(uri); michael@0: final String query = "SELECT COALESCE(SUM(flag), 0) AS flags " + michael@0: "FROM ( SELECT DISTINCT CASE" + michael@0: " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_READING_LIST_ID + michael@0: " THEN " + Bookmarks.FLAG_READING + michael@0: michael@0: " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + michael@0: " THEN " + Bookmarks.FLAG_PINNED + michael@0: michael@0: " ELSE " + Bookmarks.FLAG_BOOKMARK + michael@0: " END flag " + michael@0: "FROM " + TABLE_BOOKMARKS + " WHERE " + michael@0: Bookmarks.URL + " = ? " + michael@0: (includeDeleted ? "" : ("AND " + Bookmarks.IS_DELETED + " = 0")) + michael@0: ")"; michael@0: michael@0: return db.rawQuery(query, selectionArgs); michael@0: } michael@0: michael@0: SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); michael@0: String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); michael@0: String groupBy = null; michael@0: michael@0: switch (match) { michael@0: case BOOKMARKS_FOLDER_ID: michael@0: case BOOKMARKS_ID: michael@0: case BOOKMARKS: { michael@0: debug("Query is on bookmarks: " + uri); michael@0: michael@0: if (match == BOOKMARKS_ID) { michael@0: selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: } else if (match == BOOKMARKS_FOLDER_ID) { michael@0: selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: } michael@0: michael@0: if (!shouldShowDeleted(uri)) michael@0: selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); michael@0: michael@0: if (TextUtils.isEmpty(sortOrder)) { michael@0: sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; michael@0: } else { michael@0: debug("Using sort order " + sortOrder + "."); michael@0: } michael@0: michael@0: qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); michael@0: michael@0: if (hasFaviconsInProjection(projection)) michael@0: qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS); michael@0: else michael@0: qb.setTables(TABLE_BOOKMARKS); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case HISTORY_ID: michael@0: selection = DBUtils.concatenateWhere(selection, History._ID + " = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case HISTORY: { michael@0: debug("Query is on history: " + uri); michael@0: michael@0: if (!shouldShowDeleted(uri)) michael@0: selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection); michael@0: michael@0: if (TextUtils.isEmpty(sortOrder)) michael@0: sortOrder = DEFAULT_HISTORY_SORT_ORDER; michael@0: michael@0: qb.setProjectionMap(HISTORY_PROJECTION_MAP); michael@0: michael@0: if (hasFaviconsInProjection(projection)) michael@0: qb.setTables(VIEW_HISTORY_WITH_FAVICONS); michael@0: else michael@0: qb.setTables(TABLE_HISTORY); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case FAVICON_ID: michael@0: selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case FAVICONS: { michael@0: debug("Query is on favicons: " + uri); michael@0: michael@0: qb.setProjectionMap(FAVICONS_PROJECTION_MAP); michael@0: qb.setTables(TABLE_FAVICONS); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case THUMBNAIL_ID: michael@0: selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?"); michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { Long.toString(ContentUris.parseId(uri)) }); michael@0: // fall through michael@0: case THUMBNAILS: { michael@0: debug("Query is on thumbnails: " + uri); michael@0: michael@0: qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP); michael@0: qb.setTables(TABLE_THUMBNAILS); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case SCHEMA: { michael@0: debug("Query is on schema."); michael@0: MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION }); michael@0: schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION); michael@0: michael@0: return schemaCursor; michael@0: } michael@0: michael@0: case COMBINED: { michael@0: debug("Query is on combined: " + uri); michael@0: michael@0: if (TextUtils.isEmpty(sortOrder)) michael@0: sortOrder = DEFAULT_HISTORY_SORT_ORDER; michael@0: michael@0: // This will avoid duplicate entries in the awesomebar michael@0: // results when a history entry has multiple bookmarks. michael@0: groupBy = Combined.URL; michael@0: michael@0: qb.setProjectionMap(COMBINED_PROJECTION_MAP); michael@0: michael@0: if (hasFaviconsInProjection(projection)) michael@0: qb.setTables(VIEW_COMBINED_WITH_FAVICONS); michael@0: else michael@0: qb.setTables(Combined.VIEW_NAME); michael@0: michael@0: break; michael@0: } michael@0: michael@0: case SEARCH_SUGGEST: { michael@0: debug("Query is on search suggest: " + uri); michael@0: selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " + michael@0: Combined.TITLE + " LIKE ?)"); michael@0: michael@0: String keyword = uri.getLastPathSegment(); michael@0: if (keyword == null) michael@0: keyword = ""; michael@0: michael@0: selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, michael@0: new String[] { "%" + keyword + "%", michael@0: "%" + keyword + "%" }); michael@0: michael@0: if (TextUtils.isEmpty(sortOrder)) michael@0: sortOrder = DEFAULT_HISTORY_SORT_ORDER; michael@0: michael@0: qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP); michael@0: qb.setTables(VIEW_COMBINED_WITH_FAVICONS); michael@0: michael@0: break; michael@0: } michael@0: michael@0: default: michael@0: throw new UnsupportedOperationException("Unknown query URI " + uri); michael@0: } michael@0: michael@0: trace("Running built query."); michael@0: Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, michael@0: null, sortOrder, limit); michael@0: cursor.setNotificationUri(getContext().getContentResolver(), michael@0: BrowserContract.AUTHORITY_URI); michael@0: michael@0: return cursor; michael@0: } michael@0: michael@0: /** michael@0: * Update the positions of bookmarks in batches. michael@0: * michael@0: * Begins and ends its own transactions. michael@0: * michael@0: * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int) michael@0: */ michael@0: int updateBookmarkPositions(Uri uri, String[] guids) { michael@0: if (guids == null) { michael@0: return 0; michael@0: } michael@0: michael@0: int guidsCount = guids.length; michael@0: if (guidsCount == 0) { michael@0: return 0; michael@0: } michael@0: michael@0: int offset = 0; michael@0: int updated = 0; michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: db.beginTransaction(); michael@0: michael@0: while (offset < guidsCount) { michael@0: try { michael@0: updated += updateBookmarkPositionsInTransaction(db, guids, offset, michael@0: MAX_POSITION_UPDATES_PER_QUERY); michael@0: } catch (SQLException e) { michael@0: Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e); michael@0: michael@0: // Need to restart the transaction. michael@0: // The only way a caller knows that anything failed is that the michael@0: // returned update count will be smaller than the requested michael@0: // number of records. michael@0: db.setTransactionSuccessful(); michael@0: db.endTransaction(); michael@0: michael@0: db.beginTransaction(); michael@0: } michael@0: michael@0: offset += MAX_POSITION_UPDATES_PER_QUERY; michael@0: } michael@0: michael@0: db.setTransactionSuccessful(); michael@0: db.endTransaction(); michael@0: michael@0: return updated; michael@0: } michael@0: michael@0: /** michael@0: * Construct and execute an update expression that will modify the positions michael@0: * of records in-place. michael@0: */ michael@0: private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids, michael@0: final int offset, final int max) { michael@0: int guidsCount = guids.length; michael@0: int processCount = Math.min(max, guidsCount - offset); michael@0: michael@0: // Each must appear twice: once in a CASE, and once in the IN clause. michael@0: String[] args = new String[processCount * 2]; michael@0: System.arraycopy(guids, offset, args, 0, processCount); michael@0: System.arraycopy(guids, offset, args, processCount, processCount); michael@0: michael@0: StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS + michael@0: " SET " + Bookmarks.POSITION + michael@0: " = CASE guid"); michael@0: michael@0: // Build the CASE statement body for GUID/index pairs from offset up to michael@0: // the computed limit. michael@0: final int end = offset + processCount; michael@0: int i = offset; michael@0: for (; i < end; ++i) { michael@0: if (guids[i] == null) { michael@0: // We don't want to issue the query if not every GUID is specified. michael@0: debug("updateBookmarkPositions called with null GUID at index " + i); michael@0: return 0; michael@0: } michael@0: b.append(" WHEN ? THEN " + i); michael@0: } michael@0: michael@0: // TODO: use computeSQLInClause michael@0: b.append(" END WHERE " + Bookmarks.GUID + " IN ("); michael@0: i = 1; michael@0: while (i++ < processCount) { michael@0: b.append("?, "); michael@0: } michael@0: b.append("?)"); michael@0: db.execSQL(b.toString(), args); michael@0: michael@0: // We can't easily get a modified count without calling something like changes(). michael@0: return processCount; michael@0: } michael@0: michael@0: /** michael@0: * Construct an update expression that will modify the parents of any records michael@0: * that match. michael@0: */ michael@0: private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) { michael@0: trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")"); michael@0: String where = Bookmarks._ID + " IN (" + michael@0: " SELECT DISTINCT " + Bookmarks.PARENT + michael@0: " FROM " + TABLE_BOOKMARKS + michael@0: " WHERE " + selection + " )"; michael@0: return db.update(TABLE_BOOKMARKS, values, where, selectionArgs); michael@0: } michael@0: michael@0: long insertBookmark(Uri uri, ContentValues values) { michael@0: // Generate values if not specified. Don't overwrite michael@0: // if specified by caller. michael@0: long now = System.currentTimeMillis(); michael@0: if (!values.containsKey(Bookmarks.DATE_CREATED)) { michael@0: values.put(Bookmarks.DATE_CREATED, now); michael@0: } michael@0: michael@0: if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { michael@0: values.put(Bookmarks.DATE_MODIFIED, now); michael@0: } michael@0: michael@0: if (!values.containsKey(Bookmarks.GUID)) { michael@0: values.put(Bookmarks.GUID, Utils.generateGuid()); michael@0: } michael@0: michael@0: if (!values.containsKey(Bookmarks.POSITION)) { michael@0: debug("Inserting bookmark with no position for URI"); michael@0: values.put(Bookmarks.POSITION, michael@0: Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION)); michael@0: } michael@0: michael@0: String url = values.getAsString(Bookmarks.URL); michael@0: michael@0: debug("Inserting bookmark in database with URL: " + url); michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: beginWrite(db); michael@0: return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values); michael@0: } michael@0: michael@0: michael@0: int updateOrInsertBookmark(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: int updated = updateBookmarks(uri, values, selection, selectionArgs); michael@0: if (updated > 0) { michael@0: return updated; michael@0: } michael@0: michael@0: // Transaction already begun by updateBookmarks. michael@0: if (0 <= insertBookmark(uri, values)) { michael@0: // We 'updated' one row. michael@0: return 1; michael@0: } michael@0: michael@0: // If something went wrong, then we updated zero rows. michael@0: return 0; michael@0: } michael@0: michael@0: int updateBookmarks(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: trace("Updating bookmarks on URI: " + uri); michael@0: michael@0: final String[] bookmarksProjection = new String[] { michael@0: Bookmarks._ID, // 0 michael@0: }; michael@0: michael@0: if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { michael@0: values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); michael@0: } michael@0: michael@0: trace("Querying bookmarks to update on URI: " + uri); michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: // Compute matching IDs. michael@0: final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, michael@0: selection, selectionArgs, null, null, null); michael@0: michael@0: // Now that we're done reading, open a transaction. michael@0: final String inClause; michael@0: try { michael@0: inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID); michael@0: } finally { michael@0: cursor.close(); michael@0: } michael@0: michael@0: beginWrite(db); michael@0: return db.update(TABLE_BOOKMARKS, values, inClause, null); michael@0: } michael@0: michael@0: long insertHistory(Uri uri, ContentValues values) { michael@0: final long now = System.currentTimeMillis(); michael@0: values.put(History.DATE_CREATED, now); michael@0: values.put(History.DATE_MODIFIED, now); michael@0: michael@0: // Generate GUID for new history entry. Don't override specified GUIDs. michael@0: if (!values.containsKey(History.GUID)) { michael@0: values.put(History.GUID, Utils.generateGuid()); michael@0: } michael@0: michael@0: String url = values.getAsString(History.URL); michael@0: michael@0: debug("Inserting history in database with URL: " + url); michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: beginWrite(db); michael@0: return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); michael@0: } michael@0: michael@0: int updateOrInsertHistory(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: final int updated = updateHistory(uri, values, selection, selectionArgs); michael@0: if (updated > 0) { michael@0: return updated; michael@0: } michael@0: michael@0: // Insert a new entry if necessary michael@0: if (!values.containsKey(History.VISITS)) { michael@0: values.put(History.VISITS, 1); michael@0: } michael@0: if (!values.containsKey(History.TITLE)) { michael@0: values.put(History.TITLE, values.getAsString(History.URL)); michael@0: } michael@0: michael@0: if (0 <= insertHistory(uri, values)) { michael@0: return 1; michael@0: } michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: int updateHistory(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: trace("Updating history on URI: " + uri); michael@0: michael@0: int updated = 0; michael@0: michael@0: final String[] historyProjection = new String[] { michael@0: History._ID, // 0 michael@0: History.URL, // 1 michael@0: History.VISITS // 2 michael@0: }; michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection, michael@0: selectionArgs, null, null, null); michael@0: michael@0: try { michael@0: if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { michael@0: values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); michael@0: } michael@0: michael@0: while (cursor.moveToNext()) { michael@0: long id = cursor.getLong(0); michael@0: michael@0: trace("Updating history entry with ID: " + id); michael@0: michael@0: if (shouldIncrementVisits(uri)) { michael@0: long existing = cursor.getLong(2); michael@0: Long additional = values.getAsLong(History.VISITS); michael@0: michael@0: // Increment visit count by a specified amount, or default to increment by 1 michael@0: values.put(History.VISITS, existing + ((additional != null) ? additional.longValue() : 1)); michael@0: } michael@0: michael@0: updated += db.update(TABLE_HISTORY, values, "_id = ?", michael@0: new String[] { Long.toString(id) }); michael@0: } michael@0: } finally { michael@0: cursor.close(); michael@0: } michael@0: michael@0: return updated; michael@0: } michael@0: michael@0: private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) { michael@0: ContentValues updateValues = new ContentValues(1); michael@0: updateValues.put(FaviconColumns.FAVICON_ID, faviconId); michael@0: db.update(TABLE_HISTORY, michael@0: updateValues, michael@0: History.URL + " = ?", michael@0: new String[] { pageUrl }); michael@0: db.update(TABLE_BOOKMARKS, michael@0: updateValues, michael@0: Bookmarks.URL + " = ?", michael@0: new String[] { pageUrl }); michael@0: } michael@0: michael@0: long insertFavicon(Uri uri, ContentValues values) { michael@0: return insertFavicon(getWritableDatabase(uri), values); michael@0: } michael@0: michael@0: long insertFavicon(SQLiteDatabase db, ContentValues values) { michael@0: // This method is a dupicate of BrowserDatabaseHelper.insertFavicon. michael@0: // If changes are needed, please update both michael@0: String faviconUrl = values.getAsString(Favicons.URL); michael@0: String pageUrl = null; michael@0: michael@0: trace("Inserting favicon for URL: " + faviconUrl); michael@0: michael@0: DBUtils.stripEmptyByteArray(values, Favicons.DATA); michael@0: michael@0: // Extract the page URL from the ContentValues michael@0: if (values.containsKey(Favicons.PAGE_URL)) { michael@0: pageUrl = values.getAsString(Favicons.PAGE_URL); michael@0: values.remove(Favicons.PAGE_URL); michael@0: } michael@0: michael@0: // If no URL is provided, insert using the default one. michael@0: if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) { michael@0: values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl)); michael@0: } michael@0: michael@0: final long now = System.currentTimeMillis(); michael@0: values.put(Favicons.DATE_CREATED, now); michael@0: values.put(Favicons.DATE_MODIFIED, now); michael@0: michael@0: beginWrite(db); michael@0: final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values); michael@0: michael@0: if (pageUrl != null) { michael@0: updateFaviconIdsForUrl(db, pageUrl, faviconId); michael@0: } michael@0: return faviconId; michael@0: } michael@0: michael@0: int updateOrInsertFavicon(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: return updateFavicon(uri, values, selection, selectionArgs, michael@0: true /* insert if needed */); michael@0: } michael@0: michael@0: int updateExistingFavicon(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: return updateFavicon(uri, values, selection, selectionArgs, michael@0: false /* only update, no insert */); michael@0: } michael@0: michael@0: int updateFavicon(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs, boolean insertIfNeeded) { michael@0: String faviconUrl = values.getAsString(Favicons.URL); michael@0: String pageUrl = null; michael@0: int updated = 0; michael@0: Long faviconId = null; michael@0: long now = System.currentTimeMillis(); michael@0: michael@0: trace("Updating favicon for URL: " + faviconUrl); michael@0: michael@0: DBUtils.stripEmptyByteArray(values, Favicons.DATA); michael@0: michael@0: // Extract the page URL from the ContentValues michael@0: if (values.containsKey(Favicons.PAGE_URL)) { michael@0: pageUrl = values.getAsString(Favicons.PAGE_URL); michael@0: values.remove(Favicons.PAGE_URL); michael@0: } michael@0: michael@0: values.put(Favicons.DATE_MODIFIED, now); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: // If there's no favicon URL given and we're inserting if needed, skip michael@0: // the update and only do an insert (otherwise all rows would be michael@0: // updated). michael@0: if (!(insertIfNeeded && (faviconUrl == null))) { michael@0: updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs); michael@0: } michael@0: michael@0: if (updated > 0) { michael@0: if ((faviconUrl != null) && (pageUrl != null)) { michael@0: final Cursor cursor = db.query(TABLE_FAVICONS, michael@0: new String[] { Favicons._ID }, michael@0: Favicons.URL + " = ?", michael@0: new String[] { faviconUrl }, michael@0: null, null, null); michael@0: try { michael@0: if (cursor.moveToFirst()) { michael@0: faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID)); michael@0: } michael@0: } finally { michael@0: cursor.close(); michael@0: } michael@0: } michael@0: if (pageUrl != null) { michael@0: beginWrite(db); michael@0: } michael@0: } else if (insertIfNeeded) { michael@0: values.put(Favicons.DATE_CREATED, now); michael@0: michael@0: trace("No update, inserting favicon for URL: " + faviconUrl); michael@0: beginWrite(db); michael@0: faviconId = db.insert(TABLE_FAVICONS, null, values); michael@0: updated = 1; michael@0: } michael@0: michael@0: if (pageUrl != null) { michael@0: updateFaviconIdsForUrl(db, pageUrl, faviconId); michael@0: } michael@0: michael@0: return updated; michael@0: } michael@0: michael@0: private long insertThumbnail(Uri uri, ContentValues values) { michael@0: final String url = values.getAsString(Thumbnails.URL); michael@0: michael@0: trace("Inserting thumbnail for URL: " + url); michael@0: michael@0: DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: beginWrite(db); michael@0: return db.insertOrThrow(TABLE_THUMBNAILS, null, values); michael@0: } michael@0: michael@0: private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: return updateThumbnail(uri, values, selection, selectionArgs, michael@0: true /* insert if needed */); michael@0: } michael@0: michael@0: private int updateExistingThumbnail(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: return updateThumbnail(uri, values, selection, selectionArgs, michael@0: false /* only update, no insert */); michael@0: } michael@0: michael@0: private int updateThumbnail(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs, boolean insertIfNeeded) { michael@0: final String url = values.getAsString(Thumbnails.URL); michael@0: DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); michael@0: michael@0: trace("Updating thumbnail for URL: " + url); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: beginWrite(db); michael@0: int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs); michael@0: michael@0: if (updated == 0 && insertIfNeeded) { michael@0: trace("No update, inserting thumbnail for URL: " + url); michael@0: db.insert(TABLE_THUMBNAILS, null, values); michael@0: updated = 1; michael@0: } michael@0: michael@0: return updated; michael@0: } michael@0: michael@0: /** michael@0: * This method does not create a new transaction. Its first operation is michael@0: * guaranteed to be a write, which in the case of a new enclosing michael@0: * transaction will guarantee that a read does not need to be upgraded to michael@0: * a write. michael@0: */ michael@0: int deleteHistory(Uri uri, String selection, String[] selectionArgs) { michael@0: debug("Deleting history entry for URI: " + uri); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: if (isCallerSync(uri)) { michael@0: return db.delete(TABLE_HISTORY, selection, selectionArgs); michael@0: } michael@0: michael@0: debug("Marking history entry as deleted for URI: " + uri); michael@0: michael@0: ContentValues values = new ContentValues(); michael@0: values.put(History.IS_DELETED, 1); michael@0: michael@0: // Wipe sensitive data. michael@0: values.putNull(History.TITLE); michael@0: values.put(History.URL, ""); // Column is NOT NULL. michael@0: values.put(History.DATE_CREATED, 0); michael@0: values.put(History.DATE_LAST_VISITED, 0); michael@0: values.put(History.VISITS, 0); michael@0: values.put(History.DATE_MODIFIED, System.currentTimeMillis()); michael@0: michael@0: // Doing this UPDATE (or the DELETE above) first ensures that the michael@0: // first operation within a new enclosing transaction is a write. michael@0: // The cleanup call below will do a SELECT first, and thus would michael@0: // require the transaction to be upgraded from a reader to a writer. michael@0: // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid michael@0: // it if we can. michael@0: final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs); michael@0: try { michael@0: cleanUpSomeDeletedRecords(uri, TABLE_HISTORY); michael@0: } catch (Exception e) { michael@0: // We don't care. michael@0: Log.e(LOGTAG, "Unable to clean up deleted history records: ", e); michael@0: } michael@0: return updated; michael@0: } michael@0: michael@0: int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) { michael@0: debug("Deleting bookmarks for URI: " + uri); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: if (isCallerSync(uri)) { michael@0: beginWrite(db); michael@0: return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); michael@0: } michael@0: michael@0: debug("Marking bookmarks as deleted for URI: " + uri); michael@0: michael@0: ContentValues values = new ContentValues(); michael@0: values.put(Bookmarks.IS_DELETED, 1); michael@0: michael@0: // Doing this UPDATE (or the DELETE above) first ensures that the michael@0: // first operation within this transaction is a write. michael@0: // The cleanup call below will do a SELECT first, and thus would michael@0: // require the transaction to be upgraded from a reader to a writer. michael@0: final int updated = updateBookmarks(uri, values, selection, selectionArgs); michael@0: try { michael@0: cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS); michael@0: } catch (Exception e) { michael@0: // We don't care. michael@0: Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e); michael@0: } michael@0: return updated; michael@0: } michael@0: michael@0: int deleteFavicons(Uri uri, String selection, String[] selectionArgs) { michael@0: debug("Deleting favicons for URI: " + uri); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: return db.delete(TABLE_FAVICONS, selection, selectionArgs); michael@0: } michael@0: michael@0: int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) { michael@0: debug("Deleting thumbnails for URI: " + uri); michael@0: michael@0: final SQLiteDatabase db = getWritableDatabase(uri); michael@0: michael@0: return db.delete(TABLE_THUMBNAILS, selection, selectionArgs); michael@0: } michael@0: michael@0: int deleteUnusedImages(Uri uri) { michael@0: debug("Deleting all unused favicons and thumbnails for URI: " + uri); michael@0: michael@0: String faviconSelection = Favicons._ID + " NOT IN " michael@0: + "(SELECT " + History.FAVICON_ID michael@0: + " FROM " + TABLE_HISTORY michael@0: + " WHERE " + History.IS_DELETED + " = 0" michael@0: + " AND " + History.FAVICON_ID + " IS NOT NULL" michael@0: + " UNION ALL SELECT " + Bookmarks.FAVICON_ID michael@0: + " FROM " + TABLE_BOOKMARKS michael@0: + " WHERE " + Bookmarks.IS_DELETED + " = 0" michael@0: + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)"; michael@0: michael@0: String thumbnailSelection = Thumbnails.URL + " NOT IN " michael@0: + "(SELECT " + History.URL michael@0: + " FROM " + TABLE_HISTORY michael@0: + " WHERE " + History.IS_DELETED + " = 0" michael@0: + " AND " + History.URL + " IS NOT NULL" michael@0: + " UNION ALL SELECT " + Bookmarks.URL michael@0: + " FROM " + TABLE_BOOKMARKS michael@0: + " WHERE " + Bookmarks.IS_DELETED + " = 0" michael@0: + " AND " + Bookmarks.URL + " IS NOT NULL)"; michael@0: michael@0: return deleteFavicons(uri, faviconSelection, null) + michael@0: deleteThumbnails(uri, thumbnailSelection, null); michael@0: } michael@0: michael@0: @Override michael@0: public ContentProviderResult[] applyBatch (ArrayList operations) michael@0: throws OperationApplicationException { michael@0: final int numOperations = operations.size(); michael@0: final ContentProviderResult[] results = new ContentProviderResult[numOperations]; michael@0: michael@0: if (numOperations < 1) { michael@0: debug("applyBatch: no operations; returning immediately."); michael@0: // The original Android implementation returns a zero-length michael@0: // array in this case. We do the same. michael@0: return results; michael@0: } michael@0: michael@0: boolean failures = false; michael@0: michael@0: // We only have 1 database for all Uris that we can get. michael@0: SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri()); michael@0: michael@0: // Note that the apply() call may cause us to generate michael@0: // additional transactions for the individual operations. michael@0: // But Android's wrapper for SQLite supports nested transactions, michael@0: // so this will do the right thing. michael@0: // michael@0: // Note further that in some circumstances this can result in michael@0: // exceptions: if this transaction is first involved in reading, michael@0: // and then (naturally) tries to perform writes, SQLITE_BUSY can michael@0: // be raised. See Bug 947939 and friends. michael@0: beginBatch(db); michael@0: michael@0: for (int i = 0; i < numOperations; i++) { michael@0: try { michael@0: final ContentProviderOperation operation = operations.get(i); michael@0: results[i] = operation.apply(this, results, i); michael@0: } catch (SQLException e) { michael@0: Log.w(LOGTAG, "SQLite Exception during applyBatch.", e); michael@0: // The Android API makes it implementation-defined whether michael@0: // the failure of a single operation makes all others abort michael@0: // or not. For our use cases, best-effort operation makes michael@0: // more sense. Rolling back and forcing the caller to retry michael@0: // after it figures out what went wrong isn't very convenient michael@0: // anyway. michael@0: // Signal failed operation back, so the caller knows what michael@0: // went through and what didn't. michael@0: results[i] = new ContentProviderResult(0); michael@0: failures = true; michael@0: // http://www.sqlite.org/lang_conflict.html michael@0: // Note that we need a new transaction, subsequent operations michael@0: // on this one will fail (we're in ABORT by default, which michael@0: // isn't IGNORE). We still need to set it as successful to let michael@0: // everything before the failed op go through. michael@0: // We can't set conflict resolution on API level < 8, and even michael@0: // above 8 it requires splitting the call per operation michael@0: // (insert/update/delete). michael@0: db.setTransactionSuccessful(); michael@0: db.endTransaction(); michael@0: db.beginTransaction(); michael@0: } catch (OperationApplicationException e) { michael@0: // Repeat of above. michael@0: results[i] = new ContentProviderResult(0); michael@0: failures = true; michael@0: db.setTransactionSuccessful(); michael@0: db.endTransaction(); michael@0: db.beginTransaction(); michael@0: } michael@0: } michael@0: michael@0: trace("Flushing DB applyBatch..."); michael@0: markBatchSuccessful(db); michael@0: endBatch(db); michael@0: michael@0: if (failures) { michael@0: throw new OperationApplicationException(); michael@0: } michael@0: michael@0: return results; michael@0: } michael@0: }