1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/db/BrowserProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1447 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.db; 1.10 + 1.11 +import java.util.ArrayList; 1.12 +import java.util.Collections; 1.13 +import java.util.HashMap; 1.14 +import java.util.Map; 1.15 + 1.16 +import org.mozilla.gecko.db.BrowserContract.Bookmarks; 1.17 +import org.mozilla.gecko.db.BrowserContract.Combined; 1.18 +import org.mozilla.gecko.db.BrowserContract.CommonColumns; 1.19 +import org.mozilla.gecko.db.BrowserContract.FaviconColumns; 1.20 +import org.mozilla.gecko.db.BrowserContract.Favicons; 1.21 +import org.mozilla.gecko.db.BrowserContract.History; 1.22 +import org.mozilla.gecko.db.BrowserContract.Schema; 1.23 +import org.mozilla.gecko.db.BrowserContract.SyncColumns; 1.24 +import org.mozilla.gecko.db.BrowserContract.Thumbnails; 1.25 +import org.mozilla.gecko.sync.Utils; 1.26 + 1.27 +import android.app.SearchManager; 1.28 +import android.content.ContentProviderOperation; 1.29 +import android.content.ContentProviderResult; 1.30 +import android.content.ContentUris; 1.31 +import android.content.ContentValues; 1.32 +import android.content.OperationApplicationException; 1.33 +import android.content.UriMatcher; 1.34 +import android.database.Cursor; 1.35 +import android.database.DatabaseUtils; 1.36 +import android.database.MatrixCursor; 1.37 +import android.database.SQLException; 1.38 +import android.database.sqlite.SQLiteDatabase; 1.39 +import android.database.sqlite.SQLiteQueryBuilder; 1.40 +import android.net.Uri; 1.41 +import android.text.TextUtils; 1.42 +import android.util.Log; 1.43 + 1.44 +public class BrowserProvider extends SharedBrowserDatabaseProvider { 1.45 + private static final String LOGTAG = "GeckoBrowserProvider"; 1.46 + 1.47 + // How many records to reposition in a single query. 1.48 + // This should be less than the SQLite maximum number of query variables 1.49 + // (currently 999) divided by the number of variables used per positioning 1.50 + // query (currently 3). 1.51 + static final int MAX_POSITION_UPDATES_PER_QUERY = 100; 1.52 + 1.53 + // Minimum number of records to keep when expiring history. 1.54 + static final int DEFAULT_EXPIRY_RETAIN_COUNT = 2000; 1.55 + static final int AGGRESSIVE_EXPIRY_RETAIN_COUNT = 500; 1.56 + 1.57 + // Minimum duration to keep when expiring. 1.58 + static final long DEFAULT_EXPIRY_PRESERVE_WINDOW = 1000L * 60L * 60L * 24L * 28L; // Four weeks. 1.59 + // Minimum number of thumbnails to keep around. 1.60 + static final int DEFAULT_EXPIRY_THUMBNAIL_COUNT = 15; 1.61 + 1.62 + static final String TABLE_BOOKMARKS = Bookmarks.TABLE_NAME; 1.63 + static final String TABLE_HISTORY = History.TABLE_NAME; 1.64 + static final String TABLE_FAVICONS = Favicons.TABLE_NAME; 1.65 + static final String TABLE_THUMBNAILS = Thumbnails.TABLE_NAME; 1.66 + 1.67 + static final String VIEW_COMBINED = Combined.VIEW_NAME; 1.68 + static final String VIEW_BOOKMARKS_WITH_FAVICONS = Bookmarks.VIEW_WITH_FAVICONS; 1.69 + static final String VIEW_HISTORY_WITH_FAVICONS = History.VIEW_WITH_FAVICONS; 1.70 + static final String VIEW_COMBINED_WITH_FAVICONS = Combined.VIEW_WITH_FAVICONS; 1.71 + 1.72 + static final String VIEW_FLAGS = "flags"; 1.73 + 1.74 + // Bookmark matches 1.75 + static final int BOOKMARKS = 100; 1.76 + static final int BOOKMARKS_ID = 101; 1.77 + static final int BOOKMARKS_FOLDER_ID = 102; 1.78 + static final int BOOKMARKS_PARENT = 103; 1.79 + static final int BOOKMARKS_POSITIONS = 104; 1.80 + 1.81 + // History matches 1.82 + static final int HISTORY = 200; 1.83 + static final int HISTORY_ID = 201; 1.84 + static final int HISTORY_OLD = 202; 1.85 + 1.86 + // Favicon matches 1.87 + static final int FAVICONS = 300; 1.88 + static final int FAVICON_ID = 301; 1.89 + 1.90 + // Schema matches 1.91 + static final int SCHEMA = 400; 1.92 + 1.93 + // Combined bookmarks and history matches 1.94 + static final int COMBINED = 500; 1.95 + 1.96 + // Control matches 1.97 + static final int CONTROL = 600; 1.98 + 1.99 + // Search Suggest matches 1.100 + static final int SEARCH_SUGGEST = 700; 1.101 + 1.102 + // Thumbnail matches 1.103 + static final int THUMBNAILS = 800; 1.104 + static final int THUMBNAIL_ID = 801; 1.105 + 1.106 + static final int FLAGS = 900; 1.107 + 1.108 + static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE 1.109 + + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID 1.110 + + " ASC"; 1.111 + 1.112 + static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC"; 1.113 + 1.114 + static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); 1.115 + 1.116 + static final Map<String, String> BOOKMARKS_PROJECTION_MAP; 1.117 + static final Map<String, String> HISTORY_PROJECTION_MAP; 1.118 + static final Map<String, String> COMBINED_PROJECTION_MAP; 1.119 + static final Map<String, String> SCHEMA_PROJECTION_MAP; 1.120 + static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP; 1.121 + static final Map<String, String> FAVICONS_PROJECTION_MAP; 1.122 + static final Map<String, String> THUMBNAILS_PROJECTION_MAP; 1.123 + 1.124 + static { 1.125 + // We will reuse this. 1.126 + HashMap<String, String> map; 1.127 + 1.128 + // Bookmarks 1.129 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS); 1.130 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID); 1.131 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/parents", BOOKMARKS_PARENT); 1.132 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/positions", BOOKMARKS_POSITIONS); 1.133 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID); 1.134 + 1.135 + map = new HashMap<String, String>(); 1.136 + map.put(Bookmarks._ID, Bookmarks._ID); 1.137 + map.put(Bookmarks.TITLE, Bookmarks.TITLE); 1.138 + map.put(Bookmarks.URL, Bookmarks.URL); 1.139 + map.put(Bookmarks.FAVICON, Bookmarks.FAVICON); 1.140 + map.put(Bookmarks.FAVICON_ID, Bookmarks.FAVICON_ID); 1.141 + map.put(Bookmarks.FAVICON_URL, Bookmarks.FAVICON_URL); 1.142 + map.put(Bookmarks.TYPE, Bookmarks.TYPE); 1.143 + map.put(Bookmarks.PARENT, Bookmarks.PARENT); 1.144 + map.put(Bookmarks.POSITION, Bookmarks.POSITION); 1.145 + map.put(Bookmarks.TAGS, Bookmarks.TAGS); 1.146 + map.put(Bookmarks.DESCRIPTION, Bookmarks.DESCRIPTION); 1.147 + map.put(Bookmarks.KEYWORD, Bookmarks.KEYWORD); 1.148 + map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED); 1.149 + map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED); 1.150 + map.put(Bookmarks.GUID, Bookmarks.GUID); 1.151 + map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED); 1.152 + BOOKMARKS_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.153 + 1.154 + // History 1.155 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history", HISTORY); 1.156 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/#", HISTORY_ID); 1.157 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "history/old", HISTORY_OLD); 1.158 + 1.159 + map = new HashMap<String, String>(); 1.160 + map.put(History._ID, History._ID); 1.161 + map.put(History.TITLE, History.TITLE); 1.162 + map.put(History.URL, History.URL); 1.163 + map.put(History.FAVICON, History.FAVICON); 1.164 + map.put(History.FAVICON_ID, History.FAVICON_ID); 1.165 + map.put(History.FAVICON_URL, History.FAVICON_URL); 1.166 + map.put(History.VISITS, History.VISITS); 1.167 + map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED); 1.168 + map.put(History.DATE_CREATED, History.DATE_CREATED); 1.169 + map.put(History.DATE_MODIFIED, History.DATE_MODIFIED); 1.170 + map.put(History.GUID, History.GUID); 1.171 + map.put(History.IS_DELETED, History.IS_DELETED); 1.172 + HISTORY_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.173 + 1.174 + // Favicons 1.175 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS); 1.176 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID); 1.177 + 1.178 + map = new HashMap<String, String>(); 1.179 + map.put(Favicons._ID, Favicons._ID); 1.180 + map.put(Favicons.URL, Favicons.URL); 1.181 + map.put(Favicons.DATA, Favicons.DATA); 1.182 + map.put(Favicons.DATE_CREATED, Favicons.DATE_CREATED); 1.183 + map.put(Favicons.DATE_MODIFIED, Favicons.DATE_MODIFIED); 1.184 + FAVICONS_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.185 + 1.186 + // Thumbnails 1.187 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS); 1.188 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID); 1.189 + 1.190 + map = new HashMap<String, String>(); 1.191 + map.put(Thumbnails._ID, Thumbnails._ID); 1.192 + map.put(Thumbnails.URL, Thumbnails.URL); 1.193 + map.put(Thumbnails.DATA, Thumbnails.DATA); 1.194 + THUMBNAILS_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.195 + 1.196 + // Combined bookmarks and history 1.197 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED); 1.198 + 1.199 + map = new HashMap<String, String>(); 1.200 + map.put(Combined._ID, Combined._ID); 1.201 + map.put(Combined.BOOKMARK_ID, Combined.BOOKMARK_ID); 1.202 + map.put(Combined.HISTORY_ID, Combined.HISTORY_ID); 1.203 + map.put(Combined.DISPLAY, "MAX(" + Combined.DISPLAY + ") AS " + Combined.DISPLAY); 1.204 + map.put(Combined.URL, Combined.URL); 1.205 + map.put(Combined.TITLE, Combined.TITLE); 1.206 + map.put(Combined.VISITS, Combined.VISITS); 1.207 + map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED); 1.208 + map.put(Combined.FAVICON, Combined.FAVICON); 1.209 + map.put(Combined.FAVICON_ID, Combined.FAVICON_ID); 1.210 + map.put(Combined.FAVICON_URL, Combined.FAVICON_URL); 1.211 + COMBINED_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.212 + 1.213 + // Schema 1.214 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA); 1.215 + 1.216 + map = new HashMap<String, String>(); 1.217 + map.put(Schema.VERSION, Schema.VERSION); 1.218 + SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.219 + 1.220 + 1.221 + // Control 1.222 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL); 1.223 + 1.224 + // Search Suggest 1.225 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST); 1.226 + 1.227 + URI_MATCHER.addURI(BrowserContract.AUTHORITY, "flags", FLAGS); 1.228 + 1.229 + map = new HashMap<String, String>(); 1.230 + map.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 1.231 + Combined.TITLE + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); 1.232 + map.put(SearchManager.SUGGEST_COLUMN_TEXT_2_URL, 1.233 + Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2_URL); 1.234 + map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, 1.235 + Combined.URL + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA); 1.236 + SEARCH_SUGGEST_PROJECTION_MAP = Collections.unmodifiableMap(map); 1.237 + } 1.238 + 1.239 + static final String qualifyColumn(String table, String column) { 1.240 + return table + "." + column; 1.241 + } 1.242 + 1.243 + private static boolean hasFaviconsInProjection(String[] projection) { 1.244 + if (projection == null) return true; 1.245 + for (int i = 0; i < projection.length; ++i) { 1.246 + if (projection[i].equals(FaviconColumns.FAVICON) || 1.247 + projection[i].equals(FaviconColumns.FAVICON_URL)) 1.248 + return true; 1.249 + } 1.250 + 1.251 + return false; 1.252 + } 1.253 + 1.254 + // Calculate these once, at initialization. isLoggable is too expensive to 1.255 + // have in-line in each log call. 1.256 + private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); 1.257 + private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); 1.258 + protected static void trace(String message) { 1.259 + if (logVerbose) { 1.260 + Log.v(LOGTAG, message); 1.261 + } 1.262 + } 1.263 + 1.264 + protected static void debug(String message) { 1.265 + if (logDebug) { 1.266 + Log.d(LOGTAG, message); 1.267 + } 1.268 + } 1.269 + 1.270 + /** 1.271 + * Remove enough history items to bring the database count below <code>retain</code>, 1.272 + * removing no items with a modified time after <code>keepAfter</code>. 1.273 + * 1.274 + * Provide <code>keepAfter</code> less than or equal to zero to skip that check. 1.275 + * 1.276 + * Items will be removed according to an approximate frecency calculation. 1.277 + */ 1.278 + private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) { 1.279 + Log.d(LOGTAG, "Expiring history."); 1.280 + final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY); 1.281 + 1.282 + if (retain >= rows) { 1.283 + debug("Not expiring history: only have " + rows + " rows."); 1.284 + return; 1.285 + } 1.286 + 1.287 + final String sortOrder = BrowserContract.getFrecencySortOrder(false, true); 1.288 + final long toRemove = rows - retain; 1.289 + debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + "."); 1.290 + 1.291 + final String sql; 1.292 + if (keepAfter > 0) { 1.293 + sql = "DELETE FROM " + TABLE_HISTORY + " " + 1.294 + "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED +") < " + keepAfter + " " + 1.295 + " AND " + History._ID + " IN ( SELECT " + 1.296 + History._ID + " FROM " + TABLE_HISTORY + " " + 1.297 + "ORDER BY " + sortOrder + " LIMIT " + toRemove + 1.298 + ")"; 1.299 + } else { 1.300 + sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " + 1.301 + "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " + 1.302 + "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")"; 1.303 + } 1.304 + trace("Deleting using query: " + sql); 1.305 + 1.306 + beginWrite(db); 1.307 + db.execSQL(sql); 1.308 + } 1.309 + 1.310 + /** 1.311 + * Remove any thumbnails that for sites that aren't likely to be ever shown. 1.312 + * Items will be removed according to a frecency calculation and only if they are not pinned 1.313 + * 1.314 + * Call this method within a transaction. 1.315 + */ 1.316 + private void expireThumbnails(final SQLiteDatabase db) { 1.317 + Log.d(LOGTAG, "Expiring thumbnails."); 1.318 + final String sortOrder = BrowserContract.getFrecencySortOrder(true, false); 1.319 + final String sql = "DELETE FROM " + TABLE_THUMBNAILS + 1.320 + " WHERE " + Thumbnails.URL + " NOT IN ( " + 1.321 + " SELECT " + Combined.URL + 1.322 + " FROM " + Combined.VIEW_NAME + 1.323 + " ORDER BY " + sortOrder + 1.324 + " LIMIT " + DEFAULT_EXPIRY_THUMBNAIL_COUNT + 1.325 + ") AND " + Thumbnails.URL + " NOT IN ( " + 1.326 + " SELECT " + Bookmarks.URL + 1.327 + " FROM " + TABLE_BOOKMARKS + 1.328 + " WHERE " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + 1.329 + ")"; 1.330 + trace("Clear thumbs using query: " + sql); 1.331 + db.execSQL(sql); 1.332 + } 1.333 + 1.334 + private boolean shouldIncrementVisits(Uri uri) { 1.335 + String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS); 1.336 + return Boolean.parseBoolean(incrementVisits); 1.337 + } 1.338 + 1.339 + @Override 1.340 + public String getType(Uri uri) { 1.341 + final int match = URI_MATCHER.match(uri); 1.342 + 1.343 + trace("Getting URI type: " + uri); 1.344 + 1.345 + switch (match) { 1.346 + case BOOKMARKS: 1.347 + trace("URI is BOOKMARKS: " + uri); 1.348 + return Bookmarks.CONTENT_TYPE; 1.349 + case BOOKMARKS_ID: 1.350 + trace("URI is BOOKMARKS_ID: " + uri); 1.351 + return Bookmarks.CONTENT_ITEM_TYPE; 1.352 + case HISTORY: 1.353 + trace("URI is HISTORY: " + uri); 1.354 + return History.CONTENT_TYPE; 1.355 + case HISTORY_ID: 1.356 + trace("URI is HISTORY_ID: " + uri); 1.357 + return History.CONTENT_ITEM_TYPE; 1.358 + case SEARCH_SUGGEST: 1.359 + trace("URI is SEARCH_SUGGEST: " + uri); 1.360 + return SearchManager.SUGGEST_MIME_TYPE; 1.361 + case FLAGS: 1.362 + trace("URI is FLAGS."); 1.363 + return Bookmarks.CONTENT_ITEM_TYPE; 1.364 + } 1.365 + 1.366 + debug("URI has unrecognized type: " + uri); 1.367 + 1.368 + return null; 1.369 + } 1.370 + 1.371 + @SuppressWarnings("fallthrough") 1.372 + @Override 1.373 + public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) { 1.374 + trace("Calling delete in transaction on URI: " + uri); 1.375 + final SQLiteDatabase db = getWritableDatabase(uri); 1.376 + 1.377 + final int match = URI_MATCHER.match(uri); 1.378 + int deleted = 0; 1.379 + 1.380 + switch (match) { 1.381 + case BOOKMARKS_ID: 1.382 + trace("Delete on BOOKMARKS_ID: " + uri); 1.383 + 1.384 + selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); 1.385 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.386 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.387 + // fall through 1.388 + case BOOKMARKS: { 1.389 + trace("Deleting bookmarks: " + uri); 1.390 + deleted = deleteBookmarks(uri, selection, selectionArgs); 1.391 + deleteUnusedImages(uri); 1.392 + break; 1.393 + } 1.394 + 1.395 + case HISTORY_ID: 1.396 + trace("Delete on HISTORY_ID: " + uri); 1.397 + 1.398 + selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); 1.399 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.400 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.401 + // fall through 1.402 + case HISTORY: { 1.403 + trace("Deleting history: " + uri); 1.404 + beginWrite(db); 1.405 + deleted = deleteHistory(uri, selection, selectionArgs); 1.406 + deleteUnusedImages(uri); 1.407 + break; 1.408 + } 1.409 + 1.410 + case HISTORY_OLD: { 1.411 + String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY); 1.412 + long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW; 1.413 + int retainCount = DEFAULT_EXPIRY_RETAIN_COUNT; 1.414 + 1.415 + if (BrowserContract.ExpirePriority.AGGRESSIVE.toString().equals(priority)) { 1.416 + keepAfter = 0; 1.417 + retainCount = AGGRESSIVE_EXPIRY_RETAIN_COUNT; 1.418 + } 1.419 + expireHistory(db, retainCount, keepAfter); 1.420 + expireThumbnails(db); 1.421 + deleteUnusedImages(uri); 1.422 + break; 1.423 + } 1.424 + 1.425 + case FAVICON_ID: 1.426 + debug("Delete on FAVICON_ID: " + uri); 1.427 + 1.428 + selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?"); 1.429 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.430 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.431 + // fall through 1.432 + case FAVICONS: { 1.433 + trace("Deleting favicons: " + uri); 1.434 + beginWrite(db); 1.435 + deleted = deleteFavicons(uri, selection, selectionArgs); 1.436 + break; 1.437 + } 1.438 + 1.439 + case THUMBNAIL_ID: 1.440 + debug("Delete on THUMBNAIL_ID: " + uri); 1.441 + 1.442 + selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?"); 1.443 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.444 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.445 + // fall through 1.446 + case THUMBNAILS: { 1.447 + trace("Deleting thumbnails: " + uri); 1.448 + beginWrite(db); 1.449 + deleted = deleteThumbnails(uri, selection, selectionArgs); 1.450 + break; 1.451 + } 1.452 + 1.453 + default: 1.454 + throw new UnsupportedOperationException("Unknown delete URI " + uri); 1.455 + } 1.456 + 1.457 + debug("Deleted " + deleted + " rows for URI: " + uri); 1.458 + 1.459 + return deleted; 1.460 + } 1.461 + 1.462 + @Override 1.463 + public Uri insertInTransaction(Uri uri, ContentValues values) { 1.464 + trace("Calling insert in transaction on URI: " + uri); 1.465 + 1.466 + int match = URI_MATCHER.match(uri); 1.467 + long id = -1; 1.468 + 1.469 + switch (match) { 1.470 + case BOOKMARKS: { 1.471 + trace("Insert on BOOKMARKS: " + uri); 1.472 + id = insertBookmark(uri, values); 1.473 + break; 1.474 + } 1.475 + 1.476 + case HISTORY: { 1.477 + trace("Insert on HISTORY: " + uri); 1.478 + id = insertHistory(uri, values); 1.479 + break; 1.480 + } 1.481 + 1.482 + case FAVICONS: { 1.483 + trace("Insert on FAVICONS: " + uri); 1.484 + id = insertFavicon(uri, values); 1.485 + break; 1.486 + } 1.487 + 1.488 + case THUMBNAILS: { 1.489 + trace("Insert on THUMBNAILS: " + uri); 1.490 + id = insertThumbnail(uri, values); 1.491 + break; 1.492 + } 1.493 + 1.494 + default: 1.495 + throw new UnsupportedOperationException("Unknown insert URI " + uri); 1.496 + } 1.497 + 1.498 + debug("Inserted ID in database: " + id); 1.499 + 1.500 + if (id >= 0) 1.501 + return ContentUris.withAppendedId(uri, id); 1.502 + 1.503 + return null; 1.504 + } 1.505 + 1.506 + @SuppressWarnings("fallthrough") 1.507 + @Override 1.508 + public int updateInTransaction(Uri uri, ContentValues values, String selection, 1.509 + String[] selectionArgs) { 1.510 + trace("Calling update in transaction on URI: " + uri); 1.511 + 1.512 + int match = URI_MATCHER.match(uri); 1.513 + int updated = 0; 1.514 + 1.515 + final SQLiteDatabase db = getWritableDatabase(uri); 1.516 + switch (match) { 1.517 + // We provide a dedicated (hacky) API for callers to bulk-update the positions of 1.518 + // folder children by passing an array of GUID strings as `selectionArgs`. 1.519 + // Each child will have its position column set to its index in the provided array. 1.520 + // 1.521 + // This avoids callers having to issue a large number of UPDATE queries through 1.522 + // the usual channels. See Bug 728783. 1.523 + // 1.524 + // Note that this is decidedly not a general-purpose API; use at your own risk. 1.525 + // `values` and `selection` are ignored. 1.526 + case BOOKMARKS_POSITIONS: { 1.527 + debug("Update on BOOKMARKS_POSITIONS: " + uri); 1.528 + 1.529 + // This already starts and finishes its own transaction. 1.530 + updated = updateBookmarkPositions(uri, selectionArgs); 1.531 + break; 1.532 + } 1.533 + 1.534 + case BOOKMARKS_PARENT: { 1.535 + debug("Update on BOOKMARKS_PARENT: " + uri); 1.536 + beginWrite(db); 1.537 + updated = updateBookmarkParents(db, values, selection, selectionArgs); 1.538 + break; 1.539 + } 1.540 + 1.541 + case BOOKMARKS_ID: 1.542 + debug("Update on BOOKMARKS_ID: " + uri); 1.543 + 1.544 + selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?"); 1.545 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.546 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.547 + // fall through 1.548 + case BOOKMARKS: { 1.549 + debug("Updating bookmark: " + uri); 1.550 + if (shouldUpdateOrInsert(uri)) { 1.551 + updated = updateOrInsertBookmark(uri, values, selection, selectionArgs); 1.552 + } else { 1.553 + updated = updateBookmarks(uri, values, selection, selectionArgs); 1.554 + } 1.555 + break; 1.556 + } 1.557 + 1.558 + case HISTORY_ID: 1.559 + debug("Update on HISTORY_ID: " + uri); 1.560 + 1.561 + selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?"); 1.562 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.563 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.564 + // fall through 1.565 + case HISTORY: { 1.566 + debug("Updating history: " + uri); 1.567 + if (shouldUpdateOrInsert(uri)) { 1.568 + updated = updateOrInsertHistory(uri, values, selection, selectionArgs); 1.569 + } else { 1.570 + updated = updateHistory(uri, values, selection, selectionArgs); 1.571 + } 1.572 + break; 1.573 + } 1.574 + 1.575 + case FAVICONS: { 1.576 + debug("Update on FAVICONS: " + uri); 1.577 + 1.578 + String url = values.getAsString(Favicons.URL); 1.579 + String faviconSelection = null; 1.580 + String[] faviconSelectionArgs = null; 1.581 + 1.582 + if (!TextUtils.isEmpty(url)) { 1.583 + faviconSelection = Favicons.URL + " = ?"; 1.584 + faviconSelectionArgs = new String[] { url }; 1.585 + } 1.586 + 1.587 + if (shouldUpdateOrInsert(uri)) { 1.588 + updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs); 1.589 + } else { 1.590 + updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs); 1.591 + } 1.592 + break; 1.593 + } 1.594 + 1.595 + case THUMBNAILS: { 1.596 + debug("Update on THUMBNAILS: " + uri); 1.597 + 1.598 + String url = values.getAsString(Thumbnails.URL); 1.599 + 1.600 + // if no URL is provided, update all of the entries 1.601 + if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) { 1.602 + updated = updateExistingThumbnail(uri, values, null, null); 1.603 + } else if (shouldUpdateOrInsert(uri)) { 1.604 + updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?", 1.605 + new String[] { url }); 1.606 + } else { 1.607 + updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?", 1.608 + new String[] { url }); 1.609 + } 1.610 + break; 1.611 + } 1.612 + 1.613 + default: 1.614 + throw new UnsupportedOperationException("Unknown update URI " + uri); 1.615 + } 1.616 + 1.617 + debug("Updated " + updated + " rows for URI: " + uri); 1.618 + return updated; 1.619 + } 1.620 + 1.621 + @Override 1.622 + public Cursor query(Uri uri, String[] projection, String selection, 1.623 + String[] selectionArgs, String sortOrder) { 1.624 + SQLiteDatabase db = getReadableDatabase(uri); 1.625 + final int match = URI_MATCHER.match(uri); 1.626 + 1.627 + // The first selectionArgs value is the URI for which to query. 1.628 + if (match == FLAGS) { 1.629 + // We don't need the QB below for this. 1.630 + // 1.631 + // There are three possible kinds of bookmarks: 1.632 + // * Regular bookmarks 1.633 + // * Bookmarks whose parent is FIXED_READING_LIST_ID (reading list items) 1.634 + // * Bookmarks whose parent is FIXED_PINNED_LIST_ID (pinned items). 1.635 + // 1.636 + // Although SQLite doesn't have an aggregate operator for bitwise-OR, we're 1.637 + // using disjoint flags, so we can simply use SUM and DISTINCT to get the 1.638 + // flags we need. 1.639 + // We turn parents into flags according to the three kinds, above. 1.640 + // 1.641 + // When this query is extended to support queries across multiple tables, simply 1.642 + // extend it to look like 1.643 + // 1.644 + // SELECT COALESCE((SELECT ...), 0) | COALESCE(...) | ... 1.645 + 1.646 + final boolean includeDeleted = shouldShowDeleted(uri); 1.647 + final String query = "SELECT COALESCE(SUM(flag), 0) AS flags " + 1.648 + "FROM ( SELECT DISTINCT CASE" + 1.649 + " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_READING_LIST_ID + 1.650 + " THEN " + Bookmarks.FLAG_READING + 1.651 + 1.652 + " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID + 1.653 + " THEN " + Bookmarks.FLAG_PINNED + 1.654 + 1.655 + " ELSE " + Bookmarks.FLAG_BOOKMARK + 1.656 + " END flag " + 1.657 + "FROM " + TABLE_BOOKMARKS + " WHERE " + 1.658 + Bookmarks.URL + " = ? " + 1.659 + (includeDeleted ? "" : ("AND " + Bookmarks.IS_DELETED + " = 0")) + 1.660 + ")"; 1.661 + 1.662 + return db.rawQuery(query, selectionArgs); 1.663 + } 1.664 + 1.665 + SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 1.666 + String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT); 1.667 + String groupBy = null; 1.668 + 1.669 + switch (match) { 1.670 + case BOOKMARKS_FOLDER_ID: 1.671 + case BOOKMARKS_ID: 1.672 + case BOOKMARKS: { 1.673 + debug("Query is on bookmarks: " + uri); 1.674 + 1.675 + if (match == BOOKMARKS_ID) { 1.676 + selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?"); 1.677 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.678 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.679 + } else if (match == BOOKMARKS_FOLDER_ID) { 1.680 + selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?"); 1.681 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.682 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.683 + } 1.684 + 1.685 + if (!shouldShowDeleted(uri)) 1.686 + selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection); 1.687 + 1.688 + if (TextUtils.isEmpty(sortOrder)) { 1.689 + sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER; 1.690 + } else { 1.691 + debug("Using sort order " + sortOrder + "."); 1.692 + } 1.693 + 1.694 + qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP); 1.695 + 1.696 + if (hasFaviconsInProjection(projection)) 1.697 + qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS); 1.698 + else 1.699 + qb.setTables(TABLE_BOOKMARKS); 1.700 + 1.701 + break; 1.702 + } 1.703 + 1.704 + case HISTORY_ID: 1.705 + selection = DBUtils.concatenateWhere(selection, History._ID + " = ?"); 1.706 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.707 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.708 + // fall through 1.709 + case HISTORY: { 1.710 + debug("Query is on history: " + uri); 1.711 + 1.712 + if (!shouldShowDeleted(uri)) 1.713 + selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection); 1.714 + 1.715 + if (TextUtils.isEmpty(sortOrder)) 1.716 + sortOrder = DEFAULT_HISTORY_SORT_ORDER; 1.717 + 1.718 + qb.setProjectionMap(HISTORY_PROJECTION_MAP); 1.719 + 1.720 + if (hasFaviconsInProjection(projection)) 1.721 + qb.setTables(VIEW_HISTORY_WITH_FAVICONS); 1.722 + else 1.723 + qb.setTables(TABLE_HISTORY); 1.724 + 1.725 + break; 1.726 + } 1.727 + 1.728 + case FAVICON_ID: 1.729 + selection = DBUtils.concatenateWhere(selection, Favicons._ID + " = ?"); 1.730 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.731 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.732 + // fall through 1.733 + case FAVICONS: { 1.734 + debug("Query is on favicons: " + uri); 1.735 + 1.736 + qb.setProjectionMap(FAVICONS_PROJECTION_MAP); 1.737 + qb.setTables(TABLE_FAVICONS); 1.738 + 1.739 + break; 1.740 + } 1.741 + 1.742 + case THUMBNAIL_ID: 1.743 + selection = DBUtils.concatenateWhere(selection, Thumbnails._ID + " = ?"); 1.744 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.745 + new String[] { Long.toString(ContentUris.parseId(uri)) }); 1.746 + // fall through 1.747 + case THUMBNAILS: { 1.748 + debug("Query is on thumbnails: " + uri); 1.749 + 1.750 + qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP); 1.751 + qb.setTables(TABLE_THUMBNAILS); 1.752 + 1.753 + break; 1.754 + } 1.755 + 1.756 + case SCHEMA: { 1.757 + debug("Query is on schema."); 1.758 + MatrixCursor schemaCursor = new MatrixCursor(new String[] { Schema.VERSION }); 1.759 + schemaCursor.newRow().add(BrowserDatabaseHelper.DATABASE_VERSION); 1.760 + 1.761 + return schemaCursor; 1.762 + } 1.763 + 1.764 + case COMBINED: { 1.765 + debug("Query is on combined: " + uri); 1.766 + 1.767 + if (TextUtils.isEmpty(sortOrder)) 1.768 + sortOrder = DEFAULT_HISTORY_SORT_ORDER; 1.769 + 1.770 + // This will avoid duplicate entries in the awesomebar 1.771 + // results when a history entry has multiple bookmarks. 1.772 + groupBy = Combined.URL; 1.773 + 1.774 + qb.setProjectionMap(COMBINED_PROJECTION_MAP); 1.775 + 1.776 + if (hasFaviconsInProjection(projection)) 1.777 + qb.setTables(VIEW_COMBINED_WITH_FAVICONS); 1.778 + else 1.779 + qb.setTables(Combined.VIEW_NAME); 1.780 + 1.781 + break; 1.782 + } 1.783 + 1.784 + case SEARCH_SUGGEST: { 1.785 + debug("Query is on search suggest: " + uri); 1.786 + selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " + 1.787 + Combined.TITLE + " LIKE ?)"); 1.788 + 1.789 + String keyword = uri.getLastPathSegment(); 1.790 + if (keyword == null) 1.791 + keyword = ""; 1.792 + 1.793 + selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, 1.794 + new String[] { "%" + keyword + "%", 1.795 + "%" + keyword + "%" }); 1.796 + 1.797 + if (TextUtils.isEmpty(sortOrder)) 1.798 + sortOrder = DEFAULT_HISTORY_SORT_ORDER; 1.799 + 1.800 + qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP); 1.801 + qb.setTables(VIEW_COMBINED_WITH_FAVICONS); 1.802 + 1.803 + break; 1.804 + } 1.805 + 1.806 + default: 1.807 + throw new UnsupportedOperationException("Unknown query URI " + uri); 1.808 + } 1.809 + 1.810 + trace("Running built query."); 1.811 + Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, 1.812 + null, sortOrder, limit); 1.813 + cursor.setNotificationUri(getContext().getContentResolver(), 1.814 + BrowserContract.AUTHORITY_URI); 1.815 + 1.816 + return cursor; 1.817 + } 1.818 + 1.819 + /** 1.820 + * Update the positions of bookmarks in batches. 1.821 + * 1.822 + * Begins and ends its own transactions. 1.823 + * 1.824 + * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int) 1.825 + */ 1.826 + int updateBookmarkPositions(Uri uri, String[] guids) { 1.827 + if (guids == null) { 1.828 + return 0; 1.829 + } 1.830 + 1.831 + int guidsCount = guids.length; 1.832 + if (guidsCount == 0) { 1.833 + return 0; 1.834 + } 1.835 + 1.836 + int offset = 0; 1.837 + int updated = 0; 1.838 + 1.839 + final SQLiteDatabase db = getWritableDatabase(uri); 1.840 + db.beginTransaction(); 1.841 + 1.842 + while (offset < guidsCount) { 1.843 + try { 1.844 + updated += updateBookmarkPositionsInTransaction(db, guids, offset, 1.845 + MAX_POSITION_UPDATES_PER_QUERY); 1.846 + } catch (SQLException e) { 1.847 + Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e); 1.848 + 1.849 + // Need to restart the transaction. 1.850 + // The only way a caller knows that anything failed is that the 1.851 + // returned update count will be smaller than the requested 1.852 + // number of records. 1.853 + db.setTransactionSuccessful(); 1.854 + db.endTransaction(); 1.855 + 1.856 + db.beginTransaction(); 1.857 + } 1.858 + 1.859 + offset += MAX_POSITION_UPDATES_PER_QUERY; 1.860 + } 1.861 + 1.862 + db.setTransactionSuccessful(); 1.863 + db.endTransaction(); 1.864 + 1.865 + return updated; 1.866 + } 1.867 + 1.868 + /** 1.869 + * Construct and execute an update expression that will modify the positions 1.870 + * of records in-place. 1.871 + */ 1.872 + private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids, 1.873 + final int offset, final int max) { 1.874 + int guidsCount = guids.length; 1.875 + int processCount = Math.min(max, guidsCount - offset); 1.876 + 1.877 + // Each must appear twice: once in a CASE, and once in the IN clause. 1.878 + String[] args = new String[processCount * 2]; 1.879 + System.arraycopy(guids, offset, args, 0, processCount); 1.880 + System.arraycopy(guids, offset, args, processCount, processCount); 1.881 + 1.882 + StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS + 1.883 + " SET " + Bookmarks.POSITION + 1.884 + " = CASE guid"); 1.885 + 1.886 + // Build the CASE statement body for GUID/index pairs from offset up to 1.887 + // the computed limit. 1.888 + final int end = offset + processCount; 1.889 + int i = offset; 1.890 + for (; i < end; ++i) { 1.891 + if (guids[i] == null) { 1.892 + // We don't want to issue the query if not every GUID is specified. 1.893 + debug("updateBookmarkPositions called with null GUID at index " + i); 1.894 + return 0; 1.895 + } 1.896 + b.append(" WHEN ? THEN " + i); 1.897 + } 1.898 + 1.899 + // TODO: use computeSQLInClause 1.900 + b.append(" END WHERE " + Bookmarks.GUID + " IN ("); 1.901 + i = 1; 1.902 + while (i++ < processCount) { 1.903 + b.append("?, "); 1.904 + } 1.905 + b.append("?)"); 1.906 + db.execSQL(b.toString(), args); 1.907 + 1.908 + // We can't easily get a modified count without calling something like changes(). 1.909 + return processCount; 1.910 + } 1.911 + 1.912 + /** 1.913 + * Construct an update expression that will modify the parents of any records 1.914 + * that match. 1.915 + */ 1.916 + private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) { 1.917 + trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")"); 1.918 + String where = Bookmarks._ID + " IN (" + 1.919 + " SELECT DISTINCT " + Bookmarks.PARENT + 1.920 + " FROM " + TABLE_BOOKMARKS + 1.921 + " WHERE " + selection + " )"; 1.922 + return db.update(TABLE_BOOKMARKS, values, where, selectionArgs); 1.923 + } 1.924 + 1.925 + long insertBookmark(Uri uri, ContentValues values) { 1.926 + // Generate values if not specified. Don't overwrite 1.927 + // if specified by caller. 1.928 + long now = System.currentTimeMillis(); 1.929 + if (!values.containsKey(Bookmarks.DATE_CREATED)) { 1.930 + values.put(Bookmarks.DATE_CREATED, now); 1.931 + } 1.932 + 1.933 + if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { 1.934 + values.put(Bookmarks.DATE_MODIFIED, now); 1.935 + } 1.936 + 1.937 + if (!values.containsKey(Bookmarks.GUID)) { 1.938 + values.put(Bookmarks.GUID, Utils.generateGuid()); 1.939 + } 1.940 + 1.941 + if (!values.containsKey(Bookmarks.POSITION)) { 1.942 + debug("Inserting bookmark with no position for URI"); 1.943 + values.put(Bookmarks.POSITION, 1.944 + Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION)); 1.945 + } 1.946 + 1.947 + String url = values.getAsString(Bookmarks.URL); 1.948 + 1.949 + debug("Inserting bookmark in database with URL: " + url); 1.950 + final SQLiteDatabase db = getWritableDatabase(uri); 1.951 + beginWrite(db); 1.952 + return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values); 1.953 + } 1.954 + 1.955 + 1.956 + int updateOrInsertBookmark(Uri uri, ContentValues values, String selection, 1.957 + String[] selectionArgs) { 1.958 + int updated = updateBookmarks(uri, values, selection, selectionArgs); 1.959 + if (updated > 0) { 1.960 + return updated; 1.961 + } 1.962 + 1.963 + // Transaction already begun by updateBookmarks. 1.964 + if (0 <= insertBookmark(uri, values)) { 1.965 + // We 'updated' one row. 1.966 + return 1; 1.967 + } 1.968 + 1.969 + // If something went wrong, then we updated zero rows. 1.970 + return 0; 1.971 + } 1.972 + 1.973 + int updateBookmarks(Uri uri, ContentValues values, String selection, 1.974 + String[] selectionArgs) { 1.975 + trace("Updating bookmarks on URI: " + uri); 1.976 + 1.977 + final String[] bookmarksProjection = new String[] { 1.978 + Bookmarks._ID, // 0 1.979 + }; 1.980 + 1.981 + if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { 1.982 + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); 1.983 + } 1.984 + 1.985 + trace("Querying bookmarks to update on URI: " + uri); 1.986 + final SQLiteDatabase db = getWritableDatabase(uri); 1.987 + 1.988 + // Compute matching IDs. 1.989 + final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection, 1.990 + selection, selectionArgs, null, null, null); 1.991 + 1.992 + // Now that we're done reading, open a transaction. 1.993 + final String inClause; 1.994 + try { 1.995 + inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID); 1.996 + } finally { 1.997 + cursor.close(); 1.998 + } 1.999 + 1.1000 + beginWrite(db); 1.1001 + return db.update(TABLE_BOOKMARKS, values, inClause, null); 1.1002 + } 1.1003 + 1.1004 + long insertHistory(Uri uri, ContentValues values) { 1.1005 + final long now = System.currentTimeMillis(); 1.1006 + values.put(History.DATE_CREATED, now); 1.1007 + values.put(History.DATE_MODIFIED, now); 1.1008 + 1.1009 + // Generate GUID for new history entry. Don't override specified GUIDs. 1.1010 + if (!values.containsKey(History.GUID)) { 1.1011 + values.put(History.GUID, Utils.generateGuid()); 1.1012 + } 1.1013 + 1.1014 + String url = values.getAsString(History.URL); 1.1015 + 1.1016 + debug("Inserting history in database with URL: " + url); 1.1017 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1018 + beginWrite(db); 1.1019 + return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values); 1.1020 + } 1.1021 + 1.1022 + int updateOrInsertHistory(Uri uri, ContentValues values, String selection, 1.1023 + String[] selectionArgs) { 1.1024 + final int updated = updateHistory(uri, values, selection, selectionArgs); 1.1025 + if (updated > 0) { 1.1026 + return updated; 1.1027 + } 1.1028 + 1.1029 + // Insert a new entry if necessary 1.1030 + if (!values.containsKey(History.VISITS)) { 1.1031 + values.put(History.VISITS, 1); 1.1032 + } 1.1033 + if (!values.containsKey(History.TITLE)) { 1.1034 + values.put(History.TITLE, values.getAsString(History.URL)); 1.1035 + } 1.1036 + 1.1037 + if (0 <= insertHistory(uri, values)) { 1.1038 + return 1; 1.1039 + } 1.1040 + 1.1041 + return 0; 1.1042 + } 1.1043 + 1.1044 + int updateHistory(Uri uri, ContentValues values, String selection, 1.1045 + String[] selectionArgs) { 1.1046 + trace("Updating history on URI: " + uri); 1.1047 + 1.1048 + int updated = 0; 1.1049 + 1.1050 + final String[] historyProjection = new String[] { 1.1051 + History._ID, // 0 1.1052 + History.URL, // 1 1.1053 + History.VISITS // 2 1.1054 + }; 1.1055 + 1.1056 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1057 + final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection, 1.1058 + selectionArgs, null, null, null); 1.1059 + 1.1060 + try { 1.1061 + if (!values.containsKey(Bookmarks.DATE_MODIFIED)) { 1.1062 + values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis()); 1.1063 + } 1.1064 + 1.1065 + while (cursor.moveToNext()) { 1.1066 + long id = cursor.getLong(0); 1.1067 + 1.1068 + trace("Updating history entry with ID: " + id); 1.1069 + 1.1070 + if (shouldIncrementVisits(uri)) { 1.1071 + long existing = cursor.getLong(2); 1.1072 + Long additional = values.getAsLong(History.VISITS); 1.1073 + 1.1074 + // Increment visit count by a specified amount, or default to increment by 1 1.1075 + values.put(History.VISITS, existing + ((additional != null) ? additional.longValue() : 1)); 1.1076 + } 1.1077 + 1.1078 + updated += db.update(TABLE_HISTORY, values, "_id = ?", 1.1079 + new String[] { Long.toString(id) }); 1.1080 + } 1.1081 + } finally { 1.1082 + cursor.close(); 1.1083 + } 1.1084 + 1.1085 + return updated; 1.1086 + } 1.1087 + 1.1088 + private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) { 1.1089 + ContentValues updateValues = new ContentValues(1); 1.1090 + updateValues.put(FaviconColumns.FAVICON_ID, faviconId); 1.1091 + db.update(TABLE_HISTORY, 1.1092 + updateValues, 1.1093 + History.URL + " = ?", 1.1094 + new String[] { pageUrl }); 1.1095 + db.update(TABLE_BOOKMARKS, 1.1096 + updateValues, 1.1097 + Bookmarks.URL + " = ?", 1.1098 + new String[] { pageUrl }); 1.1099 + } 1.1100 + 1.1101 + long insertFavicon(Uri uri, ContentValues values) { 1.1102 + return insertFavicon(getWritableDatabase(uri), values); 1.1103 + } 1.1104 + 1.1105 + long insertFavicon(SQLiteDatabase db, ContentValues values) { 1.1106 + // This method is a dupicate of BrowserDatabaseHelper.insertFavicon. 1.1107 + // If changes are needed, please update both 1.1108 + String faviconUrl = values.getAsString(Favicons.URL); 1.1109 + String pageUrl = null; 1.1110 + 1.1111 + trace("Inserting favicon for URL: " + faviconUrl); 1.1112 + 1.1113 + DBUtils.stripEmptyByteArray(values, Favicons.DATA); 1.1114 + 1.1115 + // Extract the page URL from the ContentValues 1.1116 + if (values.containsKey(Favicons.PAGE_URL)) { 1.1117 + pageUrl = values.getAsString(Favicons.PAGE_URL); 1.1118 + values.remove(Favicons.PAGE_URL); 1.1119 + } 1.1120 + 1.1121 + // If no URL is provided, insert using the default one. 1.1122 + if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) { 1.1123 + values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl)); 1.1124 + } 1.1125 + 1.1126 + final long now = System.currentTimeMillis(); 1.1127 + values.put(Favicons.DATE_CREATED, now); 1.1128 + values.put(Favicons.DATE_MODIFIED, now); 1.1129 + 1.1130 + beginWrite(db); 1.1131 + final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values); 1.1132 + 1.1133 + if (pageUrl != null) { 1.1134 + updateFaviconIdsForUrl(db, pageUrl, faviconId); 1.1135 + } 1.1136 + return faviconId; 1.1137 + } 1.1138 + 1.1139 + int updateOrInsertFavicon(Uri uri, ContentValues values, String selection, 1.1140 + String[] selectionArgs) { 1.1141 + return updateFavicon(uri, values, selection, selectionArgs, 1.1142 + true /* insert if needed */); 1.1143 + } 1.1144 + 1.1145 + int updateExistingFavicon(Uri uri, ContentValues values, String selection, 1.1146 + String[] selectionArgs) { 1.1147 + return updateFavicon(uri, values, selection, selectionArgs, 1.1148 + false /* only update, no insert */); 1.1149 + } 1.1150 + 1.1151 + int updateFavicon(Uri uri, ContentValues values, String selection, 1.1152 + String[] selectionArgs, boolean insertIfNeeded) { 1.1153 + String faviconUrl = values.getAsString(Favicons.URL); 1.1154 + String pageUrl = null; 1.1155 + int updated = 0; 1.1156 + Long faviconId = null; 1.1157 + long now = System.currentTimeMillis(); 1.1158 + 1.1159 + trace("Updating favicon for URL: " + faviconUrl); 1.1160 + 1.1161 + DBUtils.stripEmptyByteArray(values, Favicons.DATA); 1.1162 + 1.1163 + // Extract the page URL from the ContentValues 1.1164 + if (values.containsKey(Favicons.PAGE_URL)) { 1.1165 + pageUrl = values.getAsString(Favicons.PAGE_URL); 1.1166 + values.remove(Favicons.PAGE_URL); 1.1167 + } 1.1168 + 1.1169 + values.put(Favicons.DATE_MODIFIED, now); 1.1170 + 1.1171 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1172 + 1.1173 + // If there's no favicon URL given and we're inserting if needed, skip 1.1174 + // the update and only do an insert (otherwise all rows would be 1.1175 + // updated). 1.1176 + if (!(insertIfNeeded && (faviconUrl == null))) { 1.1177 + updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs); 1.1178 + } 1.1179 + 1.1180 + if (updated > 0) { 1.1181 + if ((faviconUrl != null) && (pageUrl != null)) { 1.1182 + final Cursor cursor = db.query(TABLE_FAVICONS, 1.1183 + new String[] { Favicons._ID }, 1.1184 + Favicons.URL + " = ?", 1.1185 + new String[] { faviconUrl }, 1.1186 + null, null, null); 1.1187 + try { 1.1188 + if (cursor.moveToFirst()) { 1.1189 + faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID)); 1.1190 + } 1.1191 + } finally { 1.1192 + cursor.close(); 1.1193 + } 1.1194 + } 1.1195 + if (pageUrl != null) { 1.1196 + beginWrite(db); 1.1197 + } 1.1198 + } else if (insertIfNeeded) { 1.1199 + values.put(Favicons.DATE_CREATED, now); 1.1200 + 1.1201 + trace("No update, inserting favicon for URL: " + faviconUrl); 1.1202 + beginWrite(db); 1.1203 + faviconId = db.insert(TABLE_FAVICONS, null, values); 1.1204 + updated = 1; 1.1205 + } 1.1206 + 1.1207 + if (pageUrl != null) { 1.1208 + updateFaviconIdsForUrl(db, pageUrl, faviconId); 1.1209 + } 1.1210 + 1.1211 + return updated; 1.1212 + } 1.1213 + 1.1214 + private long insertThumbnail(Uri uri, ContentValues values) { 1.1215 + final String url = values.getAsString(Thumbnails.URL); 1.1216 + 1.1217 + trace("Inserting thumbnail for URL: " + url); 1.1218 + 1.1219 + DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); 1.1220 + 1.1221 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1222 + beginWrite(db); 1.1223 + return db.insertOrThrow(TABLE_THUMBNAILS, null, values); 1.1224 + } 1.1225 + 1.1226 + private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection, 1.1227 + String[] selectionArgs) { 1.1228 + return updateThumbnail(uri, values, selection, selectionArgs, 1.1229 + true /* insert if needed */); 1.1230 + } 1.1231 + 1.1232 + private int updateExistingThumbnail(Uri uri, ContentValues values, String selection, 1.1233 + String[] selectionArgs) { 1.1234 + return updateThumbnail(uri, values, selection, selectionArgs, 1.1235 + false /* only update, no insert */); 1.1236 + } 1.1237 + 1.1238 + private int updateThumbnail(Uri uri, ContentValues values, String selection, 1.1239 + String[] selectionArgs, boolean insertIfNeeded) { 1.1240 + final String url = values.getAsString(Thumbnails.URL); 1.1241 + DBUtils.stripEmptyByteArray(values, Thumbnails.DATA); 1.1242 + 1.1243 + trace("Updating thumbnail for URL: " + url); 1.1244 + 1.1245 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1246 + beginWrite(db); 1.1247 + int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs); 1.1248 + 1.1249 + if (updated == 0 && insertIfNeeded) { 1.1250 + trace("No update, inserting thumbnail for URL: " + url); 1.1251 + db.insert(TABLE_THUMBNAILS, null, values); 1.1252 + updated = 1; 1.1253 + } 1.1254 + 1.1255 + return updated; 1.1256 + } 1.1257 + 1.1258 + /** 1.1259 + * This method does not create a new transaction. Its first operation is 1.1260 + * guaranteed to be a write, which in the case of a new enclosing 1.1261 + * transaction will guarantee that a read does not need to be upgraded to 1.1262 + * a write. 1.1263 + */ 1.1264 + int deleteHistory(Uri uri, String selection, String[] selectionArgs) { 1.1265 + debug("Deleting history entry for URI: " + uri); 1.1266 + 1.1267 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1268 + 1.1269 + if (isCallerSync(uri)) { 1.1270 + return db.delete(TABLE_HISTORY, selection, selectionArgs); 1.1271 + } 1.1272 + 1.1273 + debug("Marking history entry as deleted for URI: " + uri); 1.1274 + 1.1275 + ContentValues values = new ContentValues(); 1.1276 + values.put(History.IS_DELETED, 1); 1.1277 + 1.1278 + // Wipe sensitive data. 1.1279 + values.putNull(History.TITLE); 1.1280 + values.put(History.URL, ""); // Column is NOT NULL. 1.1281 + values.put(History.DATE_CREATED, 0); 1.1282 + values.put(History.DATE_LAST_VISITED, 0); 1.1283 + values.put(History.VISITS, 0); 1.1284 + values.put(History.DATE_MODIFIED, System.currentTimeMillis()); 1.1285 + 1.1286 + // Doing this UPDATE (or the DELETE above) first ensures that the 1.1287 + // first operation within a new enclosing transaction is a write. 1.1288 + // The cleanup call below will do a SELECT first, and thus would 1.1289 + // require the transaction to be upgraded from a reader to a writer. 1.1290 + // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid 1.1291 + // it if we can. 1.1292 + final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs); 1.1293 + try { 1.1294 + cleanUpSomeDeletedRecords(uri, TABLE_HISTORY); 1.1295 + } catch (Exception e) { 1.1296 + // We don't care. 1.1297 + Log.e(LOGTAG, "Unable to clean up deleted history records: ", e); 1.1298 + } 1.1299 + return updated; 1.1300 + } 1.1301 + 1.1302 + int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) { 1.1303 + debug("Deleting bookmarks for URI: " + uri); 1.1304 + 1.1305 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1306 + 1.1307 + if (isCallerSync(uri)) { 1.1308 + beginWrite(db); 1.1309 + return db.delete(TABLE_BOOKMARKS, selection, selectionArgs); 1.1310 + } 1.1311 + 1.1312 + debug("Marking bookmarks as deleted for URI: " + uri); 1.1313 + 1.1314 + ContentValues values = new ContentValues(); 1.1315 + values.put(Bookmarks.IS_DELETED, 1); 1.1316 + 1.1317 + // Doing this UPDATE (or the DELETE above) first ensures that the 1.1318 + // first operation within this transaction is a write. 1.1319 + // The cleanup call below will do a SELECT first, and thus would 1.1320 + // require the transaction to be upgraded from a reader to a writer. 1.1321 + final int updated = updateBookmarks(uri, values, selection, selectionArgs); 1.1322 + try { 1.1323 + cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS); 1.1324 + } catch (Exception e) { 1.1325 + // We don't care. 1.1326 + Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e); 1.1327 + } 1.1328 + return updated; 1.1329 + } 1.1330 + 1.1331 + int deleteFavicons(Uri uri, String selection, String[] selectionArgs) { 1.1332 + debug("Deleting favicons for URI: " + uri); 1.1333 + 1.1334 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1335 + 1.1336 + return db.delete(TABLE_FAVICONS, selection, selectionArgs); 1.1337 + } 1.1338 + 1.1339 + int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) { 1.1340 + debug("Deleting thumbnails for URI: " + uri); 1.1341 + 1.1342 + final SQLiteDatabase db = getWritableDatabase(uri); 1.1343 + 1.1344 + return db.delete(TABLE_THUMBNAILS, selection, selectionArgs); 1.1345 + } 1.1346 + 1.1347 + int deleteUnusedImages(Uri uri) { 1.1348 + debug("Deleting all unused favicons and thumbnails for URI: " + uri); 1.1349 + 1.1350 + String faviconSelection = Favicons._ID + " NOT IN " 1.1351 + + "(SELECT " + History.FAVICON_ID 1.1352 + + " FROM " + TABLE_HISTORY 1.1353 + + " WHERE " + History.IS_DELETED + " = 0" 1.1354 + + " AND " + History.FAVICON_ID + " IS NOT NULL" 1.1355 + + " UNION ALL SELECT " + Bookmarks.FAVICON_ID 1.1356 + + " FROM " + TABLE_BOOKMARKS 1.1357 + + " WHERE " + Bookmarks.IS_DELETED + " = 0" 1.1358 + + " AND " + Bookmarks.FAVICON_ID + " IS NOT NULL)"; 1.1359 + 1.1360 + String thumbnailSelection = Thumbnails.URL + " NOT IN " 1.1361 + + "(SELECT " + History.URL 1.1362 + + " FROM " + TABLE_HISTORY 1.1363 + + " WHERE " + History.IS_DELETED + " = 0" 1.1364 + + " AND " + History.URL + " IS NOT NULL" 1.1365 + + " UNION ALL SELECT " + Bookmarks.URL 1.1366 + + " FROM " + TABLE_BOOKMARKS 1.1367 + + " WHERE " + Bookmarks.IS_DELETED + " = 0" 1.1368 + + " AND " + Bookmarks.URL + " IS NOT NULL)"; 1.1369 + 1.1370 + return deleteFavicons(uri, faviconSelection, null) + 1.1371 + deleteThumbnails(uri, thumbnailSelection, null); 1.1372 + } 1.1373 + 1.1374 + @Override 1.1375 + public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations) 1.1376 + throws OperationApplicationException { 1.1377 + final int numOperations = operations.size(); 1.1378 + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; 1.1379 + 1.1380 + if (numOperations < 1) { 1.1381 + debug("applyBatch: no operations; returning immediately."); 1.1382 + // The original Android implementation returns a zero-length 1.1383 + // array in this case. We do the same. 1.1384 + return results; 1.1385 + } 1.1386 + 1.1387 + boolean failures = false; 1.1388 + 1.1389 + // We only have 1 database for all Uris that we can get. 1.1390 + SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri()); 1.1391 + 1.1392 + // Note that the apply() call may cause us to generate 1.1393 + // additional transactions for the individual operations. 1.1394 + // But Android's wrapper for SQLite supports nested transactions, 1.1395 + // so this will do the right thing. 1.1396 + // 1.1397 + // Note further that in some circumstances this can result in 1.1398 + // exceptions: if this transaction is first involved in reading, 1.1399 + // and then (naturally) tries to perform writes, SQLITE_BUSY can 1.1400 + // be raised. See Bug 947939 and friends. 1.1401 + beginBatch(db); 1.1402 + 1.1403 + for (int i = 0; i < numOperations; i++) { 1.1404 + try { 1.1405 + final ContentProviderOperation operation = operations.get(i); 1.1406 + results[i] = operation.apply(this, results, i); 1.1407 + } catch (SQLException e) { 1.1408 + Log.w(LOGTAG, "SQLite Exception during applyBatch.", e); 1.1409 + // The Android API makes it implementation-defined whether 1.1410 + // the failure of a single operation makes all others abort 1.1411 + // or not. For our use cases, best-effort operation makes 1.1412 + // more sense. Rolling back and forcing the caller to retry 1.1413 + // after it figures out what went wrong isn't very convenient 1.1414 + // anyway. 1.1415 + // Signal failed operation back, so the caller knows what 1.1416 + // went through and what didn't. 1.1417 + results[i] = new ContentProviderResult(0); 1.1418 + failures = true; 1.1419 + // http://www.sqlite.org/lang_conflict.html 1.1420 + // Note that we need a new transaction, subsequent operations 1.1421 + // on this one will fail (we're in ABORT by default, which 1.1422 + // isn't IGNORE). We still need to set it as successful to let 1.1423 + // everything before the failed op go through. 1.1424 + // We can't set conflict resolution on API level < 8, and even 1.1425 + // above 8 it requires splitting the call per operation 1.1426 + // (insert/update/delete). 1.1427 + db.setTransactionSuccessful(); 1.1428 + db.endTransaction(); 1.1429 + db.beginTransaction(); 1.1430 + } catch (OperationApplicationException e) { 1.1431 + // Repeat of above. 1.1432 + results[i] = new ContentProviderResult(0); 1.1433 + failures = true; 1.1434 + db.setTransactionSuccessful(); 1.1435 + db.endTransaction(); 1.1436 + db.beginTransaction(); 1.1437 + } 1.1438 + } 1.1439 + 1.1440 + trace("Flushing DB applyBatch..."); 1.1441 + markBatchSuccessful(db); 1.1442 + endBatch(db); 1.1443 + 1.1444 + if (failures) { 1.1445 + throw new OperationApplicationException(); 1.1446 + } 1.1447 + 1.1448 + return results; 1.1449 + } 1.1450 +}