Bug 725881 - Content provider for form history. r=lucasr
authorWes Johnston <wjohnston@mozilla.com>
Mon, 27 Feb 2012 10:10:14 -0800
changeset 87849 00929ec6099170cb7c22169bf9ea5e7db85630f2
parent 87848 246c77eef7cbb6febbdd3c7660b6143eb0093e99
child 87850 289cd639ff3418020d6adb18153a1b0b4c9de552
push id22160
push usermbrubeck@mozilla.com
push dateTue, 28 Feb 2012 17:21:33 +0000
treeherdermozilla-central@dde4e0089a18 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs725881
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 725881 - Content provider for form history. r=lucasr
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/Makefile.in
mobile/android/base/db/BrowserContract.java.in
mobile/android/base/db/FormHistoryProvider.java.in
mobile/android/base/db/GeckoProvider.java.in
mobile/android/base/db/PasswordsProvider.java.in
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -169,16 +169,23 @@
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
 
         <provider android:name="@ANDROID_PACKAGE_NAME@.db.PasswordsProvider"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"
 									android:protectionLevel="signature"/>
 
+        <provider android:name="@ANDROID_PACKAGE_NAME@.db.FormHistoryProvider"
+                  android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
+                  android:permission="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
+									android:protectionLevel="signature"/>
 
 #include ../sync/manifests/SyncAndroidManifest_services.xml.in
     </application>
 
     <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"
                 android:protectionLevel="signature"/>
 
+    <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
+                android:protectionLevel="signature"/>
+
 </manifest> 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -149,16 +149,18 @@ endif
 FENNEC_PP_JAVA_FILES = \
   App.java \
   LauncherShortcuts.java \
   NotificationHandler.java \
   Restarter.java \
   db/BrowserContract.java \
   db/BrowserProvider.java \
   db/PasswordsProvider.java \
+  db/FormHistoryProvider.java \
+  db/GeckoProvider.java \
   SmsManager.java \
   $(NULL)
 
 
 ifneq (,$(findstring -march=armv7,$(OS_CFLAGS)))
 MIN_CPU_VERSION=7
 else
 MIN_CPU_VERSION=5
--- a/mobile/android/base/db/BrowserContract.java.in
+++ b/mobile/android/base/db/BrowserContract.java.in
@@ -41,26 +41,31 @@ package org.mozilla.gecko.db;
 import android.net.Uri;
 
 public class BrowserContract {
     public static final String AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.browser";
 
     public static final String PASSWORDS_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.passwords";
 
     public static final String DELETED_PASSWORDS_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.deleted-passwords";
+    public static final String FORM_HISTORY_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.formhistory";
+    public static final String DELETED_FORM_HISTORY_AUTHORITY = "@ANDROID_PACKAGE_NAME@.db.deleted-formhistory";
 
     public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
     
     public static final Uri PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + PASSWORDS_AUTHORITY);
 
     public static final Uri DELETED_PASSWORDS_AUTHORITY_URI = Uri.parse("content://" + DELETED_PASSWORDS_AUTHORITY);
+    public static final Uri FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + FORM_HISTORY_AUTHORITY);
+    public static final Uri DELETED_FORM_HISTORY_AUTHORITY_URI = Uri.parse("content://" + DELETED_FORM_HISTORY_AUTHORITY);
 
     public static final String DEFAULT_PROFILE = "default";
 
     public static final String PARAM_PROFILE = "profile";
