diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java | 471 |
1 files changed, 471 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java b/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java new file mode 100644 index 000000000..d48604f03 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/db/SQLiteBridgeContentProvider.java @@ -0,0 +1,471 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.db; + +import java.io.File; +import java.util.HashMap; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.GeckoProfile; +import org.mozilla.gecko.GeckoThread; +import org.mozilla.gecko.Telemetry; +import org.mozilla.gecko.mozglue.GeckoLoader; +import org.mozilla.gecko.sqlite.SQLiteBridge; +import org.mozilla.gecko.sqlite.SQLiteBridgeException; + +import android.content.ContentProvider; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; + +/* + * Provides a basic ContentProvider that sets up and sends queries through + * SQLiteBridge. Content providers should extend this by setting the appropriate + * table and version numbers in onCreate, and implementing the abstract methods: + * + * public abstract String getTable(Uri uri); + * public abstract String getSortOrder(Uri uri, String aRequested); + * public abstract void setupDefaults(Uri uri, ContentValues values); + * public abstract void initGecko(); + */ + +public abstract class SQLiteBridgeContentProvider extends ContentProvider { + private static final String ERROR_MESSAGE_DATABASE_IS_LOCKED = "Can't step statement: (5) database is locked"; + + private HashMap<String, SQLiteBridge> mDatabasePerProfile; + protected Context mContext; + private final String mLogTag; + + protected SQLiteBridgeContentProvider(String logTag) { + mLogTag = logTag; + } + + /** + * Subclasses must override this to allow error reporting code to compose + * the correct histogram name. + * + * Ensure that you define the new histograms if you define a new class! + */ + protected abstract String getTelemetryPrefix(); + + /** + * Errors are recorded in telemetry using an enumerated histogram. + * + * <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/ + * Adding_a_new_Telemetry_probe#Choosing_a_Histogram_Type> + * + * These are the allowable enumeration values. Keep these in sync with the + * histogram definition! + * + */ + private static enum TelemetryErrorOp { + BULKINSERT (0), + DELETE (1), + INSERT (2), + QUERY (3), + UPDATE (4); + + private final int bucket; + + TelemetryErrorOp(final int bucket) { + this.bucket = bucket; + } + + public int getBucket() { + return bucket; + } + } + + @Override + public void shutdown() { + if (mDatabasePerProfile == null) { + return; + } + + synchronized (this) { + for (SQLiteBridge bridge : mDatabasePerProfile.values()) { + if (bridge != null) { + try { + bridge.close(); + } catch (Exception ex) { } + } + } + mDatabasePerProfile = null; + } + super.shutdown(); + } + + @Override + public void finalize() { + shutdown(); + } + + /** + * Return true of the query is from Firefox Sync. + * @param uri query URI + */ + public static boolean isCallerSync(Uri uri) { + String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC); + return !TextUtils.isEmpty(isSync); + } + + private SQLiteBridge getDB(Context context, final String databasePath) { + SQLiteBridge bridge = null; + + boolean dbNeedsSetup = true; + try { + String resourcePath = context.getPackageResourcePath(); + GeckoLoader.loadSQLiteLibs(context, resourcePath); + GeckoLoader.loadNSSLibs(context, resourcePath); + bridge = SQLiteBridge.openDatabase(databasePath, null, 0); + int version = bridge.getVersion(); + dbNeedsSetup = version != getDBVersion(); + } catch (SQLiteBridgeException ex) { + // close the database + if (bridge != null) { + bridge.close(); + } + + // this will throw if the database can't be found + // we should attempt to set it up if Gecko is running + dbNeedsSetup = true; + Log.e(mLogTag, "Error getting version ", ex); + + // if Gecko is not running, we should bail out. Otherwise we try to + // let Gecko build the database for us + if (!GeckoThread.isRunning()) { + Log.e(mLogTag, "Can not set up database. Gecko is not running"); + return null; + } + } + + // If the database is not set up yet, or is the wrong schema version, we send an initialize + // call to Gecko. Gecko will handle building the database file correctly, as well as any + // migrations that are necessary + if (dbNeedsSetup) { + bridge = null; + initGecko(); + } + return bridge; + } + + /** + * Returns the absolute path of a database file depending on the specified profile and dbName. + * @param profile + * the profile whose dbPath must be returned + * @param dbName + * the name of the db file whose absolute path must be returned + * @return the absolute path of the db file or <code>null</code> if it was not possible to retrieve a valid path + * + */ + private String getDatabasePathForProfile(String profile, String dbName) { + // Depends on the vagaries of GeckoProfile.get, so null check for safety. + File profileDir = GeckoProfile.get(mContext, profile).getDir(); + if (profileDir == null) { + return null; + } + + String databasePath = new File(profileDir, dbName).getAbsolutePath(); + return databasePath; + } + + /** + * Returns a SQLiteBridge object according to the specified profile id and to the name of db related to the + * current provider instance. + * @param profile + * the id of the profile to be used to retrieve the related SQLiteBridge + * @return the <code>SQLiteBridge</code> related to the specified profile id or <code>null</code> if it was + * not possible to retrieve a valid SQLiteBridge + */ + private SQLiteBridge getDatabaseForProfile(String profile) { + if (profile == null) { + profile = GeckoProfile.get(mContext).getName(); + Log.d(mLogTag, "No profile provided, using '" + profile + "'"); + } + + final String dbName = getDBName(); + String mapKey = profile + "/" + dbName; + + SQLiteBridge db = null; + synchronized (this) { + db = mDatabasePerProfile.get(mapKey); + if (db != null) { + return db; + } + final String dbPath = getDatabasePathForProfile(profile, dbName); + if (dbPath == null) { + Log.e(mLogTag, "Failed to get a valid db path for profile '" + profile + "'' dbName '" + dbName + "'"); + return null; + } + db = getDB(mContext, dbPath); + if (db != null) { + mDatabasePerProfile.put(mapKey, db); + } + } + return db; + } + + /** + * Returns a SQLiteBridge object according to the specified profile path and to the name of db related to the + * current provider instance. + * @param profilePath + * the profilePath to be used to retrieve the related SQLiteBridge + * @return the <code>SQLiteBridge</code> related to the specified profile path or <code>null</code> if it was + * not possible to retrieve a valid <code>SQLiteBridge</code> + */ + private SQLiteBridge getDatabaseForProfilePath(String profilePath) { + File profileDir = new File(profilePath, getDBName()); + final String dbPath = profileDir.getPath(); + return getDatabaseForDBPath(dbPath); + } + + /** + * Returns a SQLiteBridge object according to the specified file path. + * @param dbPath + * the path of the file to be used to retrieve the related SQLiteBridge + * @return the <code>SQLiteBridge</code> related to the specified file path or <code>null</code> if it was + * not possible to retrieve a valid <code>SQLiteBridge</code> + * + */ + private SQLiteBridge getDatabaseForDBPath(String dbPath) { + SQLiteBridge db = null; + synchronized (this) { + db = mDatabasePerProfile.get(dbPath); + if (db != null) { + return db; + } + db = getDB(mContext, dbPath); + if (db != null) { + mDatabasePerProfile.put(dbPath, db); + } + } + return db; + } + + /** + * Returns a SQLiteBridge object to be used to perform operations on the given <code>Uri</code>. + * @param uri + * the <code>Uri</code> to be used to retrieve the related SQLiteBridge + * @return a <code>SQLiteBridge</code> object to be used on the given uri or <code>null</code> if it was + * not possible to retrieve a valid <code>SQLiteBridge</code> + * + */ + private SQLiteBridge getDatabase(Uri uri) { + String profile = null; + String profilePath = null; + + profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); + profilePath = uri.getQueryParameter(BrowserContract.PARAM_PROFILE_PATH); + + // Testing will specify the absolute profile path + if (profilePath != null) { + return getDatabaseForProfilePath(profilePath); + } + return getDatabaseForProfile(profile); + } + + @Override + public boolean onCreate() { + mContext = getContext(); + synchronized (this) { + mDatabasePerProfile = new HashMap<String, SQLiteBridge>(); + } + return true; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + int deleted = 0; + final SQLiteBridge db = getDatabase(uri); + if (db == null) { + return deleted; + } + + try { + deleted = db.delete(getTable(uri), selection, selectionArgs); + } catch (SQLiteBridgeException ex) { + reportError(ex, TelemetryErrorOp.DELETE); + throw ex; + } + + return deleted; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + long id = -1; + final SQLiteBridge db = getDatabase(uri); + + // If we can not get a SQLiteBridge instance, its likely that the database + // has not been set up and Gecko is not running. We return null and expect + // callers to try again later + if (db == null) { + return null; + } + + setupDefaults(uri, values); + + boolean useTransaction = !db.inTransaction(); + try { + if (useTransaction) { + db.beginTransaction(); + } + + // onPreInsert does a check for the item in the deleted table in some cases + // so we put it inside this transaction + onPreInsert(values, uri, db); + id = db.insert(getTable(uri), null, values); + + if (useTransaction) { + db.setTransactionSuccessful(); + } + } catch (SQLiteBridgeException ex) { + reportError(ex, TelemetryErrorOp.INSERT); + throw ex; + } finally { + if (useTransaction) { + db.endTransaction(); + } + } + + return ContentUris.withAppendedId(uri, id); + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] allValues) { + final SQLiteBridge db = getDatabase(uri); + // If we can not get a SQLiteBridge instance, its likely that the database + // has not been set up and Gecko is not running. We return 0 and expect + // callers to try again later + if (db == null) { + return 0; + } + + int rowsAdded = 0; + + String table = getTable(uri); + + try { + db.beginTransaction(); + for (ContentValues initialValues : allValues) { + ContentValues values = new ContentValues(initialValues); + setupDefaults(uri, values); + onPreInsert(values, uri, db); + db.insert(table, null, values); + rowsAdded++; + } + db.setTransactionSuccessful(); + } catch (SQLiteBridgeException ex) { + reportError(ex, TelemetryErrorOp.BULKINSERT); + throw ex; + } finally { + db.endTransaction(); + } + + if (rowsAdded > 0) { + final boolean shouldSyncToNetwork = !isCallerSync(uri); + mContext.getContentResolver().notifyChange(uri, null, shouldSyncToNetwork); + } + + return rowsAdded; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + int updated = 0; + final SQLiteBridge db = getDatabase(uri); + + // If we can not get a SQLiteBridge instance, its likely that the database + // has not been set up and Gecko is not running. We return null and expect + // callers to try again later + if (db == null) { + return updated; + } + + onPreUpdate(values, uri, db); + + try { + updated = db.update(getTable(uri), values, selection, selectionArgs); + } catch (SQLiteBridgeException ex) { + reportError(ex, TelemetryErrorOp.UPDATE); + throw ex; + } + + return updated; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + Cursor cursor = null; + final SQLiteBridge db = getDatabase(uri); + + // If we can not get a SQLiteBridge instance, its likely that the database + // has not been set up and Gecko is not running. We return null and expect + // callers to try again later + if (db == null) { + return cursor; + } + + sortOrder = getSortOrder(uri, sortOrder); + + try { + cursor = db.query(getTable(uri), projection, selection, selectionArgs, null, null, sortOrder, null); + onPostQuery(cursor, uri, db); + } catch (SQLiteBridgeException ex) { + reportError(ex, TelemetryErrorOp.QUERY); + throw ex; + } + + return cursor; + } + + private String getHistogram(SQLiteBridgeException e) { + // If you add values here, make sure to update + // toolkit/components/telemetry/Histograms.json. + if (ERROR_MESSAGE_DATABASE_IS_LOCKED.equals(e.getMessage())) { + return getTelemetryPrefix() + "_LOCKED"; + } + return null; + } + + protected void reportError(SQLiteBridgeException e, TelemetryErrorOp op) { + Log.e(mLogTag, "Error in database " + op.name(), e); + final String histogram = getHistogram(e); + if (histogram == null) { + return; + } + + Telemetry.addToHistogram(histogram, op.getBucket()); + } + + protected abstract String getDBName(); + + protected abstract int getDBVersion(); + + protected abstract String getTable(Uri uri); + + protected abstract String getSortOrder(Uri uri, String aRequested); + + protected abstract void setupDefaults(Uri uri, ContentValues values); + + protected abstract void initGecko(); + + protected abstract void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db); + + protected abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db); + + protected abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db); +} |