|
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/. */ |
|
4 |
|
5 package org.mozilla.gecko.db; |
|
6 |
|
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; |
|
16 |
|
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"; |
|
52 |
|
53 private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG); |
|
54 private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE); |
|
55 |
|
56 protected abstract SQLiteDatabase getReadableDatabase(Uri uri); |
|
57 protected abstract SQLiteDatabase getWritableDatabase(Uri uri); |
|
58 |
|
59 public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri); |
|
60 |
|
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); |
|
64 |
|
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>(); |
|
91 |
|
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 } |
|
100 |
|
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 } |
|
114 |
|
115 private boolean isInBatch() { |
|
116 final Boolean isInBatch = isInBatchOperation.get(); |
|
117 if (isInBatch == null) { |
|
118 return false; |
|
119 } |
|
120 return isInBatch.booleanValue(); |
|
121 } |
|
122 |
|
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 } |
|
131 |
|
132 if (shouldUseTransactions() && !db.inTransaction()) { |
|
133 trace("beginWrite: beginning transaction."); |
|
134 db.beginTransaction(); |
|
135 } |
|
136 } |
|
137 |
|
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 } |
|
147 |
|
148 if (shouldUseTransactions() && db.inTransaction()) { |
|
149 trace("Marking write transaction successful."); |
|
150 db.setTransactionSuccessful(); |
|
151 } |
|
152 } |
|
153 |
|
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 } |
|
165 |
|
166 if (shouldUseTransactions() && db.inTransaction()) { |
|
167 trace("endWrite: ending transaction."); |
|
168 db.endTransaction(); |
|
169 } |
|
170 } |
|
171 |
|
172 protected void beginBatch(final SQLiteDatabase db) { |
|
173 trace("Beginning batch."); |
|
174 isInBatchOperation.set(Boolean.TRUE); |
|
175 db.beginTransaction(); |
|
176 } |
|
177 |
|
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 } |
|
187 |
|
188 protected void endBatch(final SQLiteDatabase db) { |
|
189 trace("Ending batch."); |
|
190 db.endTransaction(); |
|
191 isInBatchOperation.set(Boolean.FALSE); |
|
192 } |
|
193 |
|
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 } |
|
213 |
|
214 @Override |
|
215 public int delete(Uri uri, String selection, String[] selectionArgs) { |
|
216 trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs); |
|
217 |
|
218 final SQLiteDatabase db = getWritableDatabase(uri); |
|
219 int deleted = 0; |
|
220 |
|
221 try { |
|
222 deleted = deleteInTransaction(uri, selection, selectionArgs); |
|
223 markWriteSuccessful(db); |
|
224 } finally { |
|
225 endWrite(db); |
|
226 } |
|
227 |
|
228 if (deleted > 0) { |
|
229 final boolean shouldSyncToNetwork = !isCallerSync(uri); |
|
230 getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); |
|
231 } |
|
232 |
|
233 return deleted; |
|
234 } |
|
235 |
|
236 @Override |
|
237 public Uri insert(Uri uri, ContentValues values) { |
|
238 trace("Calling insert on URI: " + uri); |
|
239 |
|
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 } |
|
252 |
|
253 if (result != null) { |
|
254 final boolean shouldSyncToNetwork = !isCallerSync(uri); |
|
255 getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); |
|
256 } |
|
257 |
|
258 return result; |
|
259 } |
|
260 |
|
261 @Override |
|
262 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
|
263 trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs); |
|
264 |
|
265 final SQLiteDatabase db = getWritableDatabase(uri); |
|
266 int updated = 0; |
|
267 |
|
268 try { |
|
269 updated = updateInTransaction(uri, values, selection, |
|
270 selectionArgs); |
|
271 markWriteSuccessful(db); |
|
272 } finally { |
|
273 endWrite(db); |
|
274 } |
|
275 |
|
276 if (updated > 0) { |
|
277 final boolean shouldSyncToNetwork = !isCallerSync(uri); |
|
278 getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); |
|
279 } |
|
280 |
|
281 return updated; |
|
282 } |
|
283 |
|
284 @Override |
|
285 public int bulkInsert(Uri uri, ContentValues[] values) { |
|
286 if (values == null) { |
|
287 return 0; |
|
288 } |
|
289 |
|
290 int numValues = values.length; |
|
291 int successes = 0; |
|
292 |
|
293 final SQLiteDatabase db = getWritableDatabase(uri); |
|
294 |
|
295 debug("bulkInsert: explicitly starting transaction."); |
|
296 beginBatch(db); |
|
297 |
|
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 } |
|
309 |
|
310 if (successes > 0) { |
|
311 final boolean shouldSyncToNetwork = !isCallerSync(uri); |
|
312 getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); |
|
313 } |
|
314 |
|
315 return successes; |
|
316 } |
|
317 |
|
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 } |
|
327 |
|
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 } |
|
337 |
|
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 } |
|
349 |
|
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 } |
|
358 |
|
359 protected static void trace(String message) { |
|
360 if (logVerbose) { |
|
361 Log.v(LOGTAG, message); |
|
362 } |
|
363 } |
|
364 |
|
365 protected static void debug(String message) { |
|
366 if (logDebug) { |
|
367 Log.d(LOGTAG, message); |
|
368 } |
|
369 } |
|
370 } |