mobile/android/base/db/BrowserProvider.java

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

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

Correct previous dual key logic pending first delivery installment.

     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/. */
     6 package org.mozilla.gecko.db;
     8 import java.util.ArrayList;
     9 import java.util.Collections;
    10 import java.util.HashMap;
    11 import java.util.Map;
    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;
    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;
    41 public class BrowserProvider extends SharedBrowserDatabaseProvider {
    42     private static final String LOGTAG = "GeckoBrowserProvider";
    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;
    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;
    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;
    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;
    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;
    69     static final String VIEW_FLAGS = "flags";
    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;
    78     // History matches
    79     static final int HISTORY = 200;
    80     static final int HISTORY_ID = 201;
    81     static final int HISTORY_OLD = 202;
    83     // Favicon matches
    84     static final int FAVICONS = 300;
    85     static final int FAVICON_ID = 301;
    87     // Schema matches
    88     static final int SCHEMA = 400;
    90     // Combined bookmarks and history matches
    91     static final int COMBINED = 500;
    93     // Control matches
    94     static final int CONTROL = 600;
    96     // Search Suggest matches
    97     static final int SEARCH_SUGGEST = 700;
    99     // Thumbnail matches
   100     static final int THUMBNAILS = 800;
   101     static final int THUMBNAIL_ID = 801;
   103     static final int FLAGS = 900;
   105     static final String DEFAULT_BOOKMARKS_SORT_ORDER = Bookmarks.TYPE
   106             + " ASC, " + Bookmarks.POSITION + " ASC, " + Bookmarks._ID
   107             + " ASC";
   109     static final String DEFAULT_HISTORY_SORT_ORDER = History.DATE_LAST_VISITED + " DESC";
   111     static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
   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;
   121     static {
   122         // We will reuse this.
   123         HashMap<String, String> map;
   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);
   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);
   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);
   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);
   171         // Favicons
   172         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons", FAVICONS);
   173         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "favicons/#", FAVICON_ID);
   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);
   183         // Thumbnails
   184         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails", THUMBNAILS);
   185         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "thumbnails/#", THUMBNAIL_ID);
   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);
   193         // Combined bookmarks and history
   194         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "combined", COMBINED);
   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);
   210         // Schema
   211         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "schema", SCHEMA);
   213         map = new HashMap<String, String>();
   214         map.put(Schema.VERSION, Schema.VERSION);
   215         SCHEMA_PROJECTION_MAP = Collections.unmodifiableMap(map);
   218         // Control
   219         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "control", CONTROL);
   221         // Search Suggest
   222         URI_MATCHER.addURI(BrowserContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST);
   224         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "flags", FLAGS);
   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     }
   236     static final String qualifyColumn(String table, String column) {
   237         return table + "." + column;
   238     }
   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         }
   248         return false;
   249     }
   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     }
   261     protected static void debug(String message) {
   262         if (logDebug) {
   263             Log.d(LOGTAG, message);
   264         }
   265     }
   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);
   279         if (retain >= rows) {
   280             debug("Not expiring history: only have " + rows + " rows.");
   281             return;
   282         }
   284         final String sortOrder = BrowserContract.getFrecencySortOrder(false, true);
   285         final long toRemove = rows - retain;
   286         debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");
   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);
   303         beginWrite(db);
   304         db.execSQL(sql);
   305     }
   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     }
   331     private boolean shouldIncrementVisits(Uri uri) {
   332         String incrementVisits = uri.getQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS);
   333         return Boolean.parseBoolean(incrementVisits);
   334     }
   336     @Override
   337     public String getType(Uri uri) {
   338         final int match = URI_MATCHER.match(uri);
   340         trace("Getting URI type: " + uri);
   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         }
   363         debug("URI has unrecognized type: " + uri);
   365         return null;
   366     }
   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);
   374         final int match = URI_MATCHER.match(uri);
   375         int deleted = 0;
   377         switch (match) {
   378             case BOOKMARKS_ID:
   379                 trace("Delete on BOOKMARKS_ID: " + uri);
   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             }
   392             case HISTORY_ID:
   393                 trace("Delete on HISTORY_ID: " + uri);
   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             }
   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;
   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             }
   422             case FAVICON_ID:
   423                 debug("Delete on FAVICON_ID: " + uri);
   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             }
   436             case THUMBNAIL_ID:
   437                 debug("Delete on THUMBNAIL_ID: " + uri);
   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             }
   450             default:
   451                 throw new UnsupportedOperationException("Unknown delete URI " + uri);
   452         }
   454         debug("Deleted " + deleted + " rows for URI: " + uri);
   456         return deleted;
   457     }
   459     @Override
   460     public Uri insertInTransaction(Uri uri, ContentValues values) {
   461         trace("Calling insert in transaction on URI: " + uri);
   463         int match = URI_MATCHER.match(uri);
   464         long id = -1;
   466         switch (match) {
   467             case BOOKMARKS: {
   468                 trace("Insert on BOOKMARKS: " + uri);
   469                 id = insertBookmark(uri, values);
   470                 break;
   471             }
   473             case HISTORY: {
   474                 trace("Insert on HISTORY: " + uri);
   475                 id = insertHistory(uri, values);
   476                 break;
   477             }
   479             case FAVICONS: {
   480                 trace("Insert on FAVICONS: " + uri);
   481                 id = insertFavicon(uri, values);
   482                 break;
   483             }
   485             case THUMBNAILS: {
   486                 trace("Insert on THUMBNAILS: " + uri);
   487                 id = insertThumbnail(uri, values);
   488                 break;
   489             }
   491             default:
   492                 throw new UnsupportedOperationException("Unknown insert URI " + uri);
   493         }
   495         debug("Inserted ID in database: " + id);
   497         if (id >= 0)
   498             return ContentUris.withAppendedId(uri, id);
   500         return null;
   501     }
   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);
   509         int match = URI_MATCHER.match(uri);
   510         int updated = 0;
   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);
   526                 // This already starts and finishes its own transaction.
   527                 updated = updateBookmarkPositions(uri, selectionArgs);
   528                 break;
   529             }
   531             case BOOKMARKS_PARENT: {
   532                 debug("Update on BOOKMARKS_PARENT: " + uri);
   533                 beginWrite(db);
   534                 updated = updateBookmarkParents(db, values, selection, selectionArgs);
   535                 break;
   536             }
   538             case BOOKMARKS_ID:
   539                 debug("Update on BOOKMARKS_ID: " + uri);
   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             }
   555             case HISTORY_ID:
   556                 debug("Update on HISTORY_ID: " + uri);
   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             }
   572             case FAVICONS: {
   573                 debug("Update on FAVICONS: " + uri);
   575                 String url = values.getAsString(Favicons.URL);
   576                 String faviconSelection = null;
   577                 String[] faviconSelectionArgs = null;
   579                 if (!TextUtils.isEmpty(url)) {
   580                     faviconSelection = Favicons.URL + " = ?";
   581                     faviconSelectionArgs = new String[] { url };
   582                 }
   584                 if (shouldUpdateOrInsert(uri)) {
   585                     updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
   586                 } else {
   587                     updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);
   588                 }
   589                 break;
   590             }
   592             case THUMBNAILS: {
   593                 debug("Update on THUMBNAILS: " + uri);
   595                 String url = values.getAsString(Thumbnails.URL);
   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             }
   610             default:
   611                 throw new UnsupportedOperationException("Unknown update URI " + uri);
   612         }
   614         debug("Updated " + updated + " rows for URI: " + uri);
   615         return updated;
   616     }
   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);
   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(...) | ...
   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 +
   649                 " WHEN " + Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
   650                 " THEN " + Bookmarks.FLAG_PINNED +
   652                 " ELSE " + Bookmarks.FLAG_BOOKMARK +
   653                 " END flag " +
   654                 "FROM " + TABLE_BOOKMARKS + " WHERE " +
   655                 Bookmarks.URL + " = ? " +
   656                 (includeDeleted ? "" : ("AND " + Bookmarks.IS_DELETED + " = 0")) +
   657                 ")";
   659             return db.rawQuery(query, selectionArgs);
   660         }
   662         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   663         String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
   664         String groupBy = null;
   666         switch (match) {
   667             case BOOKMARKS_FOLDER_ID:
   668             case BOOKMARKS_ID:
   669             case BOOKMARKS: {
   670                 debug("Query is on bookmarks: " + uri);
   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                 }
   682                 if (!shouldShowDeleted(uri))
   683                     selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
   685                 if (TextUtils.isEmpty(sortOrder)) {
   686                     sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
   687                 } else {
   688                     debug("Using sort order " + sortOrder + ".");
   689                 }
   691                 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
   693                 if (hasFaviconsInProjection(projection))
   694                     qb.setTables(VIEW_BOOKMARKS_WITH_FAVICONS);
   695                 else
   696                     qb.setTables(TABLE_BOOKMARKS);
   698                 break;
   699             }
   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);
   709                 if (!shouldShowDeleted(uri))
   710                     selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
   712                 if (TextUtils.isEmpty(sortOrder))
   713                     sortOrder = DEFAULT_HISTORY_SORT_ORDER;
   715                 qb.setProjectionMap(HISTORY_PROJECTION_MAP);
   717                 if (hasFaviconsInProjection(projection))
   718                     qb.setTables(VIEW_HISTORY_WITH_FAVICONS);
   719                 else
   720                     qb.setTables(TABLE_HISTORY);
   722                 break;
   723             }
   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);
   733                 qb.setProjectionMap(FAVICONS_PROJECTION_MAP);
   734                 qb.setTables(TABLE_FAVICONS);
   736                 break;
   737             }
   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);
   747                 qb.setProjectionMap(THUMBNAILS_PROJECTION_MAP);
   748                 qb.setTables(TABLE_THUMBNAILS);
   750                 break;
   751             }
   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);
   758                 return schemaCursor;
   759             }
   761             case COMBINED: {
   762                 debug("Query is on combined: " + uri);
   764                 if (TextUtils.isEmpty(sortOrder))
   765                     sortOrder = DEFAULT_HISTORY_SORT_ORDER;
   767                 // This will avoid duplicate entries in the awesomebar
   768                 // results when a history entry has multiple bookmarks.
   769                 groupBy = Combined.URL;
   771                 qb.setProjectionMap(COMBINED_PROJECTION_MAP);
   773                 if (hasFaviconsInProjection(projection))
   774                     qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
   775                 else
   776                     qb.setTables(Combined.VIEW_NAME);
   778                 break;
   779             }
   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 ?)");
   786                 String keyword = uri.getLastPathSegment();
   787                 if (keyword == null)
   788                     keyword = "";
   790                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
   791                         new String[] { "%" + keyword + "%",
   792                                        "%" + keyword + "%" });
   794                 if (TextUtils.isEmpty(sortOrder))
   795                     sortOrder = DEFAULT_HISTORY_SORT_ORDER;
   797                 qb.setProjectionMap(SEARCH_SUGGEST_PROJECTION_MAP);
   798                 qb.setTables(VIEW_COMBINED_WITH_FAVICONS);
   800                 break;
   801             }
   803             default:
   804                 throw new UnsupportedOperationException("Unknown query URI " + uri);
   805         }
   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);
   813         return cursor;
   814     }
   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         }
   828         int guidsCount = guids.length;
   829         if (guidsCount == 0) {
   830             return 0;
   831         }
   833         int offset = 0;
   834         int updated = 0;
   836         final SQLiteDatabase db = getWritableDatabase(uri);
   837         db.beginTransaction();
   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);
   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();
   853                 db.beginTransaction();
   854             }
   856             offset += MAX_POSITION_UPDATES_PER_QUERY;
   857         }
   859         db.setTransactionSuccessful();
   860         db.endTransaction();
   862         return updated;
   863     }
   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);
   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);
   879         StringBuilder b = new StringBuilder("UPDATE " + TABLE_BOOKMARKS +
   880                                             " SET " + Bookmarks.POSITION +
   881                                             " = CASE guid");
   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         }
   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);
   905         // We can't easily get a modified count without calling something like changes().
   906         return processCount;
   907     }
   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     }
   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         }
   930         if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
   931             values.put(Bookmarks.DATE_MODIFIED, now);
   932         }
   934         if (!values.containsKey(Bookmarks.GUID)) {
   935             values.put(Bookmarks.GUID, Utils.generateGuid());
   936         }
   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         }
   944         String url = values.getAsString(Bookmarks.URL);
   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     }
   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         }
   960         // Transaction already begun by updateBookmarks.
   961         if (0 <= insertBookmark(uri, values)) {
   962             // We 'updated' one row.
   963             return 1;
   964         }
   966         // If something went wrong, then we updated zero rows.
   967         return 0;
   968     }
   970     int updateBookmarks(Uri uri, ContentValues values, String selection,
   971             String[] selectionArgs) {
   972         trace("Updating bookmarks on URI: " + uri);
   974         final String[] bookmarksProjection = new String[] {
   975                 Bookmarks._ID, // 0
   976         };
   978         if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
   979             values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
   980         }
   982         trace("Querying bookmarks to update on URI: " + uri);
   983         final SQLiteDatabase db = getWritableDatabase(uri);
   985         // Compute matching IDs.
   986         final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
   987                                        selection, selectionArgs, null, null, null);
   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         }
   997         beginWrite(db);
   998         return db.update(TABLE_BOOKMARKS, values, inClause, null);
   999     }
  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);
  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());
  1011         String url = values.getAsString(History.URL);
  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);
  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;
  1026         // Insert a new entry if necessary
  1027         if (!values.containsKey(History.VISITS)) {
  1028             values.put(History.VISITS, 1);
  1030         if (!values.containsKey(History.TITLE)) {
  1031             values.put(History.TITLE, values.getAsString(History.URL));
  1034         if (0 <= insertHistory(uri, values)) {
  1035             return 1;
  1038         return 0;
  1041     int updateHistory(Uri uri, ContentValues values, String selection,
  1042             String[] selectionArgs) {
  1043         trace("Updating history on URI: " + uri);
  1045         int updated = 0;
  1047         final String[] historyProjection = new String[] {
  1048             History._ID,   // 0
  1049             History.URL,   // 1
  1050             History.VISITS // 2
  1051         };
  1053         final SQLiteDatabase db = getWritableDatabase(uri);
  1054         final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
  1055                                        selectionArgs, null, null, null);
  1057         try {
  1058             if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
  1059                 values.put(Bookmarks.DATE_MODIFIED,  System.currentTimeMillis());
  1062             while (cursor.moveToNext()) {
  1063                 long id = cursor.getLong(0);
  1065                 trace("Updating history entry with ID: " + id);
  1067                 if (shouldIncrementVisits(uri)) {
  1068                     long existing = cursor.getLong(2);
  1069                     Long additional = values.getAsLong(History.VISITS);
  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));
  1075                 updated += db.update(TABLE_HISTORY, values, "_id = ?",
  1076                                      new String[] { Long.toString(id) });
  1078         } finally {
  1079             cursor.close();
  1082         return updated;
  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 });
  1098     long insertFavicon(Uri uri, ContentValues values) {
  1099         return insertFavicon(getWritableDatabase(uri), values);
  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;
  1108         trace("Inserting favicon for URL: " + faviconUrl);
  1110         DBUtils.stripEmptyByteArray(values, Favicons.DATA);
  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);
  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));
  1123         final long now = System.currentTimeMillis();
  1124         values.put(Favicons.DATE_CREATED, now);
  1125         values.put(Favicons.DATE_MODIFIED, now);
  1127         beginWrite(db);
  1128         final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
  1130         if (pageUrl != null) {
  1131             updateFaviconIdsForUrl(db, pageUrl, faviconId);
  1133         return faviconId;
  1136     int updateOrInsertFavicon(Uri uri, ContentValues values, String selection,
  1137             String[] selectionArgs) {
  1138         return updateFavicon(uri, values, selection, selectionArgs,
  1139                 true /* insert if needed */);
  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 */);
  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();
  1156         trace("Updating favicon for URL: " + faviconUrl);
  1158         DBUtils.stripEmptyByteArray(values, Favicons.DATA);
  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);
  1166         values.put(Favicons.DATE_MODIFIED, now);
  1168         final SQLiteDatabase db = getWritableDatabase(uri);
  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);
  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));
  1188                 } finally {
  1189                     cursor.close();
  1192             if (pageUrl != null) {
  1193                 beginWrite(db);
  1195         } else if (insertIfNeeded) {
  1196             values.put(Favicons.DATE_CREATED, now);
  1198             trace("No update, inserting favicon for URL: " + faviconUrl);
  1199             beginWrite(db);
  1200             faviconId = db.insert(TABLE_FAVICONS, null, values);
  1201             updated = 1;
  1204         if (pageUrl != null) {
  1205             updateFaviconIdsForUrl(db, pageUrl, faviconId);
  1208         return updated;
  1211     private long insertThumbnail(Uri uri, ContentValues values) {
  1212         final String url = values.getAsString(Thumbnails.URL);
  1214         trace("Inserting thumbnail for URL: " + url);
  1216         DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
  1218         final SQLiteDatabase db = getWritableDatabase(uri);
  1219         beginWrite(db);
  1220         return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
  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 */);
  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 */);
  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);
  1240         trace("Updating thumbnail for URL: " + url);
  1242         final SQLiteDatabase db = getWritableDatabase(uri);
  1243         beginWrite(db);
  1244         int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
  1246         if (updated == 0 && insertIfNeeded) {
  1247             trace("No update, inserting thumbnail for URL: " + url);
  1248             db.insert(TABLE_THUMBNAILS, null, values);
  1249             updated = 1;
  1252         return updated;
  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);
  1264         final SQLiteDatabase db = getWritableDatabase(uri);
  1266         if (isCallerSync(uri)) {
  1267             return db.delete(TABLE_HISTORY, selection, selectionArgs);
  1270         debug("Marking history entry as deleted for URI: " + uri);
  1272         ContentValues values = new ContentValues();
  1273         values.put(History.IS_DELETED, 1);
  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());
  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);
  1296         return updated;
  1299     int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
  1300         debug("Deleting bookmarks for URI: " + uri);
  1302         final SQLiteDatabase db = getWritableDatabase(uri);
  1304         if (isCallerSync(uri)) {
  1305             beginWrite(db);
  1306             return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
  1309         debug("Marking bookmarks as deleted for URI: " + uri);
  1311         ContentValues values = new ContentValues();
  1312         values.put(Bookmarks.IS_DELETED, 1);
  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);
  1325         return updated;
  1328     int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
  1329         debug("Deleting favicons for URI: " + uri);
  1331         final SQLiteDatabase db = getWritableDatabase(uri);
  1333         return db.delete(TABLE_FAVICONS, selection, selectionArgs);
  1336     int deleteThumbnails(Uri uri, String selection, String[] selectionArgs) {
  1337         debug("Deleting thumbnails for URI: " + uri);
  1339         final SQLiteDatabase db = getWritableDatabase(uri);
  1341         return db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
  1344     int deleteUnusedImages(Uri uri) {
  1345         debug("Deleting all unused favicons and thumbnails for URI: " + uri);
  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)";
  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)";
  1367         return deleteFavicons(uri, faviconSelection, null) +
  1368                deleteThumbnails(uri, thumbnailSelection, null);
  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];
  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;
  1384         boolean failures = false;
  1386         // We only have 1 database for all Uris that we can get.
  1387         SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri());
  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);
  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();
  1437         trace("Flushing DB applyBatch...");
  1438         markBatchSuccessful(db);
  1439         endBatch(db);
  1441         if (failures) {
  1442             throw new OperationApplicationException();
  1445         return results;

mercurial