mobile/android/base/db/BrowserProvider.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko.db;
michael@0 7
michael@0 8 import java.util.ArrayList;
michael@0 9 import java.util.Collections;
michael@0 10 import java.util.HashMap;
michael@0 11 import java.util.Map;
michael@0 12
michael@0 13 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
michael@0 14 import org.mozilla.gecko.db.BrowserContract.Combined;
michael@0 15 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
michael@0 16 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
michael@0 17 import org.mozilla.gecko.db.BrowserContract.Favicons;
michael@0 18 import org.mozilla.gecko.db.BrowserContract.History;
michael@0 19 import org.mozilla.gecko.db.BrowserContract.Schema;
michael@0 20 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
michael@0 21 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
michael@0 22 import org.mozilla.gecko.sync.Utils;
michael@0 23
michael@0 24 import android.app.SearchManager;
michael@0 25 import android.content.ContentProviderOperation;
michael@0 26 import android.content.ContentProviderResult;
michael@0 27 import android.content.ContentUris;
michael@0 28 import android.content.ContentValues;
michael@0 29 import android.content.OperationApplicationException;
michael@0 30 import android.content.UriMatcher;
michael@0 31 import android.database.Cursor;
michael@0 32 import android.database.DatabaseUtils;
michael@0 33 import android.database.MatrixCursor;
michael@0 34 import android.database.SQLException;
michael@0 35 import android.database.sqlite.SQLiteDatabase;
michael@0 36 import android.database.sqlite.SQLiteQueryBuilder;
michael@0 37 import android.net.Uri;
michael@0 38 import android.text.TextUtils;
michael@0 39 import android.util.Log;
michael@0 40
michael@0 41 public class BrowserProvider extends SharedBrowserDatabaseProvider {
michael@0 42 private static final String LOGTAG = "GeckoBrowserProvider";
michael@0 43
michael@0 44 // How many records to reposition in a single query.
michael@0 45 // This should be less than the SQLite maximum number of query variables
michael@0 46 // (currently 999) divided by the number of variables used per positioning
michael@0 47 // query (currently 3).
michael@0 48 static final int MAX_POSITION_UPDATES_PER_QUERY = 100;
michael@0 49
michael@0 50 // Minimum number of records to keep when expiring history.
michael@0 51 static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000;
michael@0 52 static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500;
michael@0 53
michael@0 54 // Minimum duration to keep when expiring.
michael@0 55 static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks.
michael@0 56 // Minimum number of thumbnails to keep around.
michael@0 57 static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15;
michael@0 58
michael@0 59 static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME;
michael@0 60 static final String TABLE_HISTORY = History.TABLE_NAME;
michael@0 61 static final String TABLE_FAVICONS = Favicons.TABLE_NAME;
michael@0 62 static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME;
michael@0 63
michael@0 64 static final String VIEW_COMBINED = Combined.VIEW_NAME;
michael@0 65 static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS;
michael@0 66 static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS;
michael@0 67 static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS;
michael@0 68
michael@0 69 static final String VIEW_FLAGS = "flags";
michael@0 70
michael@0 71 // Bookmark matches
michael@0 72 static final int BOOKMARKS = 100;
michael@0 73 static final int BOOKMARKS_ID = 101;
michael@0 74 static final int BOOKMARKS_FOLDER_ID = 102;
michael@0 75 static final int BOOKMARKS_PARENT = 103;
michael@0 76 static final int BOOKMARKS_POSITIONS = 104;
michael@0 77
michael@0 78 // History matches
michael@0 79 static final int HISTORY = 200;
michael@0 80 static final int HISTORY_ID = 201;
michael@0 81 static final int HISTORY_OLD = 202;
michael@0 82
michael@0 83 // Favicon matches
michael@0 84 static final int FAVICONS = 300;
michael@0 85 static final int FAVICON_ID = 301;
michael@0 86
michael@0 87 // Schema matches
michael@0 88 static final int SCHEMA = 400;
michael@0 89
michael@0 90 // Combined bookmarks and history matches
michael@0 91 static final int COMBINED = 500;
michael@0 92
michael@0 93 // Control matches
michael@0 94 static final int CONTROL = 600;
michael@0 95
michael@0 96 // Search Suggest matches
michael@0 97 static final int SEARCH_SUGGEST = 700;
michael@0 98
michael@0 99 // Thumbnail matches
michael@0 100 static final int THUMBNAILS = 800;
michael@0 101 static final int THUMBNAIL_ID = 801;
michael@0 102
michael@0 103 static final int FLAGS = 900;
michael@0 104
michael@0 105 static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
michael@0 106 + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
michael@0 107 + " ASC";
michael@0 108
michael@0 109 static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
michael@0 110
michael@0 111 static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
michael@0 112
michael@0 113 static final Map<String, String> BOOKMARKS_PROJECTION_MAP;
michael@0 114 static final Map<String, String> HISTORY_PROJECTION_MAP;
michael@0 115 static final Map<String, String> COMBINED_PROJECTION_MAP;
michael@0 116 static final Map<String, String> SCHEMA_PROJECTION_MAP;
michael@0 117 static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
michael@0 118 static final Map<String, String> FAVICONS_PROJECTION_MAP;
michael@0 119 static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
michael@0 120
michael@0 121 static {
michael@0 122 // We will reuse this.
michael@0 123 HashMap<String, String> map;
michael@0 124
michael@0 125 // Bookmarks
michael@0 126 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
michael@0 127 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
michael@0 128 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT);
michael@0 129 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS);
michael@0 130 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
michael@0 131
michael@0 132 map = new HashMap<String, String>();
michael@0 133 map.put(Bookmarks._ID, Bookmarks._ID);
michael@0 134 map.put(Bookmarks.TITLE, Bookmarks.TITLE);
michael@0 135 map.put(Bookmarks.URL, Bookmarks.URL);
michael@0 136 map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
michael@0 137 map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID);
michael@0 138 map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL);
michael@0 139 map.put(Bookmarks.TYPE, Bookmarks.TYPE);
michael@0 140 map.put(Bookmarks.PARENT, Bookmarks.PARENT);
michael@0 141 map.put(Bookmarks.POSITION, Bookmarks.POSITION);
michael@0 142 map.put(Bookmarks.TAGS, Bookmarks.TAGS);
michael@0 143 map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION);
michael@0 144 map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD);
michael@0 145 map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
michael@0 146 map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
michael@0 147 map.put(Bookmarks.GUID, Bookmarks.GUID);
michael@0 148 map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
michael@0 149 BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 150
michael@0 151 // History
michael@0 152 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY);
michael@0 153 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID);
michael@0 154 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD);
michael@0 155
michael@0 156 map = new HashMap<String, String>();
michael@0 157 map.put(History._ID, History._ID);
michael@0 158 map.put(History.TITLE, History.TITLE);
michael@0 159 map.put(History.URL, History.URL);
michael@0 160 map.put(History.FAVICON, History.FAVICON);
michael@0 161 map.put(History.FAVICON_ID, History.FAVICON_ID);
michael@0 162 map.put(History.FAVICON_URL, History.FAVICON_URL);
michael@0 163 map.put(History.VISITS, History.VISITS);
michael@0 164 map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
michael@0 165 map.put(History.DATE_CREATED, History.DATE_CREATED);
michael@0 166 map.put(History.DATE_MODIFIED, History.DATE_MODIFIED);
michael@0 167 map.put(History.GUID, History.GUID);
michael@0 168 map.put(History.IS_DELETED, History.IS_DELETED);
michael@0 169 HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 170
michael@0 171 // Favicons
michael@0 172 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS);
michael@0 173 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID);
michael@0 174
michael@0 175 map = new HashMap<String, String>();
michael@0 176 map.put(Favicons._ID, Favicons._ID);
michael@0 177 map.put(Favicons.URL, Favicons.URL);
michael@0 178 map.put(Favicons.DATA, Favicons.DATA);
michael@0 179 map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED);
michael@0 180 map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED);
michael@0 181 FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 182
michael@0 183 // Thumbnails
michael@0 184 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS);
michael@0 185 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID);
michael@0 186
michael@0 187 map = new HashMap<String, String>();
michael@0 188 map.put(Thumbnails._ID, Thumbnails._ID);
michael@0 189 map.put(Thumbnails.URL, Thumbnails.URL);
michael@0 190 map.put(Thumbnails.DATA, Thumbnails.DATA);
michael@0 191 THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 192
michael@0 193 // Combined bookmarks and history
michael@0 194 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED);
michael@0 195
michael@0 196 map = new HashMap<String, String>();
michael@0 197 map.put(Combined._ID, Combined._ID);
michael@0 198 map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID);
michael@0 199 map.put(Combined.HISTORY_ID, Combined.HISTORY_ID);
michael@0 200 map.put(Combined.DISPLAY, "MAX(" + Combined.DISPLAY + ") AS " + Combined.DISPLAY);
michael@0 201 map.put(Combined.URL, Combined.URL);
michael@0 202 map.put(Combined.TITLE, Combined.TITLE);
michael@0 203 map.put(Combined.VISITS, Combined.VISITS);
michael@0 204 map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
michael@0 205 map.put(Combined.FAVICON, Combined.FAVICON);
michael@0 206 map.put(Combined.FAVICON_ID, Combined.FAVICON_ID);
michael@0 207 map.put(Combined.FAVICON_URL, Combined.FAVICON_URL);
michael@0 208 COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 209
michael@0 210 // Schema
michael@0 211 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
michael@0 212
michael@0 213 map = new HashMap<String, String>();
michael@0 214 map.put(Schema.VERSION, Schema.VERSION);
michael@0 215 SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 216
michael@0 217
michael@0 218 // Control
michael@0 219 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL);
michael@0 220
michael@0 221 // Search Suggest
michael@0 222 URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
michael@0 223
michael@0 224 URI_MATCHER.addURI(BrowserContract.AUTHORITY, "flags", FLAGS);
michael@0 225
michael@0 226 map = new HashMap<String, String>();
michael@0 227 map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
michael@0 228 Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
michael@0 229 map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
michael@0 230 Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
michael@0 231 map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
michael@0 232 Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA);
michael@0 233 SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map);
michael@0 234 }
michael@0 235
michael@0 236 static final String qualifyColumn(String table, String column) {
michael@0 237 return table + "." + column;
michael@0 238 }
michael@0 239
michael@0 240 private static boolean hasFaviconsInProjection(String[] projection) {
michael@0 241 if (projection == null) return true;
michael@0 242 for (int i = 0; i < projection.length; ++i) {
michael@0 243 if (projection[i].equals(FaviconColumns.FAVICON) ||
michael@0 244 projection[i].equals(FaviconColumns.FAVICON_URL))
michael@0 245 return true;
michael@0 246 }
michael@0 247
michael@0 248 return false;
michael@0 249 }
michael@0 250
michael@0 251 // Calculate these once, at initialization. isLoggable is too expensive to
michael@0 252 // have in-line in each log call.
michael@0 253 private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
michael@0 254 private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
michael@0 255 protected static void trace(String message) {
michael@0 256 if (logVerbose) {
michael@0 257 Log.v(LOGTAG, message);
michael@0 258 }
michael@0 259 }
michael@0 260
michael@0 261 protected static void debug(String message) {
michael@0 262 if (logDebug) {
michael@0 263 Log.d(LOGTAG, message);
michael@0 264 }
michael@0 265 }
michael@0 266
michael@0 267 /**
michael@0 268 * Remove enough history items to bring the database count below <code>retain</code>,
michael@0 269 * removing no items with a modified time after <code>keepAfter</code>.
michael@0 270 *
michael@0 271 * Provide <code>keepAfter</code> less than or equal to zero to skip that check.
michael@0 272 *
michael@0 273 * Items will be removed according to an approximate frecency calculation.
michael@0 274 */
michael@0 275 private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
michael@0 276 Log.d(LOGTAG, "Expiring history.");
michael@0 277 final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY);
michael@0 278
michael@0 279 if (retain >= rows) {
michael@0 280 debug("Not expiring history: only have " + rows + " rows.");
michael@0 281 return;
michael@0 282 }
michael@0 283
michael@0 284 final String sortOrder = BrowserContract.getFrecencySortOrder(false, true);
michael@0 285 final long toRemove = rows - retain;
michael@0 286 debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");
michael@0 287
michael@0 288 final String sql;
michael@0 289 if (keepAfter > 0) {
michael@0 290 sql = "DELETE FROM " + TABLE_HISTORY + " " +
michael@0 291 "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED +") < " + keepAfter + " " +
michael@0 292 " AND " + History._ID + " IN ( SELECT " +
michael@0 293 History._ID + " FROM " + TABLE_HISTORY + " " +
michael@0 294 "ORDER BY " + sortOrder + " LIMIT " + toRemove +
michael@0 295 ")";
michael@0 296 } else {
michael@0 297 sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
michael@0 298 "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
michael@0 299 "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
michael@0 300 }
michael@0 301 trace("Deleting using query: " + sql);
michael@0 302
michael@0 303 beginWrite(db);
michael@0 304 db.execSQL(sql);
michael@0 305 }
michael@0 306
michael@0 307 /**
michael@0 308 * Remove any thumbnails that for sites that aren't likely to be ever shown.
michael@0 309 * Items will be removed according to a frecency calculation and only if they are not pinned
michael@0 310 *
michael@0 311 * Call this method within a transaction.
michael@0 312 */
michael@0 313 private void expireThumbnails(final SQLiteDatabase db) {
michael@0 314 Log.d(LOGTAG, "Expiring thumbnails.");
michael@0 315 final String sortOrder = BrowserContract.getFrecencySortOrder(true, false);
michael@0 316 final String sql = "DELETE FROM " + TABLE_THUMBNAILS +
michael@0 317 " WHERE " + Thumbnails.URL + " NOT IN ( " +
michael@0 318 " SELECT " + Combined.URL +
michael@0 319 " FROM " + Combined.VIEW_NAME +
michael@0 320 " ORDER BY " + sortOrder +
michael@0 321 " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT +
michael@0 322 ") AND " + Thumbnails.URL + " NOT IN ( " +
michael@0 323 " SELECT " + Bookmarks.URL +
michael@0 324 " FROM " + TABLE_BOOKMARKS +
michael@0 325 " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
michael@0 326 ")";
michael@0 327 trace("Clear thumbs using query: " + sql);
michael@0 328 db.execSQL(sql);
michael@0 329 }
michael@0 330
michael@0 331 private boolean shouldIncrementVisits(Uri uri) {
michael@0 332 String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
michael@0 333 return Boolean.parseBoolean(incrementVisits);
michael@0 334 }
michael@0 335
michael@0 336 @Override
michael@0 337 public String getType(Uri uri) {
michael@0 338 final int match = URI_MATCHER.match(uri);
michael@0 339
michael@0 340 trace("Getting URI type: " + uri);
michael@0 341
michael@0 342 switch (match) {
michael@0 343 case BOOKMARKS:
michael@0 344 trace("URI is BOOKMARKS: " + uri);
michael@0 345 return Bookmarks.CONTENT_TYPE;
michael@0 346 case BOOKMARKS_ID:
michael@0 347 trace("URI is BOOKMARKS_ID: " + uri);
michael@0 348 return Bookmarks.CONTENT_ITEM_TYPE;
michael@0 349 case HISTORY:
michael@0 350 trace("URI is HISTORY: " + uri);
michael@0 351 return History.CONTENT_TYPE;
michael@0 352 case HISTORY_ID:
michael@0 353 trace("URI is HISTORY_ID: " + uri);
michael@0 354 return History.CONTENT_ITEM_TYPE;
michael@0 355 case SEARCH_SUGGEST:
michael@0 356 trace("URI is SEARCH_SUGGEST: " + uri);
michael@0 357 return SearchManager.SUGGEST_MIME_TYPE;
michael@0 358 case FLAGS:
michael@0 359 trace("URI is FLAGS.");
michael@0 360 return Bookmarks.CONTENT_ITEM_TYPE;
michael@0 361 }
michael@0 362
michael@0 363 debug("URI has unrecognized type: " + uri);
michael@0 364
michael@0 365 return null;
michael@0 366 }
michael@0 367
michael@0 368 @SuppressWarnings("fallthrough")
michael@0 369 @Override
michael@0 370 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
michael@0 371 trace("Calling delete in transaction on URI: " + uri);
michael@0 372 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 373
michael@0 374 final int match = URI_MATCHER.match(uri);
michael@0 375 int deleted = 0;
michael@0 376
michael@0 377 switch (match) {
michael@0 378 case BOOKMARKS_ID:
michael@0 379 trace("Delete on BOOKMARKS_ID: " + uri);
michael@0 380
michael@0 381 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
michael@0 382 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 383 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 384 // fall through
michael@0 385 case BOOKMARKS: {
michael@0 386 trace("Deleting bookmarks: " + uri);
michael@0 387 deleted = deleteBookmarks(uri, selection, selectionArgs);
michael@0 388 deleteUnusedImages(uri);
michael@0 389 break;
michael@0 390 }
michael@0 391
michael@0 392 case HISTORY_ID:
michael@0 393 trace("Delete on HISTORY_ID: " + uri);
michael@0 394
michael@0 395 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
michael@0 396 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 397 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 398 // fall through
michael@0 399 case HISTORY: {
michael@0 400 trace("Deleting history: " + uri);
michael@0 401 beginWrite(db);
michael@0 402 deleted = deleteHistory(uri, selection, selectionArgs);
michael@0 403 deleteUnusedImages(uri);
michael@0 404 break;
michael@0 405 }
michael@0 406
michael@0 407 case HISTORY_OLD: {
michael@0 408 String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY);
michael@0 409 long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW;
michael@0 410 int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT;
michael@0 411
michael@0 412 if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) {
michael@0 413 keepAfter = 0;
michael@0 414 retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT;
michael@0 415 }
michael@0 416 expireHistory(db, retainCount, keepAfter);
michael@0 417 expireThumbnails(db);
michael@0 418 deleteUnusedImages(uri);
michael@0 419 break;
michael@0 420 }
michael@0 421
michael@0 422 case FAVICON_ID:
michael@0 423 debug("Delete on FAVICON_ID: " + uri);
michael@0 424
michael@0 425 selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?");
michael@0 426 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 427 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 428 // fall through
michael@0 429 case FAVICONS: {
michael@0 430 trace("Deleting favicons: " + uri);
michael@0 431 beginWrite(db);
michael@0 432 deleted = deleteFavicons(uri, selection, selectionArgs);
michael@0 433 break;
michael@0 434 }
michael@0 435
michael@0 436 case THUMBNAIL_ID:
michael@0 437 debug("Delete on THUMBNAIL_ID: " + uri);
michael@0 438
michael@0 439 selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?");
michael@0 440 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 441 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 442 // fall through
michael@0 443 case THUMBNAILS: {
michael@0 444 trace("Deleting thumbnails: " + uri);
michael@0 445 beginWrite(db);
michael@0 446 deleted = deleteThumbnails(uri, selection, selectionArgs);
michael@0 447 break;
michael@0 448 }
michael@0 449
michael@0 450 default:
michael@0 451 throw new UnsupportedOperationException("Unknown delete URI " + uri);
michael@0 452 }
michael@0 453
michael@0 454 debug("Deleted " + deleted + " rows for URI: " + uri);
michael@0 455
michael@0 456 return deleted;
michael@0 457 }
michael@0 458
michael@0 459 @Override
michael@0 460 public Uri insertInTransaction(Uri uri, ContentValues values) {
michael@0 461 trace("Calling insert in transaction on URI: " + uri);
michael@0 462
michael@0 463 int match = URI_MATCHER.match(uri);
michael@0 464 long id = -1;
michael@0 465
michael@0 466 switch (match) {
michael@0 467 case BOOKMARKS: {
michael@0 468 trace("Insert on BOOKMARKS: " + uri);
michael@0 469 id = insertBookmark(uri, values);
michael@0 470 break;
michael@0 471 }
michael@0 472
michael@0 473 case HISTORY: {
michael@0 474 trace("Insert on HISTORY: " + uri);
michael@0 475 id = insertHistory(uri, values);
michael@0 476 break;
michael@0 477 }
michael@0 478
michael@0 479 case FAVICONS: {
michael@0 480 trace("Insert on FAVICONS: " + uri);
michael@0 481 id = insertFavicon(uri, values);
michael@0 482 break;
michael@0 483 }
michael@0 484
michael@0 485 case THUMBNAILS: {
michael@0 486 trace("Insert on THUMBNAILS: " + uri);
michael@0 487 id = insertThumbnail(uri, values);
michael@0 488 break;
michael@0 489 }
michael@0 490
michael@0 491 default:
michael@0 492 throw new UnsupportedOperationException("Unknown insert URI " + uri);
michael@0 493 }
michael@0 494
michael@0 495 debug("Inserted ID in database: " + id);
michael@0 496
michael@0 497 if (id >= 0)
michael@0 498 return ContentUris.withAppendedId(uri, id);
michael@0 499
michael@0 500 return null;
michael@0 501 }
michael@0 502
michael@0 503 @SuppressWarnings("fallthrough")
michael@0 504 @Override
michael@0 505 public int updateInTransaction(Uri uri, ContentValues values, String selection,
michael@0 506 String[] selectionArgs) {
michael@0 507 trace("Calling update in transaction on URI: " + uri);
michael@0 508
michael@0 509 int match = URI_MATCHER.match(uri);
michael@0 510 int updated = 0;
michael@0 511
michael@0 512 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 513 switch (match) {
michael@0 514 // We provide a dedicated (hacky) API for callers to bulk-update the positions of
michael@0 515 // folder children by passing an array of GUID strings as `selectionArgs`.
michael@0 516 // Each child will have its position column set to its index in the provided array.
michael@0 517 //
michael@0 518 // This avoids callers having to issue a large number of UPDATE queries through
michael@0 519 // the usual channels. See Bug 728783.
michael@0 520 //
michael@0 521 // Note that this is decidedly not a general-purpose API; use at your own risk.
michael@0 522 // `values` and `selection` are ignored.
michael@0 523 case BOOKMARKS_POSITIONS: {
michael@0 524 debug("Update on BOOKMARKS_POSITIONS: " + uri);
michael@0 525
michael@0 526 // This already starts and finishes its own transaction.
michael@0 527 updated = updateBookmarkPositions(uri, selectionArgs);
michael@0 528 break;
michael@0 529 }
michael@0 530
michael@0 531 case BOOKMARKS_PARENT: {
michael@0 532 debug("Update on BOOKMARKS_PARENT: " + uri);
michael@0 533 beginWrite(db);
michael@0 534 updated = updateBookmarkParents(db, values, selection, selectionArgs);
michael@0 535 break;
michael@0 536 }
michael@0 537
michael@0 538 case BOOKMARKS_ID:
michael@0 539 debug("Update on BOOKMARKS_ID: " + uri);
michael@0 540
michael@0 541 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
michael@0 542 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 543 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 544 // fall through
michael@0 545 case BOOKMARKS: {
michael@0 546 debug("Updating bookmark: " + uri);
michael@0 547 if (shouldUpdateOrInsert(uri)) {
michael@0 548 updated = updateOrInsertBookmark(uri, values, selection, selectionArgs);
michael@0 549 } else {
michael@0 550 updated = updateBookmarks(uri, values, selection, selectionArgs);
michael@0 551 }
michael@0 552 break;
michael@0 553 }
michael@0 554
michael@0 555 case HISTORY_ID:
michael@0 556 debug("Update on HISTORY_ID: " + uri);
michael@0 557
michael@0 558 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
michael@0 559 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 560 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 561 // fall through
michael@0 562 case HISTORY: {
michael@0 563 debug("Updating history: " + uri);
michael@0 564 if (shouldUpdateOrInsert(uri)) {
michael@0 565 updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
michael@0 566 } else {
michael@0 567 updated = updateHistory(uri, values, selection, selectionArgs);
michael@0 568 }
michael@0 569 break;
michael@0 570 }
michael@0 571
michael@0 572 case FAVICONS: {
michael@0 573 debug("Update on FAVICONS: " + uri);
michael@0 574
michael@0 575 String url = values.getAsString(Favicons.URL);
michael@0 576 String faviconSelection = null;
michael@0 577 String[] faviconSelectionArgs = null;
michael@0 578
michael@0 579 if (!TextUtils.isEmpty(url)) {
michael@0 580 faviconSelection = Favicons.URL + " = ?";
michael@0 581 faviconSelectionArgs = new String[] { url };
michael@0 582 }
michael@0 583
michael@0 584 if (shouldUpdateOrInsert(uri)) {
michael@0 585 updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
michael@0 586 } else {
michael@0 587 updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);
michael@0 588 }
michael@0 589 break;
michael@0 590 }
michael@0 591
michael@0 592 case THUMBNAILS: {
michael@0 593 debug("Update on THUMBNAILS: " + uri);
michael@0 594
michael@0 595 String url = values.getAsString(Thumbnails.URL);
michael@0 596
michael@0 597 // if no URL is provided, update all of the entries
michael@0 598 if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) {
michael@0 599 updated = updateExistingThumbnail(uri, values, null, null);
michael@0 600 } else if (shouldUpdateOrInsert(uri)) {
michael@0 601 updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?",
michael@0 602 new String[] { url });
michael@0 603 } else {
michael@0 604 updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
michael@0 605 new String[] { url });
michael@0 606 }
michael@0 607 break;
michael@0 608 }
michael@0 609
michael@0 610 default:
michael@0 611 throw new UnsupportedOperationException("Unknown update URI " + uri);
michael@0 612 }
michael@0 613
michael@0 614 debug("Updated " + updated + " rows for URI: " + uri);
michael@0 615 return updated;
michael@0 616 }
michael@0 617
michael@0 618 @Override
michael@0 619 public Cursor query(Uri uri, String[] projection, String selection,
michael@0 620 String[] selectionArgs, String sortOrder) {
michael@0 621 SQLiteDatabase db = getReadableDatabase(uri);
michael@0 622 final int match = URI_MATCHER.match(uri);
michael@0 623
michael@0 624 // The first selectionArgs value is the URI for which to query.
michael@0 625 if (match == FLAGS) {
michael@0 626 // We don't need the QB below for this.
michael@0 627 //
michael@0 628 // There are three possible kinds of bookmarks:
michael@0 629 // * Regular bookmarks
michael@0 630 // * Bookmarks whose parent is FIXED_READING_LIST_ID (reading list items)
michael@0 631 // * Bookmarks whose parent is FIXED_PINNED_LIST_ID (pinned items).
michael@0 632 //
michael@0 633 // Although SQLite doesn't have an aggregate operator for bitwise-OR, we're
michael@0 634 // using disjoint flags, so we can simply use SUM and DISTINCT to get the
michael@0 635 // flags we need.
michael@0 636 // We turn parents into flags according to the three kinds, above.
michael@0 637 //
michael@0 638 // When this query is extended to support queries across multiple tables, simply
michael@0 639 // extend it to look like
michael@0 640 //
michael@0 641 // SELECT COALESCE((SELECT ...), 0) | COALESCE(...) | ...
michael@0 642
michael@0 643 final boolean includeDeleted = shouldShowDeleted(uri);
michael@0 644 final String query = "SELECT COALESCE(SUM(flag), 0) AS flags " +
michael@0 645 "FROM ( SELECT DISTINCT CASE" +
michael@0 646 " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_READING_LIST_ID +
michael@0 647 " THEN " + Bookmarks.FLAG_READING +
michael@0 648
michael@0 649 " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
michael@0 650 " THEN " + Bookmarks.FLAG_PINNED +
michael@0 651
michael@0 652 " ELSE " + Bookmarks.FLAG_BOOKMARK +
michael@0 653 " END flag " +
michael@0 654 "FROM " + TABLE_BOOKMARKS + " WHERE " +
michael@0 655 Bookmarks.URL + " = ? " +
michael@0 656 (includeDeleted ? "" : ("AND " + Bookmarks.IS_DELETED + " = 0")) +
michael@0 657 ")";
michael@0 658
michael@0 659 return db.rawQuery(query, selectionArgs);
michael@0 660 }
michael@0 661
michael@0 662 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
michael@0 663 String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
michael@0 664 String groupBy = null;
michael@0 665
michael@0 666 switch (match) {
michael@0 667 case BOOKMARKS_FOLDER_ID:
michael@0 668 case BOOKMARKS_ID:
michael@0 669 case BOOKMARKS: {
michael@0 670 debug("Query is on bookmarks: " + uri);
michael@0 671
michael@0 672 if (match == BOOKMARKS_ID) {
michael@0 673 selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
michael@0 674 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 675 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 676 } else if (match == BOOKMARKS_FOLDER_ID) {
michael@0 677 selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
michael@0 678 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 679 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 680 }
michael@0 681
michael@0 682 if (!shouldShowDeleted(uri))
michael@0 683 selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
michael@0 684
michael@0 685 if (TextUtils.isEmpty(sortOrder)) {
michael@0 686 sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
michael@0 687 } else {
michael@0 688 debug("Using sort order " + sortOrder + ".");
michael@0 689 }
michael@0 690
michael@0 691 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
michael@0 692
michael@0 693 if (hasFaviconsInProjection(projection))
michael@0 694 qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS);
michael@0 695 else
michael@0 696 qb.setTables(TABLE_BOOKMARKS);
michael@0 697
michael@0 698 break;
michael@0 699 }
michael@0 700
michael@0 701 case HISTORY_ID:
michael@0 702 selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
michael@0 703 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 704 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 705 // fall through
michael@0 706 case HISTORY: {
michael@0 707 debug("Query is on history: " + uri);
michael@0 708
michael@0 709 if (!shouldShowDeleted(uri))
michael@0 710 selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
michael@0 711
michael@0 712 if (TextUtils.isEmpty(sortOrder))
michael@0 713 sortOrder = DEFAULT_HISTORY_SORT_ORDER;
michael@0 714
michael@0 715 qb.setProjectionMap(HISTORY_PROJECTION_MAP);
michael@0 716
michael@0 717 if (hasFaviconsInProjection(projection))
michael@0 718 qb.setTables(VIEW_HISTORY_WITH_FAVICONS);
michael@0 719 else
michael@0 720 qb.setTables(TABLE_HISTORY);
michael@0 721
michael@0 722 break;
michael@0 723 }
michael@0 724
michael@0 725 case FAVICON_ID:
michael@0 726 selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?");
michael@0 727 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 728 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 729 // fall through
michael@0 730 case FAVICONS: {
michael@0 731 debug("Query is on favicons: " + uri);
michael@0 732
michael@0 733 qb.setProjectionMap(FAVICONS_PROJECTION_MAP);
michael@0 734 qb.setTables(TABLE_FAVICONS);
michael@0 735
michael@0 736 break;
michael@0 737 }
michael@0 738
michael@0 739 case THUMBNAIL_ID:
michael@0 740 selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?");
michael@0 741 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 742 new String[] { Long.toString(ContentUris.parseId(uri)) });
michael@0 743 // fall through
michael@0 744 case THUMBNAILS: {
michael@0 745 debug("Query is on thumbnails: " + uri);
michael@0 746
michael@0 747 qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP);
michael@0 748 qb.setTables(TABLE_THUMBNAILS);
michael@0 749
michael@0 750 break;
michael@0 751 }
michael@0 752
michael@0 753 case SCHEMA: {
michael@0 754 debug("Query is on schema.");
michael@0 755 MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION });
michael@0 756 schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION);
michael@0 757
michael@0 758 return schemaCursor;
michael@0 759 }
michael@0 760
michael@0 761 case COMBINED: {
michael@0 762 debug("Query is on combined: " + uri);
michael@0 763
michael@0 764 if (TextUtils.isEmpty(sortOrder))
michael@0 765 sortOrder = DEFAULT_HISTORY_SORT_ORDER;
michael@0 766
michael@0 767 // This will avoid duplicate entries in the awesomebar
michael@0 768 // results when a history entry has multiple bookmarks.
michael@0 769 groupBy = Combined.URL;
michael@0 770
michael@0 771 qb.setProjectionMap(COMBINED_PROJECTION_MAP);
michael@0 772
michael@0 773 if (hasFaviconsInProjection(projection))
michael@0 774 qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
michael@0 775 else
michael@0 776 qb.setTables(Combined.VIEW_NAME);
michael@0 777
michael@0 778 break;
michael@0 779 }
michael@0 780
michael@0 781 case SEARCH_SUGGEST: {
michael@0 782 debug("Query is on search suggest: " + uri);
michael@0 783 selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
michael@0 784 Combined.TITLE + " LIKE ?)");
michael@0 785
michael@0 786 String keyword = uri.getLastPathSegment();
michael@0 787 if (keyword == null)
michael@0 788 keyword = "";
michael@0 789
michael@0 790 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
michael@0 791 new String[] { "%" + keyword + "%",
michael@0 792 "%" + keyword + "%" });
michael@0 793
michael@0 794 if (TextUtils.isEmpty(sortOrder))
michael@0 795 sortOrder = DEFAULT_HISTORY_SORT_ORDER;
michael@0 796
michael@0 797 qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP);
michael@0 798 qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
michael@0 799
michael@0 800 break;
michael@0 801 }
michael@0 802
michael@0 803 default:
michael@0 804 throw new UnsupportedOperationException("Unknown query URI " + uri);
michael@0 805 }
michael@0 806
michael@0 807 trace("Running built query.");
michael@0 808 Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
michael@0 809 null, sortOrder, limit);
michael@0 810 cursor.setNotificationUri(getContext().getContentResolver(),
michael@0 811 BrowserContract.AUTHORITY_URI);
michael@0 812
michael@0 813 return cursor;
michael@0 814 }
michael@0 815
michael@0 816 /**
michael@0 817 * Update the positions of bookmarks in batches.
michael@0 818 *
michael@0 819 * Begins and ends its own transactions.
michael@0 820 *
michael@0 821 * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
michael@0 822 */
michael@0 823 int updateBookmarkPositions(Uri uri, String[] guids) {
michael@0 824 if (guids == null) {
michael@0 825 return 0;
michael@0 826 }
michael@0 827
michael@0 828 int guidsCount = guids.length;
michael@0 829 if (guidsCount == 0) {
michael@0 830 return 0;
michael@0 831 }
michael@0 832
michael@0 833 int offset = 0;
michael@0 834 int updated = 0;
michael@0 835
michael@0 836 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 837 db.beginTransaction();
michael@0 838
michael@0 839 while (offset < guidsCount) {
michael@0 840 try {
michael@0 841 updated += updateBookmarkPositionsInTransaction(db, guids, offset,
michael@0 842 MAX_POSITION_UPDATES_PER_QUERY);
michael@0 843 } catch (SQLException e) {
michael@0 844 Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e);
michael@0 845
michael@0 846 // Need to restart the transaction.
michael@0 847 // The only way a caller knows that anything failed is that the
michael@0 848 // returned update count will be smaller than the requested
michael@0 849 // number of records.
michael@0 850 db.setTransactionSuccessful();
michael@0 851 db.endTransaction();
michael@0 852
michael@0 853 db.beginTransaction();
michael@0 854 }
michael@0 855
michael@0 856 offset += MAX_POSITION_UPDATES_PER_QUERY;
michael@0 857 }
michael@0 858
michael@0 859 db.setTransactionSuccessful();
michael@0 860 db.endTransaction();
michael@0 861
michael@0 862 return updated;
michael@0 863 }
michael@0 864
michael@0 865 /**
michael@0 866 * Construct and execute an update expression that will modify the positions
michael@0 867 * of records in-place.
michael@0 868 */
michael@0 869 private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
michael@0 870 final int offset, final int max) {
michael@0 871 int guidsCount = guids.length;
michael@0 872 int processCount = Math.min(max, guidsCount - offset);
michael@0 873
michael@0 874 // Each must appear twice: once in a CASE, and once in the IN clause.
michael@0 875 String[] args = new String[processCount * 2];
michael@0 876 System.arraycopy(guids, offset, args, 0, processCount);
michael@0 877 System.arraycopy(guids, offset, args, processCount, processCount);
michael@0 878
michael@0 879 StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS +
michael@0 880 " SET " + Bookmarks.POSITION +
michael@0 881 " = CASE guid");
michael@0 882
michael@0 883 // Build the CASE statement body for GUID/index pairs from offset up to
michael@0 884 // the computed limit.
michael@0 885 final int end = offset + processCount;
michael@0 886 int i = offset;
michael@0 887 for (; i < end; ++i) {
michael@0 888 if (guids[i] == null) {
michael@0 889 // We don't want to issue the query if not every GUID is specified.
michael@0 890 debug("updateBookmarkPositions called with null GUID at index " + i);
michael@0 891 return 0;
michael@0 892 }
michael@0 893 b.append(" WHEN ? THEN " + i);
michael@0 894 }
michael@0 895
michael@0 896 // TODO: use computeSQLInClause
michael@0 897 b.append(" END WHERE " + Bookmarks.GUID + " IN (");
michael@0 898 i = 1;
michael@0 899 while (i++ < processCount) {
michael@0 900 b.append("?, ");
michael@0 901 }
michael@0 902 b.append("?)");
michael@0 903 db.execSQL(b.toString(), args);
michael@0 904
michael@0 905 // We can't easily get a modified count without calling something like changes().
michael@0 906 return processCount;
michael@0 907 }
michael@0 908
michael@0 909 /**
michael@0 910 * Construct an update expression that will modify the parents of any records
michael@0 911 * that match.
michael@0 912 */
michael@0 913 private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) {
michael@0 914 trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
michael@0 915 String where = Bookmarks._ID + " IN (" +
michael@0 916 " SELECT DISTINCT " + Bookmarks.PARENT +
michael@0 917 " FROM " + TABLE_BOOKMARKS +
michael@0 918 " WHERE " + selection + " )";
michael@0 919 return db.update(TABLE_BOOKMARKS, values, where, selectionArgs);
michael@0 920 }
michael@0 921
michael@0 922 long insertBookmark(Uri uri, ContentValues values) {
michael@0 923 // Generate values if not specified. Don't overwrite
michael@0 924 // if specified by caller.
michael@0 925 long now = System.currentTimeMillis();
michael@0 926 if (!values.containsKey(Bookmarks.DATE_CREATED)) {
michael@0 927 values.put(Bookmarks.DATE_CREATED, now);
michael@0 928 }
michael@0 929
michael@0 930 if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
michael@0 931 values.put(Bookmarks.DATE_MODIFIED, now);
michael@0 932 }
michael@0 933
michael@0 934 if (!values.containsKey(Bookmarks.GUID)) {
michael@0 935 values.put(Bookmarks.GUID, Utils.generateGuid());
michael@0 936 }
michael@0 937
michael@0 938 if (!values.containsKey(Bookmarks.POSITION)) {
michael@0 939 debug("Inserting bookmark with no position for URI");
michael@0 940 values.put(Bookmarks.POSITION,
michael@0 941 Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION));
michael@0 942 }
michael@0 943
michael@0 944 String url = values.getAsString(Bookmarks.URL);
michael@0 945
michael@0 946 debug("Inserting bookmark in database with URL: " + url);
michael@0 947 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 948 beginWrite(db);
michael@0 949 return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
michael@0 950 }
michael@0 951
michael@0 952
michael@0 953 int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
michael@0 954 String[] selectionArgs) {
michael@0 955 int updated = updateBookmarks(uri, values, selection, selectionArgs);
michael@0 956 if (updated > 0) {
michael@0 957 return updated;
michael@0 958 }
michael@0 959
michael@0 960 // Transaction already begun by updateBookmarks.
michael@0 961 if (0 <= insertBookmark(uri, values)) {
michael@0 962 // We 'updated' one row.
michael@0 963 return 1;
michael@0 964 }
michael@0 965
michael@0 966 // If something went wrong, then we updated zero rows.
michael@0 967 return 0;
michael@0 968 }
michael@0 969
michael@0 970 int updateBookmarks(Uri uri, ContentValues values, String selection,
michael@0 971 String[] selectionArgs) {
michael@0 972 trace("Updating bookmarks on URI: " + uri);
michael@0 973
michael@0 974 final String[] bookmarksProjection = new String[] {
michael@0 975 Bookmarks._ID, // 0
michael@0 976 };
michael@0 977
michael@0 978 if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
michael@0 979 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
michael@0 980 }
michael@0 981
michael@0 982 trace("Querying bookmarks to update on URI: " + uri);
michael@0 983 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 984
michael@0 985 // Compute matching IDs.
michael@0 986 final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
michael@0 987 selection, selectionArgs, null, null, null);
michael@0 988
michael@0 989 // Now that we're done reading, open a transaction.
michael@0 990 final String inClause;
michael@0 991 try {
michael@0 992 inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
michael@0 993 } finally {
michael@0 994 cursor.close();
michael@0 995 }
michael@0 996
michael@0 997 beginWrite(db);
michael@0 998 return db.update(TABLE_BOOKMARKS, values, inClause, null);
michael@0 999 }
michael@0 1000
michael@0 1001 long insertHistory(Uri uri, ContentValues values) {
michael@0 1002 final long now = System.currentTimeMillis();
michael@0 1003 values.put(History.DATE_CREATED, now);
michael@0 1004 values.put(History.DATE_MODIFIED, now);
michael@0 1005
michael@0 1006 // Generate GUID for new history entry. Don't override specified GUIDs.
michael@0 1007 if (!values.containsKey(History.GUID)) {
michael@0 1008 values.put(History.GUID, Utils.generateGuid());
michael@0 1009 }
michael@0 1010
michael@0 1011 String url = values.getAsString(History.URL);
michael@0 1012
michael@0 1013 debug("Inserting history in database with URL: " + url);
michael@0 1014 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1015 beginWrite(db);
michael@0 1016 return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
michael@0 1017 }
michael@0 1018
michael@0 1019 int updateOrInsertHistory(Uri uri, ContentValues values, String selection,
michael@0 1020 String[] selectionArgs) {
michael@0 1021 final int updated = updateHistory(uri, values, selection, selectionArgs);
michael@0 1022 if (updated > 0) {
michael@0 1023 return updated;
michael@0 1024 }
michael@0 1025
michael@0 1026 // Insert a new entry if necessary
michael@0 1027 if (!values.containsKey(History.VISITS)) {
michael@0 1028 values.put(History.VISITS, 1);
michael@0 1029 }
michael@0 1030 if (!values.containsKey(History.TITLE)) {
michael@0 1031 values.put(History.TITLE, values.getAsString(History.URL));
michael@0 1032 }
michael@0 1033
michael@0 1034 if (0 <= insertHistory(uri, values)) {
michael@0 1035 return 1;
michael@0 1036 }
michael@0 1037
michael@0 1038 return 0;
michael@0 1039 }
michael@0 1040
michael@0 1041 int updateHistory(Uri uri, ContentValues values, String selection,
michael@0 1042 String[] selectionArgs) {
michael@0 1043 trace("Updating history on URI: " + uri);
michael@0 1044
michael@0 1045 int updated = 0;
michael@0 1046
michael@0 1047 final String[] historyProjection = new String[] {
michael@0 1048 History._ID, // 0
michael@0 1049 History.URL, // 1
michael@0 1050 History.VISITS // 2
michael@0 1051 };
michael@0 1052
michael@0 1053 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1054 final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
michael@0 1055 selectionArgs, null, null, null);
michael@0 1056
michael@0 1057 try {
michael@0 1058 if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
michael@0 1059 values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
michael@0 1060 }
michael@0 1061
michael@0 1062 while (cursor.moveToNext()) {
michael@0 1063 long id = cursor.getLong(0);
michael@0 1064
michael@0 1065 trace("Updating history entry with ID: " + id);
michael@0 1066
michael@0 1067 if (shouldIncrementVisits(uri)) {
michael@0 1068 long existing = cursor.getLong(2);
michael@0 1069 Long additional = values.getAsLong(History.VISITS);
michael@0 1070
michael@0 1071 // Increment visit count by a specified amount, or default to increment by 1
michael@0 1072 values.put(History.VISITS, existing + ((additional != null) ? additional.longValue() : 1));
michael@0 1073 }
michael@0 1074
michael@0 1075 updated += db.update(TABLE_HISTORY, values, "_id = ?",
michael@0 1076 new String[] { Long.toString(id) });
michael@0 1077 }
michael@0 1078 } finally {
michael@0 1079 cursor.close();
michael@0 1080 }
michael@0 1081
michael@0 1082 return updated;
michael@0 1083 }
michael@0 1084
michael@0 1085 private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) {
michael@0 1086 ContentValues updateValues = new ContentValues(1);
michael@0 1087 updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
michael@0 1088 db.update(TABLE_HISTORY,
michael@0 1089 updateValues,
michael@0 1090 History.URL + " = ?",
michael@0 1091 new String[] { pageUrl });
michael@0 1092 db.update(TABLE_BOOKMARKS,
michael@0 1093 updateValues,
michael@0 1094 Bookmarks.URL + " = ?",
michael@0 1095 new String[] { pageUrl });
michael@0 1096 }
michael@0 1097
michael@0 1098 long insertFavicon(Uri uri, ContentValues values) {
michael@0 1099 return insertFavicon(getWritableDatabase(uri), values);
michael@0 1100 }
michael@0 1101
michael@0 1102 long insertFavicon(SQLiteDatabase db, ContentValues values) {
michael@0 1103 // This method is a dupicate of BrowserDatabaseHelper.insertFavicon.
michael@0 1104 // If changes are needed, please update both
michael@0 1105 String faviconUrl = values.getAsString(Favicons.URL);
michael@0 1106 String pageUrl = null;
michael@0 1107
michael@0 1108 trace("Inserting favicon for URL: " + faviconUrl);
michael@0 1109
michael@0 1110 DBUtils.stripEmptyByteArray(values, Favicons.DATA);
michael@0 1111
michael@0 1112 // Extract the page URL from the ContentValues
michael@0 1113 if (values.containsKey(Favicons.PAGE_URL)) {
michael@0 1114 pageUrl = values.getAsString(Favicons.PAGE_URL);
michael@0 1115 values.remove(Favicons.PAGE_URL);
michael@0 1116 }
michael@0 1117
michael@0 1118 // If no URL is provided, insert using the default one.
michael@0 1119 if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) {
michael@0 1120 values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl));
michael@0 1121 }
michael@0 1122
michael@0 1123 final long now = System.currentTimeMillis();
michael@0 1124 values.put(Favicons.DATE_CREATED, now);
michael@0 1125 values.put(Favicons.DATE_MODIFIED, now);
michael@0 1126
michael@0 1127 beginWrite(db);
michael@0 1128 final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
michael@0 1129
michael@0 1130 if (pageUrl != null) {
michael@0 1131 updateFaviconIdsForUrl(db, pageUrl, faviconId);
michael@0 1132 }
michael@0 1133 return faviconId;
michael@0 1134 }
michael@0 1135
michael@0 1136 int updateOrInsertFavicon(Uri uri, ContentValues values, String selection,
michael@0 1137 String[] selectionArgs) {
michael@0 1138 return updateFavicon(uri, values, selection, selectionArgs,
michael@0 1139 true /* insert if needed */);
michael@0 1140 }
michael@0 1141
michael@0 1142 int updateExistingFavicon(Uri uri, ContentValues values, String selection,
michael@0 1143 String[] selectionArgs) {
michael@0 1144 return updateFavicon(uri, values, selection, selectionArgs,
michael@0 1145 false /* only update, no insert */);
michael@0 1146 }
michael@0 1147
michael@0 1148 int updateFavicon(Uri uri, ContentValues values, String selection,
michael@0 1149 String[] selectionArgs, boolean insertIfNeeded) {
michael@0 1150 String faviconUrl = values.getAsString(Favicons.URL);
michael@0 1151 String pageUrl = null;
michael@0 1152 int updated = 0;
michael@0 1153 Long faviconId = null;
michael@0 1154 long now = System.currentTimeMillis();
michael@0 1155
michael@0 1156 trace("Updating favicon for URL: " + faviconUrl);
michael@0 1157
michael@0 1158 DBUtils.stripEmptyByteArray(values, Favicons.DATA);
michael@0 1159
michael@0 1160 // Extract the page URL from the ContentValues
michael@0 1161 if (values.containsKey(Favicons.PAGE_URL)) {
michael@0 1162 pageUrl = values.getAsString(Favicons.PAGE_URL);
michael@0 1163 values.remove(Favicons.PAGE_URL);
michael@0 1164 }
michael@0 1165
michael@0 1166 values.put(Favicons.DATE_MODIFIED, now);
michael@0 1167
michael@0 1168 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1169
michael@0 1170 // If there's no favicon URL given and we're inserting if needed, skip
michael@0 1171 // the update and only do an insert (otherwise all rows would be
michael@0 1172 // updated).
michael@0 1173 if (!(insertIfNeeded && (faviconUrl == null))) {
michael@0 1174 updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs);
michael@0 1175 }
michael@0 1176
michael@0 1177 if (updated > 0) {
michael@0 1178 if ((faviconUrl != null) && (pageUrl != null)) {
michael@0 1179 final Cursor cursor = db.query(TABLE_FAVICONS,
michael@0 1180 new String[] { Favicons._ID },
michael@0 1181 Favicons.URL + " = ?",
michael@0 1182 new String[] { faviconUrl },
michael@0 1183 null, null, null);
michael@0 1184 try {
michael@0 1185 if (cursor.moveToFirst()) {
michael@0 1186 faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID));
michael@0 1187 }
michael@0 1188 } finally {
michael@0 1189 cursor.close();
michael@0 1190 }
michael@0 1191 }
michael@0 1192 if (pageUrl != null) {
michael@0 1193 beginWrite(db);
michael@0 1194 }
michael@0 1195 } else if (insertIfNeeded) {
michael@0 1196 values.put(Favicons.DATE_CREATED, now);
michael@0 1197
michael@0 1198 trace("No update, inserting favicon for URL: " + faviconUrl);
michael@0 1199 beginWrite(db);
michael@0 1200 faviconId = db.insert(TABLE_FAVICONS, null, values);
michael@0 1201 updated = 1;
michael@0 1202 }
michael@0 1203
michael@0 1204 if (pageUrl != null) {
michael@0 1205 updateFaviconIdsForUrl(db, pageUrl, faviconId);
michael@0 1206 }
michael@0 1207
michael@0 1208 return updated;
michael@0 1209 }
michael@0 1210
michael@0 1211 private long insertThumbnail(Uri uri, ContentValues values) {
michael@0 1212 final String url = values.getAsString(Thumbnails.URL);
michael@0 1213
michael@0 1214 trace("Inserting thumbnail for URL: " + url);
michael@0 1215
michael@0 1216 DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
michael@0 1217
michael@0 1218 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1219 beginWrite(db);
michael@0 1220 return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
michael@0 1221 }
michael@0 1222
michael@0 1223 private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
michael@0 1224 String[] selectionArgs) {
michael@0 1225 return updateThumbnail(uri, values, selection, selectionArgs,
michael@0 1226 true /* insert if needed */);
michael@0 1227 }
michael@0 1228
michael@0 1229 private int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
michael@0 1230 String[] selectionArgs) {
michael@0 1231 return updateThumbnail(uri, values, selection, selectionArgs,
michael@0 1232 false /* only update, no insert */);
michael@0 1233 }
michael@0 1234
michael@0 1235 private int updateThumbnail(Uri uri, ContentValues values, String selection,
michael@0 1236 String[] selectionArgs, boolean insertIfNeeded) {
michael@0 1237 final String url = values.getAsString(Thumbnails.URL);
michael@0 1238 DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
michael@0 1239
michael@0 1240 trace("Updating thumbnail for URL: " + url);
michael@0 1241
michael@0 1242 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1243 beginWrite(db);
michael@0 1244 int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
michael@0 1245
michael@0 1246 if (updated == 0 && insertIfNeeded) {
michael@0 1247 trace("No update, inserting thumbnail for URL: " + url);
michael@0 1248 db.insert(TABLE_THUMBNAILS, null, values);
michael@0 1249 updated = 1;
michael@0 1250 }
michael@0 1251
michael@0 1252 return updated;
michael@0 1253 }
michael@0 1254
michael@0 1255 /**
michael@0 1256 * This method does not create a new transaction. Its first operation is
michael@0 1257 * guaranteed to be a write, which in the case of a new enclosing
michael@0 1258 * transaction will guarantee that a read does not need to be upgraded to
michael@0 1259 * a write.
michael@0 1260 */
michael@0 1261 int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
michael@0 1262 debug("Deleting history entry for URI: " + uri);
michael@0 1263
michael@0 1264 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1265
michael@0 1266 if (isCallerSync(uri)) {
michael@0 1267 return db.delete(TABLE_HISTORY, selection, selectionArgs);
michael@0 1268 }
michael@0 1269
michael@0 1270 debug("Marking history entry as deleted for URI: " + uri);
michael@0 1271
michael@0 1272 ContentValues values = new ContentValues();
michael@0 1273 values.put(History.IS_DELETED, 1);
michael@0 1274
michael@0 1275 // Wipe sensitive data.
michael@0 1276 values.putNull(History.TITLE);
michael@0 1277 values.put(History.URL, ""); // Column is NOT NULL.
michael@0 1278 values.put(History.DATE_CREATED, 0);
michael@0 1279 values.put(History.DATE_LAST_VISITED, 0);
michael@0 1280 values.put(History.VISITS, 0);
michael@0 1281 values.put(History.DATE_MODIFIED, System.currentTimeMillis());
michael@0 1282
michael@0 1283 // Doing this UPDATE (or the DELETE above) first ensures that the
michael@0 1284 // first operation within a new enclosing transaction is a write.
michael@0 1285 // The cleanup call below will do a SELECT first, and thus would
michael@0 1286 // require the transaction to be upgraded from a reader to a writer.
michael@0 1287 // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
michael@0 1288 // it if we can.
michael@0 1289 final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
michael@0 1290 try {
michael@0 1291 cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
michael@0 1292 } catch (Exception e) {
michael@0 1293 // We don't care.
michael@0 1294 Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
michael@0 1295 }
michael@0 1296 return updated;
michael@0 1297 }
michael@0 1298
michael@0 1299 int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
michael@0 1300 debug("Deleting bookmarks for URI: " + uri);
michael@0 1301
michael@0 1302 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1303
michael@0 1304 if (isCallerSync(uri)) {
michael@0 1305 beginWrite(db);
michael@0 1306 return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
michael@0 1307 }
michael@0 1308
michael@0 1309 debug("Marking bookmarks as deleted for URI: " + uri);
michael@0 1310
michael@0 1311 ContentValues values = new ContentValues();
michael@0 1312 values.put(Bookmarks.IS_DELETED, 1);
michael@0 1313
michael@0 1314 // Doing this UPDATE (or the DELETE above) first ensures that the
michael@0 1315 // first operation within this transaction is a write.
michael@0 1316 // The cleanup call below will do a SELECT first, and thus would
michael@0 1317 // require the transaction to be upgraded from a reader to a writer.
michael@0 1318 final int updated = updateBookmarks(uri, values, selection, selectionArgs);
michael@0 1319 try {
michael@0 1320 cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
michael@0 1321 } catch (Exception e) {
michael@0 1322 // We don't care.
michael@0 1323 Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
michael@0 1324 }
michael@0 1325 return updated;
michael@0 1326 }
michael@0 1327
michael@0 1328 int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
michael@0 1329 debug("Deleting favicons for URI: " + uri);
michael@0 1330
michael@0 1331 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1332
michael@0 1333 return db.delete(TABLE_FAVICONS, selection, selectionArgs);
michael@0 1334 }
michael@0 1335
michael@0 1336 int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) {
michael@0 1337 debug("Deleting thumbnails for URI: " + uri);
michael@0 1338
michael@0 1339 final SQLiteDatabase db = getWritableDatabase(uri);
michael@0 1340
michael@0 1341 return db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
michael@0 1342 }
michael@0 1343
michael@0 1344 int deleteUnusedImages(Uri uri) {
michael@0 1345 debug("Deleting all unused favicons and thumbnails for URI: " + uri);
michael@0 1346
michael@0 1347 String faviconSelection = Favicons._ID + " NOT IN "
michael@0 1348 + "(SELECT " + History.FAVICON_ID
michael@0 1349 + " FROM " + TABLE_HISTORY
michael@0 1350 + " WHERE " + History.IS_DELETED + " = 0"
michael@0 1351 + " AND " + History.FAVICON_ID + " IS NOT NULL"
michael@0 1352 + " UNION ALL SELECT " + Bookmarks.FAVICON_ID
michael@0 1353 + " FROM " + TABLE_BOOKMARKS
michael@0 1354 + " WHERE " + Bookmarks.IS_DELETED + " = 0"
michael@0 1355 + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)";
michael@0 1356
michael@0 1357 String thumbnailSelection = Thumbnails.URL + " NOT IN "
michael@0 1358 + "(SELECT " + History.URL
michael@0 1359 + " FROM " + TABLE_HISTORY
michael@0 1360 + " WHERE " + History.IS_DELETED + " = 0"
michael@0 1361 + " AND " + History.URL + " IS NOT NULL"
michael@0 1362 + " UNION ALL SELECT " + Bookmarks.URL
michael@0 1363 + " FROM " + TABLE_BOOKMARKS
michael@0 1364 + " WHERE " + Bookmarks.IS_DELETED + " = 0"
michael@0 1365 + " AND " + Bookmarks.URL + " IS NOT NULL)";
michael@0 1366
michael@0 1367 return deleteFavicons(uri, faviconSelection, null) +
michael@0 1368 deleteThumbnails(uri, thumbnailSelection, null);
michael@0 1369 }
michael@0 1370
michael@0 1371 @Override
michael@0 1372 public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
michael@0 1373 throws OperationApplicationException {
michael@0 1374 final int numOperations = operations.size();
michael@0 1375 final ContentProviderResult[] results = new ContentProviderResult[numOperations];
michael@0 1376
michael@0 1377 if (numOperations < 1) {
michael@0 1378 debug("applyBatch: no operations; returning immediately.");
michael@0 1379 // The original Android implementation returns a zero-length
michael@0 1380 // array in this case. We do the same.
michael@0 1381 return results;
michael@0 1382 }
michael@0 1383
michael@0 1384 boolean failures = false;
michael@0 1385
michael@0 1386 // We only have 1 database for all Uris that we can get.
michael@0 1387 SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri());
michael@0 1388
michael@0 1389 // Note that the apply() call may cause us to generate
michael@0 1390 // additional transactions for the individual operations.
michael@0 1391 // But Android's wrapper for SQLite supports nested transactions,
michael@0 1392 // so this will do the right thing.
michael@0 1393 //
michael@0 1394 // Note further that in some circumstances this can result in
michael@0 1395 // exceptions: if this transaction is first involved in reading,
michael@0 1396 // and then (naturally) tries to perform writes, SQLITE_BUSY can
michael@0 1397 // be raised. See Bug 947939 and friends.
michael@0 1398 beginBatch(db);
michael@0 1399
michael@0 1400 for (int i = 0; i < numOperations; i++) {
michael@0 1401 try {
michael@0 1402 final ContentProviderOperation operation = operations.get(i);
michael@0 1403 results[i] = operation.apply(this, results, i);
michael@0 1404 } catch (SQLException e) {
michael@0 1405 Log.w(LOGTAG, "SQLite Exception during applyBatch.", e);
michael@0 1406 // The Android API makes it implementation-defined whether
michael@0 1407 // the failure of a single operation makes all others abort
michael@0 1408 // or not. For our use cases, best-effort operation makes
michael@0 1409 // more sense. Rolling back and forcing the caller to retry
michael@0 1410 // after it figures out what went wrong isn't very convenient
michael@0 1411 // anyway.
michael@0 1412 // Signal failed operation back, so the caller knows what
michael@0 1413 // went through and what didn't.
michael@0 1414 results[i] = new ContentProviderResult(0);
michael@0 1415 failures = true;
michael@0 1416 // http://www.sqlite.org/lang_conflict.html
michael@0 1417 // Note that we need a new transaction, subsequent operations
michael@0 1418 // on this one will fail (we're in ABORT by default, which
michael@0 1419 // isn't IGNORE). We still need to set it as successful to let
michael@0 1420 // everything before the failed op go through.
michael@0 1421 // We can't set conflict resolution on API level < 8, and even
michael@0 1422 // above 8 it requires splitting the call per operation
michael@0 1423 // (insert/update/delete).
michael@0 1424 db.setTransactionSuccessful();
michael@0 1425 db.endTransaction();
michael@0 1426 db.beginTransaction();
michael@0 1427 } catch (OperationApplicationException e) {
michael@0 1428 // Repeat of above.
michael@0 1429 results[i] = new ContentProviderResult(0);
michael@0 1430 failures = true;
michael@0 1431 db.setTransactionSuccessful();
michael@0 1432 db.endTransaction();
michael@0 1433 db.beginTransaction();
michael@0 1434 }
michael@0 1435 }
michael@0 1436
michael@0 1437 trace("Flushing DB applyBatch...");
michael@0 1438 markBatchSuccessful(db);
michael@0 1439 endBatch(db);
michael@0 1440
michael@0 1441 if (failures) {
michael@0 1442 throw new OperationApplicationException();
michael@0 1443 }
michael@0 1444
michael@0 1445 return results;
michael@0 1446 }
michael@0 1447 }

mercurial