|
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 } |