gecko-dev/mobile/android/base/db/AbstractTransactionalProvider.java
Richard Newman 3dabb9ceb8 Bug 986114 - Part 1: ReadingListProvider and BrowserProvider should share DB accessors. r=nalexander
* * *
Bug 986114 - Follow-up: Fix bustage on a CLOSED TREE.
2014-03-21 16:00:38 -07:00

370 lines
13 KiB
Java

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
/**
* This abstract class exists to capture some of the transaction-handling
* commonalities in Fennec's DB layer.
*
* In particular, this abstracts DB access, batching, and a particular
* transaction approach.
*
* That approach is: subclasses implement the abstract methods
* {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
* {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
* {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
*
* These are all called expecting a transaction to be established, so failed
* modifications can be rolled-back, and work batched.
*
* If no transaction is established, that's not a problem. Transaction nesting
* can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
*
* The decision of when to begin a transaction is left to the subclasses,
* primarily to avoid the pattern of a transaction being begun, a read occurring,
* and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
* which we don't handle well. Better to avoid starting a transaction too soon!
*
* You are probably interested in some subclasses:
*
* * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
* querying databases that are stored in the user's profile directory.
* * {@link PerProfileDatabaseProvider} is a simple version that only allows a
* single ContentProvider to access each per-profile database.
* * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
* that allows for multiple providers to safely work with the same databases.
*/
@SuppressWarnings("javadoc")
public abstract class AbstractTransactionalProvider extends ContentProvider {
private static final String LOGTAG = "GeckoTransProvider";
private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
/**
* Track whether we're in a batch operation.
*
* When we're in a batch operation, individual write steps won't even try
* to start a transaction... and neither will they attempt to finish one.
*
* Set this to <code>Boolean.TRUE</code> when you're entering a batch --
* a section of code in which {@link ContentProvider} methods will be
* called, but nested transactions should not be started. Callers are
* responsible for beginning and ending the enclosing transaction, and
* for setting this to <code>Boolean.FALSE</code> when done.
*
* This is a ThreadLocal separate from `db.inTransaction` because batched
* operations start transactions independent of individual ContentProvider
* operations. This doesn't work well with the entire concept of this
* abstract class -- that is, automatically beginning and ending transactions
* for each insert/delete/update operation -- and doing so without
* causing arbitrary nesting requires external tracking.
*
* Note that beginWrite takes a DB argument, but we don't differentiate
* between databases in this tracking flag. If your ContentProvider manages
* multiple database transactions within the same thread, you'll need to
* amend this scheme -- but then, you're already doing some serious wizardry,
* so rock on.
*/
final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
/**
* Return true if OS version and database parallelism support indicates
* that this provider should bundle writes into transactions.
*/
@SuppressWarnings("static-method")
protected boolean shouldUseTransactions() {
return Build.VERSION.SDK_INT >= 11;
}
protected static String computeSQLInClause(int items, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
int i = 0;
for (; i < items - 1; ++i) {
builder.append("?, ");
}
if (i < items) {
builder.append("?");
}
builder.append(")");
return builder.toString();
}
private boolean isInBatch() {
final Boolean isInBatch = isInBatchOperation.get();
if (isInBatch == null) {
return false;
}
return isInBatch.booleanValue();
}
/**
* If we're not currently in a transaction, and we should be, start one.
*/
protected void beginWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not bothering with an intermediate write transaction: inside batch operation.");
return;
}
if (shouldUseTransactions() && !db.inTransaction()) {
trace("beginWrite: beginning transaction.");
db.beginTransaction();
}
}
/**
* If we're not in a batch, but we are in a write transaction, mark it as
* successful.
*/
protected void markWriteSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not marking write successful: inside batch operation.");
return;
}
if (shouldUseTransactions() && db.inTransaction()) {
trace("Marking write transaction successful.");
db.setTransactionSuccessful();
}
}
/**
* If we're not in a batch, but we are in a write transaction,
* end it.
*
* @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
*/
protected void endWrite(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Not ending write: inside batch operation.");
return;
}
if (shouldUseTransactions() && db.inTransaction()) {
trace("endWrite: ending transaction.");
db.endTransaction();
}
}
protected void beginBatch(final SQLiteDatabase db) {
trace("Beginning batch.");
isInBatchOperation.set(Boolean.TRUE);
db.beginTransaction();
}
protected void markBatchSuccessful(final SQLiteDatabase db) {
if (isInBatch()) {
trace("Marking batch successful.");
db.setTransactionSuccessful();
return;
}
Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
throw new IllegalStateException("Not in batch.");
}
protected void endBatch(final SQLiteDatabase db) {
trace("Ending batch.");
db.endTransaction();
isInBatchOperation.set(Boolean.FALSE);
}
/**
* Turn a single-column cursor of longs into a single SQL "IN" clause.
* We can do this without using selection arguments because Long isn't
* vulnerable to injection.
*/
protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
final StringBuilder builder = new StringBuilder(field);
builder.append(" IN (");
final int commaLimit = cursor.getCount() - 1;
int i = 0;
while (cursor.moveToNext()) {
builder.append(cursor.getLong(0));
if (i++ < commaLimit) {
builder.append(", ");
}
}
builder.append(")");
return builder.toString();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int deleted = 0;
try {
deleted = deleteInTransaction(uri, selection, selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (deleted > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return deleted;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
trace("Calling insert on URI: " + uri);
final SQLiteDatabase db = getWritableDatabase(uri);
Uri result = null;
try {
result = insertInTransaction(uri, values);
markWriteSuccessful(db);
} catch (SQLException sqle) {
Log.e(LOGTAG, "exception in DB operation", sqle);
} catch (UnsupportedOperationException uoe) {
Log.e(LOGTAG, "don't know how to perform that insert", uoe);
} finally {
endWrite(db);
}
if (result != null) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return result;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
final SQLiteDatabase db = getWritableDatabase(uri);
int updated = 0;
try {
updated = updateInTransaction(uri, values, selection,
selectionArgs);
markWriteSuccessful(db);
} finally {
endWrite(db);
}
if (updated > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return updated;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
if (values == null) {
return 0;
}
int numValues = values.length;
int successes = 0;
final SQLiteDatabase db = getWritableDatabase(uri);
debug("bulkInsert: explicitly starting transaction.");
beginBatch(db);
try {
for (int i = 0; i < numValues; i++) {
insertInTransaction(uri, values[i]);
successes++;
}
trace("Flushing DB bulkinsert...");
markBatchSuccessful(db);
} finally {
debug("bulkInsert: explicitly ending transaction.");
endBatch(db);
}
if (successes > 0) {
final boolean shouldSyncToNetwork = !isCallerSync(uri);
getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
}
return successes;
}
/**
* Indicates whether a query should include deleted fields
* based on the URI.
* @param uri query URI
*/
protected static boolean shouldShowDeleted(Uri uri) {
String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
return !TextUtils.isEmpty(showDeleted);
}
/**
* Indicates whether an insertion should be made if a record doesn't
* exist, based on the URI.
* @param uri query URI
*/
protected static boolean shouldUpdateOrInsert(Uri uri) {
String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
return Boolean.parseBoolean(insertIfNeeded);
}
/**
* Indicates whether query is a test based on the URI.
* @param uri query URI
*/
protected static boolean isTest(Uri uri) {
if (uri == null) {
return false;
}
String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
return !TextUtils.isEmpty(isTest);
}
/**
* Return true of the query is from Firefox Sync.
* @param uri query URI
*/
protected static boolean isCallerSync(Uri uri) {
String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
return !TextUtils.isEmpty(isSync);
}
protected static void trace(String message) {
if (logVerbose) {
Log.v(LOGTAG, message);
}
}
protected static void debug(String message) {
if (logDebug) {
Log.d(LOGTAG, message);
}
}
}