mobile/android/base/db/SQLiteBridgeContentProvider.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:e465aa16b1be
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 package org.mozilla.gecko.db;
6
7 import java.io.File;
8 import java.util.HashMap;
9
10 import org.mozilla.gecko.GeckoProfile;
11 import org.mozilla.gecko.GeckoThread;
12 import org.mozilla.gecko.Telemetry;
13 import org.mozilla.gecko.mozglue.GeckoLoader;
14 import org.mozilla.gecko.sqlite.SQLiteBridge;
15 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
16
17 import android.content.ContentProvider;
18 import android.content.ContentUris;
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.text.TextUtils;
24 import android.util.Log;
25
26 /*
27 * Provides a basic ContentProvider that sets up and sends queries through
28 * SQLiteBridge. Content providers should extend this by setting the appropriate
29 * table and version numbers in onCreate, and implementing the abstract methods:
30 *
31 * public abstract String getTable(Uri uri);
32 * public abstract String getSortOrder(Uri uri, String aRequested);
33 * public abstract void setupDefaults(Uri uri, ContentValues values);
34 * public abstract void initGecko();
35 */
36
37 public abstract class SQLiteBridgeContentProvider extends ContentProvider {
38 private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked";
39
40 private HashMap<String, SQLiteBridge> mDatabasePerProfile;
41 protected Context mContext = null;
42 private final String mLogTag;
43
44 protected SQLiteBridgeContentProvider(String logTag) {
45 mLogTag = logTag;
46 }
47
48 /**
49 * Subclasses must override this to allow error reporting code to compose
50 * the correct histogram name.
51 *
52 * Ensure that you define the new histograms if you define a new class!
53 */
54 protected abstract String getTelemetryPrefix();
55
56 /**
57 * Errors are recorded in telemetry using an enumerated histogram.
58 *
59 * <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/
60 * Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type>
61 *
62 * These are the allowable enumeration values. Keep these in sync with the
63 * histogram definition!
64 *
65 */
66 private static enum TelemetryErrorOp {
67 BULKINSERT (0),
68 DELETE (1),
69 INSERT (2),
70 QUERY (3),
71 UPDATE (4);
72
73 private final int bucket;
74
75 TelemetryErrorOp(final int bucket) {
76 this.bucket = bucket;
77 }
78
79 public int getBucket() {
80 return bucket;
81 }
82 }
83
84 @Override
85 public void shutdown() {
86 if (mDatabasePerProfile == null) {
87 return;
88 }
89
90 synchronized (this) {
91 for (SQLiteBridge bridge : mDatabasePerProfile.values()) {
92 if (bridge != null) {
93 try {
94 bridge.close();
95 } catch (Exception ex) { }
96 }
97 }
98 mDatabasePerProfile = null;
99 }
100 }
101
102 @Override
103 public void finalize() {
104 shutdown();
105 }
106
107 /**
108 * Return true of the query is from Firefox Sync.
109 * @param uri query URI
110 */
111 public static boolean isCallerSync(Uri uri) {
112 String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
113 return !TextUtils.isEmpty(isSync);
114 }
115
116 private SQLiteBridge getDB(Context context, final String databasePath) {
117 SQLiteBridge bridge = null;
118
119 boolean dbNeedsSetup = true;
120 try {
121 String resourcePath = context.getPackageResourcePath();
122 GeckoLoader.loadSQLiteLibs(context, resourcePath);
123 GeckoLoader.loadNSSLibs(context, resourcePath);
124 bridge = SQLiteBridge.openDatabase(databasePath, null, 0);
125 int version = bridge.getVersion();
126 dbNeedsSetup = version != getDBVersion();
127 } catch (SQLiteBridgeException ex) {
128 // close the database
129 if (bridge != null) {
130 bridge.close();
131 }
132
133 // this will throw if the database can't be found
134 // we should attempt to set it up if Gecko is running
135 dbNeedsSetup = true;
136 Log.e(mLogTag, "Error getting version ", ex);
137
138 // if Gecko is not running, we should bail out. Otherwise we try to
139 // let Gecko build the database for us
140 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
141 Log.e(mLogTag, "Can not set up database. Gecko is not running");
142 return null;
143 }
144 }
145
146 // If the database is not set up yet, or is the wrong schema version, we send an initialize
147 // call to Gecko. Gecko will handle building the database file correctly, as well as any
148 // migrations that are necessary
149 if (dbNeedsSetup) {
150 bridge = null;
151 initGecko();
152 }
153 return bridge;
154 }
155
156 /**
157 * Returns the absolute path of a database file depending on the specified profile and dbName.
158 * @param profile
159 * the profile whose dbPath must be returned
160 * @param dbName
161 * the name of the db file whose absolute path must be returned
162 * @return the absolute path of the db file or <code>null</code> if it was not possible to retrieve a valid path
163 *
164 */
165 private String getDatabasePathForProfile(String profile, String dbName) {
166 // Depends on the vagaries of GeckoProfile.get, so null check for safety.
167 File profileDir = GeckoProfile.get(mContext, profile).getDir();
168 if (profileDir == null) {
169 return null;
170 }
171
172 String databasePath = new File(profileDir, dbName).getAbsolutePath();
173 return databasePath;
174 }
175
176 /**
177 * Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the
178 * current provider instance.
179 * @param profile
180 * the id of the profile to be used to retrieve the related SQLiteBridge
181 * @return the <code>SQLiteBridge</code> related to the specified profile id or <code>null</code> if it was
182 * not possible to retrieve a valid SQLiteBridge
183 */
184 private SQLiteBridge getDatabaseForProfile(String profile) {
185 if (TextUtils.isEmpty(profile)) {
186 profile = GeckoProfile.get(mContext).getName();
187 Log.d(mLogTag, "No profile provided, using '" + profile + "'");
188 }
189
190 final String dbName = getDBName();
191 String mapKey = profile + "/" + dbName;
192
193 SQLiteBridge db = null;
194 synchronized (this) {
195 db = mDatabasePerProfile.get(mapKey);
196 if (db != null) {
197 return db;
198 }
199 final String dbPath = getDatabasePathForProfile(profile, dbName);
200 if (dbPath == null) {
201 Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'");
202 return null;
203 }
204 db = getDB(mContext, dbPath);
205 if (db != null) {
206 mDatabasePerProfile.put(mapKey, db);
207 }
208 }
209 return db;
210 }
211
212 /**
213 * Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the
214 * current provider instance.
215 * @param profilePath
216 * the profilePath to be used to retrieve the related SQLiteBridge
217 * @return the <code>SQLiteBridge</code> related to the specified profile path or <code>null</code> if it was
218 * not possible to retrieve a valid <code>SQLiteBridge</code>
219 */
220 private SQLiteBridge getDatabaseForProfilePath(String profilePath) {
221 File profileDir = new File(profilePath, getDBName());
222 final String dbPath = profileDir.getPath();
223 return getDatabaseForDBPath(dbPath);
224 }
225
226 /**
227 * Returns a SQLiteBridge object according to the specified file path.
228 * @param dbPath
229 * the path of the file to be used to retrieve the related SQLiteBridge
230 * @return the <code>SQLiteBridge</code> related to the specified file path or <code>null</code> if it was
231 * not possible to retrieve a valid <code>SQLiteBridge</code>
232 *
233 */
234 private SQLiteBridge getDatabaseForDBPath(String dbPath) {
235 SQLiteBridge db = null;
236 synchronized (this) {
237 db = mDatabasePerProfile.get(dbPath);
238 if (db != null) {
239 return db;
240 }
241 db = getDB(mContext, dbPath);
242 if (db != null) {
243 mDatabasePerProfile.put(dbPath, db);
244 }
245 }
246 return db;
247 }
248
249 /**
250 * Returns a SQLiteBridge object to be used to perform operations on the given <code>Uri</code>.
251 * @param uri
252 * the <code>Uri</code> to be used to retrieve the related SQLiteBridge
253 * @return a <code>SQLiteBridge</code> object to be used on the given uri or <code>null</code> if it was
254 * not possible to retrieve a valid <code>SQLiteBridge</code>
255 *
256 */
257 private SQLiteBridge getDatabase(Uri uri) {
258 String profile = null;
259 String profilePath = null;
260
261 profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
262 profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH);
263
264 // Testing will specify the absolute profile path
265 if (profilePath != null) {
266 return getDatabaseForProfilePath(profilePath);
267 }
268 return getDatabaseForProfile(profile);
269 }
270
271 @Override
272 public boolean onCreate() {
273 mContext = getContext();
274 synchronized (this) {
275 mDatabasePerProfile = new HashMap<String, SQLiteBridge>();
276 }
277 return true;
278 }
279
280 @Override
281 public String getType(Uri uri) {
282 return null;
283 }
284
285 @Override
286 public int delete(Uri uri, String selection, String[] selectionArgs) {
287 int deleted = 0;
288 final SQLiteBridge db = getDatabase(uri);
289 if (db == null) {
290 return deleted;
291 }
292
293 try {
294 deleted = db.delete(getTable(uri), selection, selectionArgs);
295 } catch (SQLiteBridgeException ex) {
296 reportError(ex, TelemetryErrorOp.DELETE);
297 throw ex;
298 }
299
300 return deleted;
301 }
302
303 @Override
304 public Uri insert(Uri uri, ContentValues values) {
305 long id = -1;
306 final SQLiteBridge db = getDatabase(uri);
307
308 // If we can not get a SQLiteBridge instance, its likely that the database
309 // has not been set up and Gecko is not running. We return null and expect
310 // callers to try again later
311 if (db == null) {
312 return null;
313 }
314
315 setupDefaults(uri, values);
316
317 boolean useTransaction = !db.inTransaction();
318 try {
319 if (useTransaction) {
320 db.beginTransaction();
321 }
322
323 // onPreInsert does a check for the item in the deleted table in some cases
324 // so we put it inside this transaction
325 onPreInsert(values, uri, db);
326 id = db.insert(getTable(uri), null, values);
327
328 if (useTransaction) {
329 db.setTransactionSuccessful();
330 }
331 } catch (SQLiteBridgeException ex) {
332 reportError(ex, TelemetryErrorOp.INSERT);
333 throw ex;
334 } finally {
335 if (useTransaction) {
336 db.endTransaction();
337 }
338 }
339
340 return ContentUris.withAppendedId(uri, id);
341 }
342
343 @Override
344 public int bulkInsert(Uri uri, ContentValues[] allValues) {
345 final SQLiteBridge db = getDatabase(uri);
346 // If we can not get a SQLiteBridge instance, its likely that the database
347 // has not been set up and Gecko is not running. We return 0 and expect
348 // callers to try again later
349 if (db == null) {
350 return 0;
351 }
352
353 int rowsAdded = 0;
354
355 String table = getTable(uri);
356
357 try {
358 db.beginTransaction();
359 for (ContentValues initialValues : allValues) {
360 ContentValues values = new ContentValues(initialValues);
361 setupDefaults(uri, values);
362 onPreInsert(values, uri, db);
363 db.insert(table, null, values);
364 rowsAdded++;
365 }
366 db.setTransactionSuccessful();
367 } catch (SQLiteBridgeException ex) {
368 reportError(ex, TelemetryErrorOp.BULKINSERT);
369 throw ex;
370 } finally {
371 db.endTransaction();
372 }
373
374 if (rowsAdded > 0) {
375 final boolean shouldSyncToNetwork = !isCallerSync(uri);
376 mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
377 }
378
379 return rowsAdded;
380 }
381
382 @Override
383 public int update(Uri uri, ContentValues values, String selection,
384 String[] selectionArgs) {
385 int updated = 0;
386 final SQLiteBridge db = getDatabase(uri);
387
388 // If we can not get a SQLiteBridge instance, its likely that the database
389 // has not been set up and Gecko is not running. We return null and expect
390 // callers to try again later
391 if (db == null) {
392 return updated;
393 }
394
395 onPreUpdate(values, uri, db);
396
397 try {
398 updated = db.update(getTable(uri), values, selection, selectionArgs);
399 } catch (SQLiteBridgeException ex) {
400 reportError(ex, TelemetryErrorOp.UPDATE);
401 throw ex;
402 }
403
404 return updated;
405 }
406
407 @Override
408 public Cursor query(Uri uri, String[] projection, String selection,
409 String[] selectionArgs, String sortOrder) {
410 Cursor cursor = null;
411 final SQLiteBridge db = getDatabase(uri);
412
413 // If we can not get a SQLiteBridge instance, its likely that the database
414 // has not been set up and Gecko is not running. We return null and expect
415 // callers to try again later
416 if (db == null) {
417 return cursor;
418 }
419
420 sortOrder = getSortOrder(uri, sortOrder);
421
422 try {
423 cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null);
424 onPostQuery(cursor, uri, db);
425 } catch (SQLiteBridgeException ex) {
426 reportError(ex, TelemetryErrorOp.QUERY);
427 throw ex;
428 }
429
430 return cursor;
431 }
432
433 private String getHistogram(SQLiteBridgeException e) {
434 // If you add values here, make sure to update
435 // toolkit/components/telemetry/Histograms.json.
436 if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) {
437 return getTelemetryPrefix() + "_LOCKED";
438 }
439 return null;
440 }
441
442 protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) {
443 Log.e(mLogTag, "Error in database " + op.name(), e);
444 final String histogram = getHistogram(e);
445 if (histogram == null) {
446 return;
447 }
448
449 Telemetry.HistogramAdd(histogram, op.getBucket());
450 }
451
452 protected abstract String getDBName();
453
454 protected abstract int getDBVersion();
455
456 protected abstract String getTable(Uri uri);
457
458 protected abstract String getSortOrder(Uri uri, String aRequested);
459
460 protected abstract void setupDefaults(Uri uri, ContentValues values);
461
462 protected abstract void initGecko();
463
464 protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db);
465
466 protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db);
467
468 protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db);
469 }

mercurial