1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/db/SQLiteBridgeContentProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,469 @@ 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 file, 1.6 + * 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 java.io.File; 1.11 +import java.util.HashMap; 1.12 + 1.13 +import org.mozilla.gecko.GeckoProfile; 1.14 +import org.mozilla.gecko.GeckoThread; 1.15 +import org.mozilla.gecko.Telemetry; 1.16 +import org.mozilla.gecko.mozglue.GeckoLoader; 1.17 +import org.mozilla.gecko.sqlite.SQLiteBridge; 1.18 +import org.mozilla.gecko.sqlite.SQLiteBridgeException; 1.19 + 1.20 +import android.content.ContentProvider; 1.21 +import android.content.ContentUris; 1.22 +import android.content.ContentValues; 1.23 +import android.content.Context; 1.24 +import android.database.Cursor; 1.25 +import android.net.Uri; 1.26 +import android.text.TextUtils; 1.27 +import android.util.Log; 1.28 + 1.29 +/* 1.30 + * Provides a basic ContentProvider that sets up and sends queries through 1.31 + * SQLiteBridge. Content providers should extend this by setting the appropriate 1.32 + * table and version numbers in onCreate, and implementing the abstract methods: 1.33 + * 1.34 + * public abstract String getTable(Uri uri); 1.35 + * public abstract String getSortOrder(Uri uri, String aRequested); 1.36 + * public abstract void setupDefaults(Uri uri, ContentValues values); 1.37 + * public abstract void initGecko(); 1.38 + */ 1.39 + 1.40 +public abstract class SQLiteBridgeContentProvider extends ContentProvider { 1.41 + private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked"; 1.42 + 1.43 + private HashMap<String, SQLiteBridge> mDatabasePerProfile; 1.44 + protected Context mContext = null; 1.45 + private final String mLogTag; 1.46 + 1.47 + protected SQLiteBridgeContentProvider(String logTag) { 1.48 + mLogTag = logTag; 1.49 + } 1.50 + 1.51 + /** 1.52 + * Subclasses must override this to allow error reporting code to compose 1.53 + * the correct histogram name. 1.54 + * 1.55 + * Ensure that you define the new histograms if you define a new class! 1.56 + */ 1.57 + protected abstract String getTelemetryPrefix(); 1.58 + 1.59 + /** 1.60 + * Errors are recorded in telemetry using an enumerated histogram. 1.61 + * 1.62 + * <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/ 1.63 + * Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type> 1.64 + * 1.65 + * These are the allowable enumeration values. Keep these in sync with the 1.66 + * histogram definition! 1.67 + * 1.68 + */ 1.69 + private static enum TelemetryErrorOp { 1.70 + BULKINSERT (0), 1.71 + DELETE (1), 1.72 + INSERT (2), 1.73 + QUERY (3), 1.74 + UPDATE (4); 1.75 + 1.76 + private final int bucket; 1.77 + 1.78 + TelemetryErrorOp(final int bucket) { 1.79 + this.bucket = bucket; 1.80 + } 1.81 + 1.82 + public int getBucket() { 1.83 + return bucket; 1.84 + } 1.85 + } 1.86 + 1.87 + @Override 1.88 + public void shutdown() { 1.89 + if (mDatabasePerProfile == null) { 1.90 + return; 1.91 + } 1.92 + 1.93 + synchronized (this) { 1.94 + for (SQLiteBridge bridge : mDatabasePerProfile.values()) { 1.95 + if (bridge != null) { 1.96 + try { 1.97 + bridge.close(); 1.98 + } catch (Exception ex) { } 1.99 + } 1.100 + } 1.101 + mDatabasePerProfile = null; 1.102 + } 1.103 + } 1.104 + 1.105 + @Override 1.106 + public void finalize() { 1.107 + shutdown(); 1.108 + } 1.109 + 1.110 + /** 1.111 + * Return true of the query is from Firefox Sync. 1.112 + * @param uri query URI 1.113 + */ 1.114 + public static boolean isCallerSync(Uri uri) { 1.115 + String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC); 1.116 + return !TextUtils.isEmpty(isSync); 1.117 + } 1.118 + 1.119 + private SQLiteBridge getDB(Context context, final String databasePath) { 1.120 + SQLiteBridge bridge = null; 1.121 + 1.122 + boolean dbNeedsSetup = true; 1.123 + try { 1.124 + String resourcePath = context.getPackageResourcePath(); 1.125 + GeckoLoader.loadSQLiteLibs(context, resourcePath); 1.126 + GeckoLoader.loadNSSLibs(context, resourcePath); 1.127 + bridge = SQLiteBridge.openDatabase(databasePath, null, 0); 1.128 + int version = bridge.getVersion(); 1.129 + dbNeedsSetup = version != getDBVersion(); 1.130 + } catch (SQLiteBridgeException ex) { 1.131 + // close the database 1.132 + if (bridge != null) { 1.133 + bridge.close(); 1.134 + } 1.135 + 1.136 + // this will throw if the database can't be found 1.137 + // we should attempt to set it up if Gecko is running 1.138 + dbNeedsSetup = true; 1.139 + Log.e(mLogTag, "Error getting version ", ex); 1.140 + 1.141 + // if Gecko is not running, we should bail out. Otherwise we try to 1.142 + // let Gecko build the database for us 1.143 + if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { 1.144 + Log.e(mLogTag, "Can not set up database. Gecko is not running"); 1.145 + return null; 1.146 + } 1.147 + } 1.148 + 1.149 + // If the database is not set up yet, or is the wrong schema version, we send an initialize 1.150 + // call to Gecko. Gecko will handle building the database file correctly, as well as any 1.151 + // migrations that are necessary 1.152 + if (dbNeedsSetup) { 1.153 + bridge = null; 1.154 + initGecko(); 1.155 + } 1.156 + return bridge; 1.157 + } 1.158 + 1.159 + /** 1.160 + * Returns the absolute path of a database file depending on the specified profile and dbName. 1.161 + * @param profile 1.162 + * the profile whose dbPath must be returned 1.163 + * @param dbName 1.164 + * the name of the db file whose absolute path must be returned 1.165 + * @return the absolute path of the db file or <code>null</code> if it was not possible to retrieve a valid path 1.166 + * 1.167 + */ 1.168 + private String getDatabasePathForProfile(String profile, String dbName) { 1.169 + // Depends on the vagaries of GeckoProfile.get, so null check for safety. 1.170 + File profileDir = GeckoProfile.get(mContext, profile).getDir(); 1.171 + if (profileDir == null) { 1.172 + return null; 1.173 + } 1.174 + 1.175 + String databasePath = new File(profileDir, dbName).getAbsolutePath(); 1.176 + return databasePath; 1.177 + } 1.178 + 1.179 + /** 1.180 + * Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the 1.181 + * current provider instance. 1.182 + * @param profile 1.183 + * the id of the profile to be used to retrieve the related SQLiteBridge 1.184 + * @return the <code>SQLiteBridge</code> related to the specified profile id or <code>null</code> if it was 1.185 + * not possible to retrieve a valid SQLiteBridge 1.186 + */ 1.187 + private SQLiteBridge getDatabaseForProfile(String profile) { 1.188 + if (TextUtils.isEmpty(profile)) { 1.189 + profile = GeckoProfile.get(mContext).getName(); 1.190 + Log.d(mLogTag, "No profile provided, using '" + profile + "'"); 1.191 + } 1.192 + 1.193 + final String dbName = getDBName(); 1.194 + String mapKey = profile + "/" + dbName; 1.195 + 1.196 + SQLiteBridge db = null; 1.197 + synchronized (this) { 1.198 + db = mDatabasePerProfile.get(mapKey); 1.199 + if (db != null) { 1.200 + return db; 1.201 + } 1.202 + final String dbPath = getDatabasePathForProfile(profile, dbName); 1.203 + if (dbPath == null) { 1.204 + Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'"); 1.205 + return null; 1.206 + } 1.207 + db = getDB(mContext, dbPath); 1.208 + if (db != null) { 1.209 + mDatabasePerProfile.put(mapKey, db); 1.210 + } 1.211 + } 1.212 + return db; 1.213 + } 1.214 + 1.215 + /** 1.216 + * Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the 1.217 + * current provider instance. 1.218 + * @param profilePath 1.219 + * the profilePath to be used to retrieve the related SQLiteBridge 1.220 + * @return the <code>SQLiteBridge</code> related to the specified profile path or <code>null</code> if it was 1.221 + * not possible to retrieve a valid <code>SQLiteBridge</code> 1.222 + */ 1.223 + private SQLiteBridge getDatabaseForProfilePath(String profilePath) { 1.224 + File profileDir = new File(profilePath, getDBName()); 1.225 + final String dbPath = profileDir.getPath(); 1.226 + return getDatabaseForDBPath(dbPath); 1.227 + } 1.228 + 1.229 + /** 1.230 + * Returns a SQLiteBridge object according to the specified file path. 1.231 + * @param dbPath 1.232 + * the path of the file to be used to retrieve the related SQLiteBridge 1.233 + * @return the <code>SQLiteBridge</code> related to the specified file path or <code>null</code> if it was 1.234 + * not possible to retrieve a valid <code>SQLiteBridge</code> 1.235 + * 1.236 + */ 1.237 + private SQLiteBridge getDatabaseForDBPath(String dbPath) { 1.238 + SQLiteBridge db = null; 1.239 + synchronized (this) { 1.240 + db = mDatabasePerProfile.get(dbPath); 1.241 + if (db != null) { 1.242 + return db; 1.243 + } 1.244 + db = getDB(mContext, dbPath); 1.245 + if (db != null) { 1.246 + mDatabasePerProfile.put(dbPath, db); 1.247 + } 1.248 + } 1.249 + return db; 1.250 + } 1.251 + 1.252 + /** 1.253 + * Returns a SQLiteBridge object to be used to perform operations on the given <code>Uri</code>. 1.254 + * @param uri 1.255 + * the <code>Uri</code> to be used to retrieve the related SQLiteBridge 1.256 + * @return a <code>SQLiteBridge</code> object to be used on the given uri or <code>null</code> if it was 1.257 + * not possible to retrieve a valid <code>SQLiteBridge</code> 1.258 + * 1.259 + */ 1.260 + private SQLiteBridge getDatabase(Uri uri) { 1.261 + String profile = null; 1.262 + String profilePath = null; 1.263 + 1.264 + profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); 1.265 + profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH); 1.266 + 1.267 + // Testing will specify the absolute profile path 1.268 + if (profilePath != null) { 1.269 + return getDatabaseForProfilePath(profilePath); 1.270 + } 1.271 + return getDatabaseForProfile(profile); 1.272 + } 1.273 + 1.274 + @Override 1.275 + public boolean onCreate() { 1.276 + mContext = getContext(); 1.277 + synchronized (this) { 1.278 + mDatabasePerProfile = new HashMap<String, SQLiteBridge>(); 1.279 + } 1.280 + return true; 1.281 + } 1.282 + 1.283 + @Override 1.284 + public String getType(Uri uri) { 1.285 + return null; 1.286 + } 1.287 + 1.288 + @Override 1.289 + public int delete(Uri uri, String selection, String[] selectionArgs) { 1.290 + int deleted = 0; 1.291 + final SQLiteBridge db = getDatabase(uri); 1.292 + if (db == null) { 1.293 + return deleted; 1.294 + } 1.295 + 1.296 + try { 1.297 + deleted = db.delete(getTable(uri), selection, selectionArgs); 1.298 + } catch (SQLiteBridgeException ex) { 1.299 + reportError(ex, TelemetryErrorOp.DELETE); 1.300 + throw ex; 1.301 + } 1.302 + 1.303 + return deleted; 1.304 + } 1.305 + 1.306 + @Override 1.307 + public Uri insert(Uri uri, ContentValues values) { 1.308 + long id = -1; 1.309 + final SQLiteBridge db = getDatabase(uri); 1.310 + 1.311 + // If we can not get a SQLiteBridge instance, its likely that the database 1.312 + // has not been set up and Gecko is not running. We return null and expect 1.313 + // callers to try again later 1.314 + if (db == null) { 1.315 + return null; 1.316 + } 1.317 + 1.318 + setupDefaults(uri, values); 1.319 + 1.320 + boolean useTransaction = !db.inTransaction(); 1.321 + try { 1.322 + if (useTransaction) { 1.323 + db.beginTransaction(); 1.324 + } 1.325 + 1.326 + // onPreInsert does a check for the item in the deleted table in some cases 1.327 + // so we put it inside this transaction 1.328 + onPreInsert(values, uri, db); 1.329 + id = db.insert(getTable(uri), null, values); 1.330 + 1.331 + if (useTransaction) { 1.332 + db.setTransactionSuccessful(); 1.333 + } 1.334 + } catch (SQLiteBridgeException ex) { 1.335 + reportError(ex, TelemetryErrorOp.INSERT); 1.336 + throw ex; 1.337 + } finally { 1.338 + if (useTransaction) { 1.339 + db.endTransaction(); 1.340 + } 1.341 + } 1.342 + 1.343 + return ContentUris.withAppendedId(uri, id); 1.344 + } 1.345 + 1.346 + @Override 1.347 + public int bulkInsert(Uri uri, ContentValues[] allValues) { 1.348 + final SQLiteBridge db = getDatabase(uri); 1.349 + // If we can not get a SQLiteBridge instance, its likely that the database 1.350 + // has not been set up and Gecko is not running. We return 0 and expect 1.351 + // callers to try again later 1.352 + if (db == null) { 1.353 + return 0; 1.354 + } 1.355 + 1.356 + int rowsAdded = 0; 1.357 + 1.358 + String table = getTable(uri); 1.359 + 1.360 + try { 1.361 + db.beginTransaction(); 1.362 + for (ContentValues initialValues : allValues) { 1.363 + ContentValues values = new ContentValues(initialValues); 1.364 + setupDefaults(uri, values); 1.365 + onPreInsert(values, uri, db); 1.366 + db.insert(table, null, values); 1.367 + rowsAdded++; 1.368 + } 1.369 + db.setTransactionSuccessful(); 1.370 + } catch (SQLiteBridgeException ex) { 1.371 + reportError(ex, TelemetryErrorOp.BULKINSERT); 1.372 + throw ex; 1.373 + } finally { 1.374 + db.endTransaction(); 1.375 + } 1.376 + 1.377 + if (rowsAdded > 0) { 1.378 + final boolean shouldSyncToNetwork = !isCallerSync(uri); 1.379 + mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); 1.380 + } 1.381 + 1.382 + return rowsAdded; 1.383 + } 1.384 + 1.385 + @Override 1.386 + public int update(Uri uri, ContentValues values, String selection, 1.387 + String[] selectionArgs) { 1.388 + int updated = 0; 1.389 + final SQLiteBridge db = getDatabase(uri); 1.390 + 1.391 + // If we can not get a SQLiteBridge instance, its likely that the database 1.392 + // has not been set up and Gecko is not running. We return null and expect 1.393 + // callers to try again later 1.394 + if (db == null) { 1.395 + return updated; 1.396 + } 1.397 + 1.398 + onPreUpdate(values, uri, db); 1.399 + 1.400 + try { 1.401 + updated = db.update(getTable(uri), values, selection, selectionArgs); 1.402 + } catch (SQLiteBridgeException ex) { 1.403 + reportError(ex, TelemetryErrorOp.UPDATE); 1.404 + throw ex; 1.405 + } 1.406 + 1.407 + return updated; 1.408 + } 1.409 + 1.410 + @Override 1.411 + public Cursor query(Uri uri, String[] projection, String selection, 1.412 + String[] selectionArgs, String sortOrder) { 1.413 + Cursor cursor = null; 1.414 + final SQLiteBridge db = getDatabase(uri); 1.415 + 1.416 + // If we can not get a SQLiteBridge instance, its likely that the database 1.417 + // has not been set up and Gecko is not running. We return null and expect 1.418 + // callers to try again later 1.419 + if (db == null) { 1.420 + return cursor; 1.421 + } 1.422 + 1.423 + sortOrder = getSortOrder(uri, sortOrder); 1.424 + 1.425 + try { 1.426 + cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null); 1.427 + onPostQuery(cursor, uri, db); 1.428 + } catch (SQLiteBridgeException ex) { 1.429 + reportError(ex, TelemetryErrorOp.QUERY); 1.430 + throw ex; 1.431 + } 1.432 + 1.433 + return cursor; 1.434 + } 1.435 + 1.436 + private String getHistogram(SQLiteBridgeException e) { 1.437 + // If you add values here, make sure to update 1.438 + // toolkit/components/telemetry/Histograms.json. 1.439 + if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) { 1.440 + return getTelemetryPrefix() + "_LOCKED"; 1.441 + } 1.442 + return null; 1.443 + } 1.444 + 1.445 + protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) { 1.446 + Log.e(mLogTag, "Error in database " + op.name(), e); 1.447 + final String histogram = getHistogram(e); 1.448 + if (histogram == null) { 1.449 + return; 1.450 + } 1.451 + 1.452 + Telemetry.HistogramAdd(histogram, op.getBucket()); 1.453 + } 1.454 + 1.455 + protected abstract String getDBName(); 1.456 + 1.457 + protected abstract int getDBVersion(); 1.458 + 1.459 + protected abstract String getTable(Uri uri); 1.460 + 1.461 + protected abstract String getSortOrder(Uri uri, String aRequested); 1.462 + 1.463 + protected abstract void setupDefaults(Uri uri, ContentValues values); 1.464 + 1.465 + protected abstract void initGecko(); 1.466 + 1.467 + protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db); 1.468 + 1.469 + protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db); 1.470 + 1.471 + protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db); 1.472 +}