1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/db/AbstractTransactionalProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,370 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.db; 1.9 + 1.10 +import android.content.ContentProvider; 1.11 +import android.content.ContentValues; 1.12 +import android.database.Cursor; 1.13 +import android.database.SQLException; 1.14 +import android.database.sqlite.SQLiteDatabase; 1.15 +import android.net.Uri; 1.16 +import android.os.Build; 1.17 +import android.text.TextUtils; 1.18 +import android.util.Log; 1.19 + 1.20 +/** 1.21 + * This abstract class exists to capture some of the transaction-handling 1.22 + * commonalities in Fennec's DB layer. 1.23 + * 1.24 + * In particular, this abstracts DB access, batching, and a particular 1.25 + * transaction approach. 1.26 + * 1.27 + * That approach is: subclasses implement the abstract methods 1.28 + * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)}, 1.29 + * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and 1.30 + * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}. 1.31 + * 1.32 + * These are all called expecting a transaction to be established, so failed 1.33 + * modifications can be rolled-back, and work batched. 1.34 + * 1.35 + * If no transaction is established, that's not a problem. Transaction nesting 1.36 + * can be avoided by using {@link #beginWrite(SQLiteDatabase)}. 1.37 + * 1.38 + * The decision of when to begin a transaction is left to the subclasses, 1.39 + * primarily to avoid the pattern of a transaction being begun, a read occurring, 1.40 + * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY, 1.41 + * which we don't handle well. Better to avoid starting a transaction too soon! 1.42 + * 1.43 + * You are probably interested in some subclasses: 1.44 + * 1.45 + * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for 1.46 + * querying databases that are stored in the user's profile directory. 1.47 + * * {@link PerProfileDatabaseProvider} is a simple version that only allows a 1.48 + * single ContentProvider to access each per-profile database. 1.49 + * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider 1.50 + * that allows for multiple providers to safely work with the same databases. 1.51 + */ 1.52 +@SuppressWarnings("javadoc") 1.53 +public abstract class AbstractTransactionalProvider extends ContentProvider { 1.54 + private static final String LOGTAG = "GeckoTransProvider"; 1.55 + 1.56 + private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); 1.57 + private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); 1.58 + 1.59 + protected abstract SQLiteDatabase getReadableDatabase(Uri uri); 1.60 + protected abstract SQLiteDatabase getWritableDatabase(Uri uri); 1.61 + 1.62 + public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri); 1.63 + 1.64 + protected abstract Uri insertInTransaction(Uri uri, ContentValues values); 1.65 + protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs); 1.66 + protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs); 1.67 + 1.68 + /** 1.69 + * Track whether we're in a batch operation. 1.70 + * 1.71 + * When we're in a batch operation, individual write steps won't even try 1.72 + * to start a transaction... and neither will they attempt to finish one. 1.73 + * 1.74 + * Set this to <code>Boolean.TRUE</code> when you're entering a batch -- 1.75 + * a section of code in which {@link ContentProvider} methods will be 1.76 + * called, but nested transactions should not be started. Callers are 1.77 + * responsible for beginning and ending the enclosing transaction, and 1.78 + * for setting this to <code>Boolean.FALSE</code> when done. 1.79 + * 1.80 + * This is a ThreadLocal separate from `db.inTransaction` because batched 1.81 + * operations start transactions independent of individual ContentProvider 1.82 + * operations. This doesn't work well with the entire concept of this 1.83 + * abstract class -- that is, automatically beginning and ending transactions 1.84 + * for each insert/delete/update operation -- and doing so without 1.85 + * causing arbitrary nesting requires external tracking. 1.86 + * 1.87 + * Note that beginWrite takes a DB argument, but we don't differentiate 1.88 + * between databases in this tracking flag. If your ContentProvider manages 1.89 + * multiple database transactions within the same thread, you'll need to 1.90 + * amend this scheme -- but then, you're already doing some serious wizardry, 1.91 + * so rock on. 1.92 + */ 1.93 + final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>(); 1.94 + 1.95 + /** 1.96 + * Return true if OS version and database parallelism support indicates 1.97 + * that this provider should bundle writes into transactions. 1.98 + */ 1.99 + @SuppressWarnings("static-method") 1.100 + protected boolean shouldUseTransactions() { 1.101 + return Build.VERSION.SDK_INT >= 11; 1.102 + } 1.103 + 1.104 + protected static String computeSQLInClause(int items, String field) { 1.105 + final StringBuilder builder = new StringBuilder(field); 1.106 + builder.append(" IN ("); 1.107 + int i = 0; 1.108 + for (; i < items - 1; ++i) { 1.109 + builder.append("?, "); 1.110 + } 1.111 + if (i < items) { 1.112 + builder.append("?"); 1.113 + } 1.114 + builder.append(")"); 1.115 + return builder.toString(); 1.116 + } 1.117 + 1.118 + private boolean isInBatch() { 1.119 + final Boolean isInBatch = isInBatchOperation.get(); 1.120 + if (isInBatch == null) { 1.121 + return false; 1.122 + } 1.123 + return isInBatch.booleanValue(); 1.124 + } 1.125 + 1.126 + /** 1.127 + * If we're not currently in a transaction, and we should be, start one. 1.128 + */ 1.129 + protected void beginWrite(final SQLiteDatabase db) { 1.130 + if (isInBatch()) { 1.131 + trace("Not bothering with an intermediate write transaction: inside batch operation."); 1.132 + return; 1.133 + } 1.134 + 1.135 + if (shouldUseTransactions() && !db.inTransaction()) { 1.136 + trace("beginWrite: beginning transaction."); 1.137 + db.beginTransaction(); 1.138 + } 1.139 + } 1.140 + 1.141 + /** 1.142 + * If we're not in a batch, but we are in a write transaction, mark it as 1.143 + * successful. 1.144 + */ 1.145 + protected void markWriteSuccessful(final SQLiteDatabase db) { 1.146 + if (isInBatch()) { 1.147 + trace("Not marking write successful: inside batch operation."); 1.148 + return; 1.149 + } 1.150 + 1.151 + if (shouldUseTransactions() && db.inTransaction()) { 1.152 + trace("Marking write transaction successful."); 1.153 + db.setTransactionSuccessful(); 1.154 + } 1.155 + } 1.156 + 1.157 + /** 1.158 + * If we're not in a batch, but we are in a write transaction, 1.159 + * end it. 1.160 + * 1.161 + * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase) 1.162 + */ 1.163 + protected void endWrite(final SQLiteDatabase db) { 1.164 + if (isInBatch()) { 1.165 + trace("Not ending write: inside batch operation."); 1.166 + return; 1.167 + } 1.168 + 1.169 + if (shouldUseTransactions() && db.inTransaction()) { 1.170 + trace("endWrite: ending transaction."); 1.171 + db.endTransaction(); 1.172 + } 1.173 + } 1.174 + 1.175 + protected void beginBatch(final SQLiteDatabase db) { 1.176 + trace("Beginning batch."); 1.177 + isInBatchOperation.set(Boolean.TRUE); 1.178 + db.beginTransaction(); 1.179 + } 1.180 + 1.181 + protected void markBatchSuccessful(final SQLiteDatabase db) { 1.182 + if (isInBatch()) { 1.183 + trace("Marking batch successful."); 1.184 + db.setTransactionSuccessful(); 1.185 + return; 1.186 + } 1.187 + Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!"); 1.188 + throw new IllegalStateException("Not in batch."); 1.189 + } 1.190 + 1.191 + protected void endBatch(final SQLiteDatabase db) { 1.192 + trace("Ending batch."); 1.193 + db.endTransaction(); 1.194 + isInBatchOperation.set(Boolean.FALSE); 1.195 + } 1.196 + 1.197 + /** 1.198 + * Turn a single-column cursor of longs into a single SQL "IN" clause. 1.199 + * We can do this without using selection arguments because Long isn't 1.200 + * vulnerable to injection. 1.201 + */ 1.202 + protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) { 1.203 + final StringBuilder builder = new StringBuilder(field); 1.204 + builder.append(" IN ("); 1.205 + final int commaLimit = cursor.getCount() - 1; 1.206 + int i = 0; 1.207 + while (cursor.moveToNext()) { 1.208 + builder.append(cursor.getLong(0)); 1.209 + if (i++ < commaLimit) { 1.210 + builder.append(", "); 1.211 + } 1.212 + } 1.213 + builder.append(")"); 1.214 + return builder.toString(); 1.215 + } 1.216 + 1.217 + @Override 1.218 + public int delete(Uri uri, String selection, String[] selectionArgs) { 1.219 + trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs); 1.220 + 1.221 + final SQLiteDatabase db = getWritableDatabase(uri); 1.222 + int deleted = 0; 1.223 + 1.224 + try { 1.225 + deleted = deleteInTransaction(uri, selection, selectionArgs); 1.226 + markWriteSuccessful(db); 1.227 + } finally { 1.228 + endWrite(db); 1.229 + } 1.230 + 1.231 + if (deleted > 0) { 1.232 + final boolean shouldSyncToNetwork = !isCallerSync(uri); 1.233 + getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); 1.234 + } 1.235 + 1.236 + return deleted; 1.237 + } 1.238 + 1.239 + @Override 1.240 + public Uri insert(Uri uri, ContentValues values) { 1.241 + trace("Calling insert on URI: " + uri); 1.242 + 1.243 + final SQLiteDatabase db = getWritableDatabase(uri); 1.244 + Uri result = null; 1.245 + try { 1.246 + result = insertInTransaction(uri, values); 1.247 + markWriteSuccessful(db); 1.248 + } catch (SQLException sqle) { 1.249 + Log.e(LOGTAG, "exception in DB operation", sqle); 1.250 + } catch (UnsupportedOperationException uoe) { 1.251 + Log.e(LOGTAG, "don't know how to perform that insert", uoe); 1.252 + } finally { 1.253 + endWrite(db); 1.254 + } 1.255 + 1.256 + if (result != null) { 1.257 + final boolean shouldSyncToNetwork = !isCallerSync(uri); 1.258 + getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); 1.259 + } 1.260 + 1.261 + return result; 1.262 + } 1.263 + 1.264 + @Override 1.265 + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 1.266 + trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs); 1.267 + 1.268 + final SQLiteDatabase db = getWritableDatabase(uri); 1.269 + int updated = 0; 1.270 + 1.271 + try { 1.272 + updated = updateInTransaction(uri, values, selection, 1.273 + selectionArgs); 1.274 + markWriteSuccessful(db); 1.275 + } finally { 1.276 + endWrite(db); 1.277 + } 1.278 + 1.279 + if (updated > 0) { 1.280 + final boolean shouldSyncToNetwork = !isCallerSync(uri); 1.281 + getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); 1.282 + } 1.283 + 1.284 + return updated; 1.285 + } 1.286 + 1.287 + @Override 1.288 + public int bulkInsert(Uri uri, ContentValues[] values) { 1.289 + if (values == null) { 1.290 + return 0; 1.291 + } 1.292 + 1.293 + int numValues = values.length; 1.294 + int successes = 0; 1.295 + 1.296 + final SQLiteDatabase db = getWritableDatabase(uri); 1.297 + 1.298 + debug("bulkInsert: explicitly starting transaction."); 1.299 + beginBatch(db); 1.300 + 1.301 + try { 1.302 + for (int i = 0; i < numValues; i++) { 1.303 + insertInTransaction(uri, values[i]); 1.304 + successes++; 1.305 + } 1.306 + trace("Flushing DB bulkinsert..."); 1.307 + markBatchSuccessful(db); 1.308 + } finally { 1.309 + debug("bulkInsert: explicitly ending transaction."); 1.310 + endBatch(db); 1.311 + } 1.312 + 1.313 + if (successes > 0) { 1.314 + final boolean shouldSyncToNetwork = !isCallerSync(uri); 1.315 + getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); 1.316 + } 1.317 + 1.318 + return successes; 1.319 + } 1.320 + 1.321 + /** 1.322 + * Indicates whether a query should include deleted fields 1.323 + * based on the URI. 1.324 + * @param uri query URI 1.325 + */ 1.326 + protected static boolean shouldShowDeleted(Uri uri) { 1.327 + String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED); 1.328 + return !TextUtils.isEmpty(showDeleted); 1.329 + } 1.330 + 1.331 + /** 1.332 + * Indicates whether an insertion should be made if a record doesn't 1.333 + * exist, based on the URI. 1.334 + * @param uri query URI 1.335 + */ 1.336 + protected static boolean shouldUpdateOrInsert(Uri uri) { 1.337 + String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED); 1.338 + return Boolean.parseBoolean(insertIfNeeded); 1.339 + } 1.340 + 1.341 + /** 1.342 + * Indicates whether query is a test based on the URI. 1.343 + * @param uri query URI 1.344 + */ 1.345 + protected static boolean isTest(Uri uri) { 1.346 + if (uri == null) { 1.347 + return false; 1.348 + } 1.349 + String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST); 1.350 + return !TextUtils.isEmpty(isTest); 1.351 + } 1.352 + 1.353 + /** 1.354 + * Return true of the query is from Firefox Sync. 1.355 + * @param uri query URI 1.356 + */ 1.357 + protected static boolean isCallerSync(Uri uri) { 1.358 + String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC); 1.359 + return !TextUtils.isEmpty(isSync); 1.360 + } 1.361 + 1.362 + protected static void trace(String message) { 1.363 + if (logVerbose) { 1.364 + Log.v(LOGTAG, message); 1.365 + } 1.366 + } 1.367 + 1.368 + protected static void debug(String message) { 1.369 + if (logDebug) { 1.370 + Log.d(LOGTAG, message); 1.371 + } 1.372 + } 1.373 +} 1.374 \ No newline at end of file