+    public static final String PARAM_PROFILE_PATH = "profilePath";
 
     public static final String PARAM_LIMIT = "limit";
 
     public static final String PARAM_IS_SYNC = "sync";
 
     public static final String PARAM_SHOW_DELETED = "show_deleted";
 
     public interface CommonColumns {
@@ -84,16 +89,24 @@ public class BrowserContract {
     }
 
     public interface ImageColumns {
         public static final String FAVICON = "favicon";
 
         public static final String THUMBNAIL = "thumbnail";
     }
 
+    public interface DeletedColumns {
+        public static final String ID = "id";
+
+        public static final String GUID = "guid";
+
+        public static final String TIME_DELETED = "timeDeleted";
+    }
+
     public static final class Images implements CommonColumns, ImageColumns, SyncColumns {
         private Images() {}
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images");
 
         public static final String URL = "url_key";
 
         public static final String FAVICON_URL = "favicon_url";
@@ -185,22 +198,44 @@ public class BrowserContract {
 
         public static final String TIME_LAST_USED = "timeLastUsed";
 
         public static final String TIME_PASSWORD_CHANGED = "timePasswordChanged";
 
         public static final String TIMES_USED = "timesUsed";
     }
 
-    public static final class DeletedPasswords implements CommonColumns, SyncColumns {
+    public static final class DeletedPasswords implements DeletedColumns {
         private DeletedPasswords() {}
 
-        public static final Uri CONTENT_URI = Uri.withAppendedPath(DELETED_PASSWORDS_AUTHORITY_URI, "deleted-passwords");
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords";
+    }
 
-        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-passwords";
+    public static final class FormHistory {
+        private FormHistory() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(FORM_HISTORY_AUTHORITY_URI, "formhistory");
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/formhistory";
 
         public static final String ID = "id";
 
-        public static final String GUID = "guid";
+        public static final String FIELD_NAME = "fieldname";
+
+        public static final String VALUE = "value";
+
+        public static final String TIMES_USED = "timesUsed";
+
+        public static final String FIRST_USED = "firstUsed";
+
+        public static final String LAST_USED = "lastUsed";
 
-        public static final String TIME_DELETED = "timeDeleted";
+        public static final String GUID = "guid";
+    }
+
+    public static final class DeletedFormHistory implements DeletedColumns {
+        private DeletedFormHistory() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(DELETED_FORM_HISTORY_AUTHORITY_URI, "deleted-formhistory");
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/deleted-formhistory";
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/FormHistoryProvider.java.in
@@ -0,0 +1,149 @@
+/* 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.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.FormHistory;
+import org.mozilla.gecko.db.BrowserContract.DeletedFormHistory;
+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;
+
+public class FormHistoryProvider extends GeckoProvider {
+    static final String TABLE_FORM_HISTORY = "moz_formhistory";
+    static final String TABLE_DELETED_FORM_HISTORY = "moz_deleted_formhistory";
+
+    private static final int FORM_HISTORY = 100;
+    private static final int DELETED_FORM_HISTORY = 101;
+
+    private static final UriMatcher URI_MATCHER;
+
+    private static HashMap<String, String> FORM_HISTORY_PROJECTION_MAP;
+    private static HashMap<String, String> DELETED_FORM_HISTORY_PROJECTION_MAP;
+
+    // This should be kept in sync with the db version in toolkit/components/satchel/nsFormHistory.js
+    private static int DB_VERSION = 4;
+    private static String DB_FILENAME = "formhistory.sqlite";
+
+    static {
+        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+        URI_MATCHER.addURI(BrowserContract.FORM_HISTORY_AUTHORITY, "formhistory", FORM_HISTORY);
+        URI_MATCHER.addURI(BrowserContract.DELETED_FORM_HISTORY_AUTHORITY, "deleted-formhistory", DELETED_FORM_HISTORY);
+        FORM_HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+        DELETED_FORM_HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+    }
+
+    @Override
+    public boolean onCreate() {
+        setLogTag("FormHistoryProvider");
+        setDBName(DB_FILENAME);
+        setDBVersion(DB_VERSION);
+
+        return super.onCreate();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        final int match = URI_MATCHER.match(uri);
+
+        switch (match) {
+            case FORM_HISTORY:
+                return FormHistory.CONTENT_TYPE;
+
+            case DELETED_FORM_HISTORY:
+                return DeletedFormHistory.CONTENT_TYPE;
+
+            default:
+                throw new UnsupportedOperationException("Unknown type " + uri);
+        }
+    }
+
+    public String getTable(Uri uri) {
+        String table = null;
+        final int match = URI_MATCHER.match(uri);
+        switch (match) {
+            case DELETED_FORM_HISTORY:
+                table = TABLE_DELETED_FORM_HISTORY;
+                break;
+
+            case FORM_HISTORY:
+                table = TABLE_FORM_HISTORY;
+                break;
+
+            default:
+                throw new UnsupportedOperationException("Unknown table " + uri);
+        }
+        return table;
+    }
+
+    @Override
+    public String getSortOrder(Uri uri, String aRequested) {
+        if (!TextUtils.isEmpty(aRequested))
+            return aRequested;
+
+        return null;
+    }
+
+    public void setupDefaults(Uri uri, ContentValues values) {
+        int match = URI_MATCHER.match(uri);
+        long now = System.currentTimeMillis();
+
+        switch (match) {
+            case DELETED_FORM_HISTORY:
+                values.put(DeletedFormHistory.TIME_DELETED, now);
+
+                // Deleted entries must contain a guid
+                if (!values.containsKey(FormHistory.GUID)) {
+                    throw new IllegalArgumentException("Must provide a GUID for a deleted form history");
+                }
+                break;
+
+            case FORM_HISTORY:
+                // Generate GUID for new entry. Don't override specified GUIDs.
+                if (!values.containsKey(FormHistory.GUID)) {
+                    String guid = Utils.generateGuid();
+                    values.put(FormHistory.GUID, guid);
+                }
+                break;
+
+            default:
+                throw new UnsupportedOperationException("Unknown insert URI " + uri);
+        }
+    }
+
+    public void initGecko() {
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormHistory:Init", null));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/GeckoProvider.java.in
@@ -0,0 +1,282 @@
+/* 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.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<String, SQLiteBridge> mDatabasePerProfile;
+    protected Context mContext = null;
+
+    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 {
+            bridge = new SQLiteBridge(databasePath);
+            int version = bridge.getVersion();
+            Log.i(mLogTag, version + " == " + mDBVersion);
+            dbNeedsSetup = version != mDBVersion;
+        } catch(SQLiteBridgeException ex) {
+            // 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();
+        }
+        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<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) {
+            Log.e(mLogTag, "Error deleting record", 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);
+
+        try {
+            id = db.insert(getTable(uri), null, values);
+        } catch(SQLiteBridgeException ex) {
+            Log.e(mLogTag, "Error inserting in db", ex);
+        }
+
+        return ContentUris.withAppendedId(uri, id);
+    }
+
+    @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;
+
+        try {
+            updated = db.update(getTable(uri), values, selection, selectionArgs);
+        } catch(SQLiteBridgeException ex) {
+            Log.e(mLogTag, "Error updating table", 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);
+        } catch (SQLiteBridgeException ex) {
+            Log.e(mLogTag, "Error querying database", 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();
+}
--- a/mobile/android/base/db/PasswordsProvider.java.in
+++ b/mobile/android/base/db/PasswordsProvider.java.in
@@ -36,41 +36,34 @@ 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;
 
-public class PasswordsProvider extends ContentProvider {
-    private Context mContext = null;
-    private static final String LOGTAG = "GeckoPasswordsProvider";
-
-    static final String DATABASE_NAME = "signons.sqlite";
+public class PasswordsProvider extends GeckoProvider {
+    static final String TABLE_PASSWORDS = "moz_logins";
+    static final String TABLE_DELETED_PASSWORDS = "moz_deleted_logins";
 
-    static final int DATABASE_VERSION = 5;
-    
-    static final String TABLE_PASSWORDS = "moz_logins";
-    static final String TABLE_DELETED_PASSWORDS = "deleted_logins";
-
-    private static final int PASSWORDS = 500;
-    private static final int DELETED_PASSWORDS = 502;
+    private static final int PASSWORDS = 100;
+    private static final int DELETED_PASSWORDS = 101;
 
     static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC";
     static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC";
 
     private static final UriMatcher URI_MATCHER;
 
     private static HashMap<String, String> PASSWORDS_PROJECTION_MAP;
     private static HashMap<String, String> DELETED_PASSWORDS_PROJECTION_MAP;
 
-    private HashMap<String, SQLiteBridge> mDatabasePerProfile;
-
-    private static ArrayList<String> mSyncColumns;
+    // this should be kept in sync with the version in toolkit/components/passwordmgr/storage-mozStorage.js
+    private static final int DB_VERSION = 5;
+    private static final String DB_FILENAME = "signons.sqlite";
 
     static {
         URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
 
         // content://org.mozilla.gecko.providers.browser/passwords/#
         URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS);
 
         PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
@@ -84,187 +77,92 @@ public class PasswordsProvider extends C
         PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD);
         PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID);
         PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE);
         PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED);
         PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED);
         PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED);
         PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED);
 
-        URI_MATCHER.addURI(BrowserContract.DELETED_PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS);
-
-        mSyncColumns = new ArrayList<String>();
-        mSyncColumns.add(Passwords._ID);
-        mSyncColumns.add(Passwords.DATE_CREATED);
-        mSyncColumns.add(Passwords.DATE_MODIFIED);
-
         DELETED_PASSWORDS_PROJECTION_MAP = new HashMap<String, String>();
         DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID);
         DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID);
         DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED);
     }
 
-    private SQLiteBridge getDB(Context context, final String databasePath) {
-        SQLiteBridge mBridge = null;
-        try {
-            mBridge = new SQLiteBridge(databasePath);
-
-            boolean dbNeedsSetup = true;
-            try {
-                int version = mBridge.getVersion();
-                Log.i(LOGTAG, version + " == " + DATABASE_VERSION);
-                dbNeedsSetup = version != DATABASE_VERSION;
-            } catch(Exception ex) {
-                Log.e(LOGTAG, "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)) {
-                    mBridge = null;
-                    throw new UnsupportedOperationException("Need to launch Gecko to set password database up");
-                }
-            }
-            if (dbNeedsSetup) {
-                Log.i(LOGTAG, "Sending init to gecko");
-                mBridge = null;
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Passwords:Init", databasePath));
-            }
-        } catch(SQLiteBridgeException ex) {
-            Log.e(LOGTAG, "Error getting database", ex);
-        }
-        return mBridge;
-    }
-
-    private SQLiteBridge getDatabaseForProfile(String profile) {
-        // Each profile has a separate signons.sqlite database. The target
-        // profile is provided using a URI query argument in each request
-        // to our content provider.
-
-        if (TextUtils.isEmpty(profile)) {
-            Log.d(LOGTAG, "No profile provided, using default");
-            profile = BrowserContract.DEFAULT_PROFILE;
-        }
-
-        SQLiteBridge db = mDatabasePerProfile.get(profile);
-
-        if (db == null) {
-            synchronized (this) {
-                try {
-                    db = getDB(getContext(), getDatabasePath(profile));
-                } catch(UnsupportedOperationException ex) {
-                    Log.i(LOGTAG, "Gecko has not set the database up yet");
-                    return db;
-                }
-                mDatabasePerProfile.put(profile, db);
-            }
-        }
-
-        Log.d(LOGTAG, "Successfully created database helper for profile: " + profile);
-
-        return db;
-    }
-
-    private String getDatabasePath(String profile) {
-        File profileDir = GeckoProfile.get(mContext, profile).getDir();
-        if (profileDir == null) {
-            Log.d(LOGTAG, "Couldn't find directory for profile: " + profile);
-            return null;
-        }
-
-        String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath();
-        return databasePath;
-    }
-
-    private SQLiteBridge getDatabase(Uri uri) {
-        String profile = null;
-
-        if (uri != null)
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-
-        return getDatabaseForProfile(profile);
-    }
-
     @Override
     public boolean onCreate() {
-        mContext = getContext();
-        mDatabasePerProfile = new HashMap<String, SQLiteBridge>();
-
-        return true;
+        setLogTag("GeckoPasswordsProvider");
+        setDBName(DB_FILENAME);
+        setDBVersion(DB_VERSION);
+        return super.onCreate();
     }
 
     @Override
     public String getType(Uri uri) {
         final int match = URI_MATCHER.match(uri);
 
         switch (match) {
             case PASSWORDS:
                 return Passwords.CONTENT_TYPE;
+
             case DELETED_PASSWORDS:
                 return DeletedPasswords.CONTENT_TYPE;
+
+            default:
+                throw new UnsupportedOperationException("Unknown type " + uri);
         }
-
-        Log.d(LOGTAG, "URI has unrecognized type: " + uri);
-        return null;
     }
 
-    private String getTable(Uri uri) {
+    public String getTable(Uri uri) {
         final int match = URI_MATCHER.match(uri);
         switch (match) {
             case DELETED_PASSWORDS:
                 return TABLE_DELETED_PASSWORDS;
-            case PASSWORDS: {
+
+            case PASSWORDS:
                 return TABLE_PASSWORDS;
-            }
+
             default:
                 throw new UnsupportedOperationException("Unknown table " + uri);
         }
     }
 
-    private String getSortOrder(Uri uri, String aRequested) {
+    public String getSortOrder(Uri uri, String aRequested) {
         if (!TextUtils.isEmpty(aRequested))
             return aRequested;
 
         final int match = URI_MATCHER.match(uri);
         switch (match) {
             case DELETED_PASSWORDS:
                 return DEFAULT_DELETED_PASSWORDS_SORT_ORDER;
-            case PASSWORDS: {
+
+            case PASSWORDS:
                 return DEFAULT_PASSWORDS_SORT_ORDER;
-            }
+
             default:
-                throw new UnsupportedOperationException("Unknown delete URI " + uri);
+                throw new UnsupportedOperationException("Unknown URI " + uri);
         }
     }
 
-    private Uri getAuthUri(Uri uri) {
-        final int match = URI_MATCHER.match(uri);
-        switch (match) {
-            case DELETED_PASSWORDS:
-                return BrowserContract.DELETED_PASSWORDS_AUTHORITY_URI;
-            case PASSWORDS: {
-                return BrowserContract.PASSWORDS_AUTHORITY_URI;
-            }
-            default:
-                throw new UnsupportedOperationException("Unknown delete URI " + uri);
-        }
-    }
-
-    private void setupDefaults(Uri uri, ContentValues values)
+    public void setupDefaults(Uri uri, ContentValues values)
             throws IllegalArgumentException {
         int match = URI_MATCHER.match(uri);
         long now = System.currentTimeMillis();
         switch (match) {
             case DELETED_PASSWORDS:
                 values.put(DeletedPasswords.TIME_DELETED, now);
 
                 // Deleted passwords must contain a guid
                 if (!values.containsKey(Passwords.GUID)) {
                     throw new IllegalArgumentException("Must provide a GUID for a deleted password");
                 }
                 break;
-            case PASSWORDS: {
+
+            case PASSWORDS:
                 values.put(Passwords.TIME_CREATED, now);
 
                 // Generate GUID for new password. Don't override specified GUIDs.
                 if (!values.containsKey(Passwords.GUID)) {
                     String guid = Utils.generateGuid();
                     values.put(Passwords.GUID, guid);
                 }
                 String nowString = new Long(now).toString();
@@ -278,129 +176,19 @@ public class PasswordsProvider extends C
                 DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, "");
                 DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, "");
                 DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, "");
                 DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0");
                 DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString);
                 DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString);
                 DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0");
                 break;
-            }
 
             default:
-                throw new UnsupportedOperationException("Unknown insert URI " + uri);
+                throw new UnsupportedOperationException("Unknown URI " + uri);
         }
     }
 
     @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        int deleted = 0;
-        final SQLiteBridge db = getDatabase(uri);
-        if (db == null)
-            return deleted;
-
-        String table = getTable(uri);
-        if (table.equals(""))
-            return deleted;
-
-        if (selection != null) {
-            for (String item : mSyncColumns)
-                selection = selection.replace(item, translateColumn(item));
-        }
-
-        try {
-            deleted = db.delete(table, selection, selectionArgs);
-        } catch (SQLiteBridgeException ex) {
-            Log.e(LOGTAG, "Error deleting record", ex);
-        }
-
-        return deleted;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        long id = -1;
-        final SQLiteBridge db = getDatabase(uri);
-        if (db == null)
-            return null;
-
-        String table = getTable(uri);
-        if (table.equals(""))
-            return null;
-
-        try {
-            setupDefaults(uri, values);
-        } catch(Exception ex) {
-            Log.e(LOGTAG, "Error setting up defaults", ex);
-            return null;
-        }
-
-        try {
-            id = db.insert(table, "", values);
-        } catch(SQLiteBridgeException ex) {
-            Log.e(LOGTAG, "Error inserting in db", ex);
-        }
-
-        return ContentUris.withAppendedId(uri, id);
-    }
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        int updated = 0;
-        final SQLiteBridge db = getDatabase(uri);
-        if (db == null)
-            return updated;
-
-        String table = getTable(uri);
-        if (TextUtils.isEmpty(table))
-            return updated;
-
-        if (selection != null) {
-            for (String item : mSyncColumns)
-                selection = selection.replace(item, translateColumn(item));
-        }
-
-        try {
-            return db.update(table, values, selection, selectionArgs);
-        } catch(SQLiteBridgeException ex) {
-            Log.e(LOGTAG, "Error updating table", ex);
-        }
-        return 0;
-    }
-
-    @Override
-    public Cursor query(Uri uri, String[] projection, String selection,
-            String[] selectionArgs, String sortOrder) {
-        Cursor cursor = null;
-        final SQLiteBridge db = getDatabase(uri);
-        if (db == null)
-            return cursor;
-
-        String table = getTable(uri);
-        if (table.equals(""))
-            return cursor;
-
-        sortOrder = getSortOrder(uri, sortOrder);
-        if (TextUtils.isEmpty(sortOrder))
-            return cursor;
-
-        try {
-            cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
-            cursor.setNotificationUri(getContext().getContentResolver(), getAuthUri(uri));
-        } catch (SQLiteBridgeException ex) {
-            Log.e(LOGTAG, "Error querying database", ex);
-        }
-
-        return cursor;
-    }
-
-    private String translateColumn(String column) {
-        if (column.equals(SyncColumns.DATE_CREATED))
-            return Passwords.TIME_CREATED;
-        else if (column.equals(SyncColumns.DATE_MODIFIED))
-            return Passwords.TIME_PASSWORD_CHANGED;
-        else if (column.equals(CommonColumns._ID))
-            return Passwords.ID;
-        else
-            return column;
+    public void initGecko() {
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Passwords:Init", null));
     }
 }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -218,16 +218,17 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Preferences:Set", false);
     Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
     Services.obs.addObserver(this, "Sanitize:ClearAll", false);
     Services.obs.addObserver(this, "PanZoom:PanZoom", false);
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Viewport:Change", false);
     Services.obs.addObserver(this, "SearchEngines:Get", false);
     Services.obs.addObserver(this, "Passwords:Init", false);
+    Services.obs.addObserver(this, "FormHistory:Init", false);
 
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
@@ -958,16 +959,23 @@ var BrowserApp = {
     } else if (aTopic == "SearchEngines:Get") {
       this.getSearchEngines();
     } else if (aTopic == "Passwords:Init") {
       var storage = Components.classes["@mozilla.org/login-manager/storage/mozStorage;1"].  
         getService(Components.interfaces.nsILoginManagerStorage);
       storage.init();
 
       sendMessageToJava({gecko: { type: "Passwords:Init:Return" }});
+      Services.obs.removeObserver(this, "Passwords:Init", false);
+    } else if (aTopic == "FormHistory:Init") {
+      var fh = Components.classes["@mozilla.org/satchel/form-history;1"].  
+        getService(Components.interfaces.nsIFormHistory2);
+      var db = fh.DBConnection;
+      sendMessageToJava({gecko: { type: "FormHistory:Init:Return" }});
+      Services.obs.removeObserver(this, "FormHistory:Init", false);
     } else if (aTopic == "sessionstore-state-purge-complete") {
       sendMessageToJava({ gecko: { type: "Session:StatePurged" }});
     }
   },
 
   get defaultBrowserWidth() {
     delete this.defaultBrowserWidth;
     let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");