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