/* 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/. */ #filter substitution package @ANDROID_PACKAGE_NAME@.db; import java.io.File; import java.io.IOException; import java.lang.IllegalArgumentException; import java.util.HashMap; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.Random; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoProfile; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoEventListener; import org.mozilla.gecko.db.BrowserContract.CommonColumns; import org.mozilla.gecko.db.DBUtils; import org.mozilla.gecko.db.BrowserContract.Passwords; import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; import org.mozilla.gecko.db.BrowserContract.SyncColumns; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sqlite.SQLiteBridge; import org.mozilla.gecko.sqlite.SQLiteBridgeException; import org.mozilla.gecko.sync.Utils; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Build; 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 GeckoProvider extends ContentProvider { private String mLogTag = "GeckoPasswordsProvider"; private String mDBName = ""; private int mDBVersion = 0; private HashMap mDatabasePerProfile; protected Context mContext = null; @Override public void shutdown() { if (mDatabasePerProfile == null) return; Collection bridges = mDatabasePerProfile.values(); Iterator it = bridges.iterator(); while (it.hasNext()) { SQLiteBridge bridge = it.next(); if (bridge != null) { try { bridge.close(); } catch (Exception ex) { } } } mDatabasePerProfile = null; } public void finalize() { shutdown(); } protected void setLogTag(String aLogTag) { mLogTag = aLogTag; } protected String getLogTag() { return mLogTag; } protected void setDBName(String aDBName) { mDBName = aDBName; } protected String getDBName() { return mDBName; } protected void setDBVersion(int aVersion) { mDBVersion = aVersion; } protected int getDBVersion() { return mDBVersion; } private SQLiteBridge getDB(Context context, final String databasePath) { SQLiteBridge bridge = null; boolean dbNeedsSetup = true; try { String resourcePath = context.getPackageResourcePath(); GeckoAppShell.loadSQLiteLibs(context, resourcePath); GeckoAppShell.loadNSSLibs(context, resourcePath); bridge = SQLiteBridge.openDatabase(databasePath, null, 0); int version = bridge.getVersion(); dbNeedsSetup = version != mDBVersion; } 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 (!GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) { 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) { Log.i(mLogTag, "Sending init to gecko"); bridge = null; initGecko(); } if (bridge != null) mDatabasePerProfile.put(databasePath, bridge); return bridge; } private SQLiteBridge getDatabaseForProfile(String profile) { if (TextUtils.isEmpty(profile)) { Log.d(mLogTag, "No profile provided, using default"); profile = BrowserContract.DEFAULT_PROFILE; } SQLiteBridge db = null; synchronized (this) { db = mDatabasePerProfile.get(profile); if (db == null) { db = getDB(mContext, getDatabasePathForProfile(profile)); } } Log.d(mLogTag, "Successfully created database helper for profile: " + profile); return db; } private SQLiteBridge getDatabaseForPath(String profilePath) { SQLiteBridge db = null; synchronized (this) { db = mDatabasePerProfile.get(profilePath); if (db == null) { File profileDir = new File(profilePath, mDBName); db = getDB(mContext, profileDir.getPath()); } } Log.d(mLogTag, "Successfully created database helper for path: " + profilePath); return db; } private String getDatabasePathForProfile(String profile) { File profileDir = GeckoProfile.get(mContext, profile).getDir(); if (profileDir == null) { Log.d(mLogTag, "Couldn't find directory for profile: " + profile); return null; } Log.d(mLogTag, "Using path: " + profileDir.getPath()); String databasePath = new File(profileDir, mDBName).getAbsolutePath(); return databasePath; } 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 getDatabaseForPath(profilePath); return getDatabaseForProfile(profile); } @Override public boolean onCreate() { mContext = getContext(); synchronized (this) { mDatabasePerProfile = new HashMap(); } 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) { Log.e(mLogTag, "Error deleting record", ex); 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) { Log.e(mLogTag, "Error inserting in db", ex); 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; long id = -1; 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); id = db.insert(table, null, values); rowsAdded++; } db.setTransactionSuccessful(); } catch (SQLiteBridgeException ex) { Log.e(mLogTag, "Error inserting in db", ex); throw ex; } finally { db.endTransaction(); } 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) { Log.e(mLogTag, "Error updating table", ex); 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) { Log.e(mLogTag, "Error querying database", ex); throw ex; } return cursor; } 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 void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db); public abstract void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db); public abstract void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db); }