Wed, 31 Dec 2014 07:22:50 +0100
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 | } |