Wed, 31 Dec 2014 06:09:35 +0100
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 }