mobile/android/base/db/SQLiteBridgeContentProvider.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

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

mercurial