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