mobile/android/base/db/BrowserProvider.java

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

mercurial