mobile/android/base/db/AbstractTransactionalProvider.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.db;
     7 import android.content.ContentProvider;
     8 import android.content.ContentValues;
     9 import android.database.Cursor;
    10 import android.database.SQLException;
    11 import android.database.sqlite.SQLiteDatabase;
    12 import android.net.Uri;
    13 import android.os.Build;
    14 import android.text.TextUtils;
    15 import android.util.Log;
    17 /**
    18  * This abstract class exists to capture some of the transaction-handling
    19  * commonalities in Fennec's DB layer.
    20  *
    21  * In particular, this abstracts DB access, batching, and a particular
    22  * transaction approach.
    23  *
    24  * That approach is: subclasses implement the abstract methods
    25  * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
    26  * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
    27  * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
    28  *
    29  * These are all called expecting a transaction to be established, so failed
    30  * modifications can be rolled-back, and work batched.
    31  *
    32  * If no transaction is established, that's not a problem. Transaction nesting
    33  * can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
    34  *
    35  * The decision of when to begin a transaction is left to the subclasses,
    36  * primarily to avoid the pattern of a transaction being begun, a read occurring,
    37  * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
    38  * which we don't handle well. Better to avoid starting a transaction too soon!
    39  *
    40  * You are probably interested in some subclasses:
    41  *
    42  * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
    43  *   querying databases that are stored in the user's profile directory.
    44  * * {@link PerProfileDatabaseProvider} is a simple version that only allows a
    45  *   single ContentProvider to access each per-profile database.
    46  * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
    47  *   that allows for multiple providers to safely work with the same databases.
    48  */
    49 @SuppressWarnings("javadoc")
    50 public abstract class AbstractTransactionalProvider extends ContentProvider {
    51     private static final String LOGTAG = "GeckoTransProvider";
    53     private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
    54     private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
    56     protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
    57     protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
    59     public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
    61     protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
    62     protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
    63     protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
    65     /**
    66      * Track whether we're in a batch operation.
    67      *
    68      * When we're in a batch operation, individual write steps won't even try
    69      * to start a transaction... and neither will they attempt to finish one.
    70      *
    71      * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
    72      * a section of code in which {@link ContentProvider} methods will be
    73      * called, but nested transactions should not be started. Callers are
    74      * responsible for beginning and ending the enclosing transaction, and
    75      * for setting this to <code>Boolean.FALSE</code> when done.
    76      *
    77      * This is a ThreadLocal separate from `db.inTransaction` because batched
    78      * operations start transactions independent of individual ContentProvider
    79      * operations. This doesn't work well with the entire concept of this
    80      * abstract class -- that is, automatically beginning and ending transactions
    81      * for each insert/delete/update operation -- and doing so without
    82      * causing arbitrary nesting requires external tracking.
    83      *
    84      * Note that beginWrite takes a DB argument, but we don't differentiate
    85      * between databases in this tracking flag. If your ContentProvider manages
    86      * multiple database transactions within the same thread, you'll need to
    87      * amend this scheme -- but then, you're already doing some serious wizardry,
    88      * so rock on.
    89      */
    90     final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
    92     /**
    93      * Return true if OS version and database parallelism support indicates
    94      * that this provider should bundle writes into transactions.
    95      */
    96     @SuppressWarnings("static-method")
    97     protected boolean shouldUseTransactions() {
    98         return Build.VERSION.SDK_INT >= 11;
    99     }
   101     protected static String computeSQLInClause(int items, String field) {
   102         final StringBuilder builder = new StringBuilder(field);
   103         builder.append(" IN (");
   104         int i = 0;
   105         for (; i < items - 1; ++i) {
   106             builder.append("?, ");
   107         }
   108         if (i < items) {
   109             builder.append("?");
   110         }
   111         builder.append(")");
   112         return builder.toString();
   113     }
   115     private boolean isInBatch() {
   116         final Boolean isInBatch = isInBatchOperation.get();
   117         if (isInBatch == null) {
   118             return false;
   119         }
   120         return isInBatch.booleanValue();
   121     }
   123     /**
   124      * If we're not currently in a transaction, and we should be, start one.
   125      */
   126     protected void beginWrite(final SQLiteDatabase db) {
   127         if (isInBatch()) {
   128             trace("Not bothering with an intermediate write transaction: inside batch operation.");
   129             return;
   130         }
   132         if (shouldUseTransactions() && !db.inTransaction()) {
   133             trace("beginWrite: beginning transaction.");
   134             db.beginTransaction();
   135         }
   136     }
   138     /**
   139      * If we're not in a batch, but we are in a write transaction, mark it as
   140      * successful.
   141      */
   142     protected void markWriteSuccessful(final SQLiteDatabase db) {
   143         if (isInBatch()) {
   144             trace("Not marking write successful: inside batch operation.");
   145             return;
   146         }
   148         if (shouldUseTransactions() && db.inTransaction()) {
   149             trace("Marking write transaction successful.");
   150             db.setTransactionSuccessful();
   151         }
   152     }
   154     /**
   155      * If we're not in a batch, but we are in a write transaction,
   156      * end it.
   157      *
   158      * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
   159      */
   160     protected void endWrite(final SQLiteDatabase db) {
   161         if (isInBatch()) {
   162             trace("Not ending write: inside batch operation.");
   163             return;
   164         }
   166         if (shouldUseTransactions() && db.inTransaction()) {
   167             trace("endWrite: ending transaction.");
   168             db.endTransaction();
   169         }
   170     }
   172     protected void beginBatch(final SQLiteDatabase db) {
   173         trace("Beginning batch.");
   174         isInBatchOperation.set(Boolean.TRUE);
   175         db.beginTransaction();
   176     }
   178     protected void markBatchSuccessful(final SQLiteDatabase db) {
   179         if (isInBatch()) {
   180             trace("Marking batch successful.");
   181             db.setTransactionSuccessful();
   182             return;
   183         }
   184         Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
   185         throw new IllegalStateException("Not in batch.");
   186     }
   188     protected void endBatch(final SQLiteDatabase db) {
   189         trace("Ending batch.");
   190         db.endTransaction();
   191         isInBatchOperation.set(Boolean.FALSE);
   192     }
   194     /**
   195      * Turn a single-column cursor of longs into a single SQL "IN" clause.
   196      * We can do this without using selection arguments because Long isn't
   197      * vulnerable to injection.
   198      */
   199     protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
   200         final StringBuilder builder = new StringBuilder(field);
   201         builder.append(" IN (");
   202         final int commaLimit = cursor.getCount() - 1;
   203         int i = 0;
   204         while (cursor.moveToNext()) {
   205             builder.append(cursor.getLong(0));
   206             if (i++ < commaLimit) {
   207                 builder.append(", ");
   208             }
   209         }
   210         builder.append(")");
   211         return builder.toString();
   212     }
   214     @Override
   215     public int delete(Uri uri, String selection, String[] selectionArgs) {
   216         trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
   218         final SQLiteDatabase db = getWritableDatabase(uri);
   219         int deleted = 0;
   221         try {
   222             deleted = deleteInTransaction(uri, selection, selectionArgs);
   223             markWriteSuccessful(db);
   224         } finally {
   225             endWrite(db);
   226         }
   228         if (deleted > 0) {
   229             final boolean shouldSyncToNetwork = !isCallerSync(uri);
   230             getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
   231         }
   233         return deleted;
   234     }
   236     @Override
   237     public Uri insert(Uri uri, ContentValues values) {
   238         trace("Calling insert on URI: " + uri);
   240         final SQLiteDatabase db = getWritableDatabase(uri);
   241         Uri result = null;
   242         try {
   243             result = insertInTransaction(uri, values);
   244             markWriteSuccessful(db);
   245         } catch (SQLException sqle) {
   246             Log.e(LOGTAG, "exception in DB operation", sqle);
   247         } catch (UnsupportedOperationException uoe) {
   248             Log.e(LOGTAG, "don't know how to perform that insert", uoe);
   249         } finally {
   250             endWrite(db);
   251         }
   253         if (result != null) {
   254             final boolean shouldSyncToNetwork = !isCallerSync(uri);
   255             getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
   256         }
   258         return result;
   259     }
   261     @Override
   262     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   263         trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
   265         final SQLiteDatabase db = getWritableDatabase(uri);
   266         int updated = 0;
   268         try {
   269             updated = updateInTransaction(uri, values, selection,
   270                                           selectionArgs);
   271             markWriteSuccessful(db);
   272         } finally {
   273             endWrite(db);
   274         }
   276         if (updated > 0) {
   277             final boolean shouldSyncToNetwork = !isCallerSync(uri);
   278             getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
   279         }
   281         return updated;
   282     }
   284     @Override
   285     public int bulkInsert(Uri uri, ContentValues[] values) {
   286         if (values == null) {
   287             return 0;
   288         }
   290         int numValues = values.length;
   291         int successes = 0;
   293         final SQLiteDatabase db = getWritableDatabase(uri);
   295         debug("bulkInsert: explicitly starting transaction.");
   296         beginBatch(db);
   298         try {
   299             for (int i = 0; i < numValues; i++) {
   300                 insertInTransaction(uri, values[i]);
   301                 successes++;
   302             }
   303             trace("Flushing DB bulkinsert...");
   304             markBatchSuccessful(db);
   305         } finally {
   306             debug("bulkInsert: explicitly ending transaction.");
   307             endBatch(db);
   308         }
   310         if (successes > 0) {
   311             final boolean shouldSyncToNetwork = !isCallerSync(uri);
   312             getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
   313         }
   315         return successes;
   316     }
   318     /**
   319      * Indicates whether a query should include deleted fields
   320      * based on the URI.
   321      * @param uri query URI
   322      */
   323     protected static boolean shouldShowDeleted(Uri uri) {
   324         String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
   325         return !TextUtils.isEmpty(showDeleted);
   326     }
   328     /**
   329      * Indicates whether an insertion should be made if a record doesn't
   330      * exist, based on the URI.
   331      * @param uri query URI
   332      */
   333     protected static boolean shouldUpdateOrInsert(Uri uri) {
   334         String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
   335         return Boolean.parseBoolean(insertIfNeeded);
   336     }
   338     /**
   339      * Indicates whether query is a test based on the URI.
   340      * @param uri query URI
   341      */
   342     protected static boolean isTest(Uri uri) {
   343         if (uri == null) {
   344             return false;
   345         }
   346         String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
   347         return !TextUtils.isEmpty(isTest);
   348     }
   350     /**
   351      * Return true of the query is from Firefox Sync.
   352      * @param uri query URI
   353      */
   354     protected static boolean isCallerSync(Uri uri) {
   355         String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
   356         return !TextUtils.isEmpty(isSync);
   357     }
   359     protected static void trace(String message) {
   360         if (logVerbose) {
   361             Log.v(LOGTAG, message);
   362         }
   363     }
   365     protected static void debug(String message) {
   366         if (logDebug) {
   367             Log.d(LOGTAG, message);
   368         }
   369     }
   370 }

mercurial