summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/db/AbstractTransactionalProvider.java
diff options
context:
space:
mode:
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.java328
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