diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java new file mode 100644 index 000000000..7e289b76f --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java @@ -0,0 +1,328 @@ +/* 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 org.mozilla.gecko.AppConstants.Versions; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +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 final boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); + private static final 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>(); + + private boolean isInBatch() { + final Boolean isInBatch = isInBatchOperation.get(); + if (isInBatch == null) { + return false; + } + + return isInBatch; + } + + /** + * 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 (!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 (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 (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); + } + + @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); + } + } +}
\ No newline at end of file |