Bug 704682 - Add passwords content provider. r=blassey,gpascutto
authorWes Johnston <wjohnston@mozilla.com>
Fri, 16 Dec 2011 15:11:09 -0800
changeset 86535 85346ae0ea9942f1ac9cff829f2f588fd8deff19
parent 86534 8fa903fa888740143d0537fb51abca5061c6e01e
child 86536 aca408a1d17b2f35dddf6d4ab25398b78d749d70
push idunknown
push userunknown
push dateunknown
reviewersblassey, gpascutto
bugs704682
milestone13.0a1
Bug 704682 - Add passwords content provider. r=blassey,gpascutto
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/Makefile.in
mobile/android/base/db/BrowserContract.java.in
mobile/android/base/db/BrowserProvider.java.in
mobile/android/base/db/DBUtils.java
mobile/android/base/db/PasswordsProvider.java.in
mobile/android/base/sqlite/SQLiteBridge.java
mobile/android/chrome/content/browser.js
mozglue/android/SQLiteBridge.cpp
mozglue/android/SQLiteBridge.h
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -158,15 +158,21 @@
                   android:theme="@style/Gecko.TitleBar"
                   android:label="@string/settings_title"
                   android:excludeFromRecents="true"/>
 
         <provider android:name="@ANDROID_PACKAGE_NAME@.db.BrowserProvider"
                   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"/>
+
+
 #include ../sync/manifests/SyncAndroidManifest_services.xml.in
     </application>
 
     <permission android:name="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"
                 android:protectionLevel="signature"/>
 
 </manifest> 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -67,16 +67,17 @@ FENNEC_JAVA_FILES = \
   AwesomeBar.java \
   AwesomeBarTabs.java \
   BrowserToolbar.java \
   ConfirmPreference.java \
   SyncPreference.java \
   db/AndroidBrowserDB.java \
   db/BrowserDB.java \
   db/LocalBrowserDB.java \
+  db/DBUtils.java \
   DoorHanger.java \
   DoorHangerPopup.java \
   Favicons.java \
   FloatUtils.java \
   GeckoActionBar.java \
   GeckoApp.java \
   GeckoAppShell.java \
   GeckoAsyncTask.java \
@@ -144,16 +145,17 @@ endif
 
 FENNEC_PP_JAVA_FILES = \
   App.java \
   LauncherShortcuts.java \
   NotificationHandler.java \
   Restarter.java \
   db/BrowserContract.java \
   db/BrowserProvider.java \
+  db/PasswordsProvider.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
@@ -38,17 +38,25 @@
 #filter substitution
 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 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 String DEFAULT_PROFILE = "default";
 
     public static final String PARAM_PROFILE = "profile";
 
     public static final String PARAM_LIMIT = "limit";
 
     public static final String PARAM_IS_SYNC = "sync";
@@ -136,9 +144,57 @@ public class BrowserContract {
 
     public static final class Schema {
         private Schema() {}
 
         public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "schema");
 
         public static final String VERSION = "version";
     }
+
+    public static final class Passwords implements CommonColumns, SyncColumns {
+        private Passwords() {}
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(PASSWORDS_AUTHORITY_URI, "passwords");
+
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/passwords";
+
+        public static final String ID = "id";
+
+        public static final String HOSTNAME = "hostname";
+
+        public static final String HTTP_REALM = "httpRealm";
+
+        public static final String FORM_SUBMIT_URL = "formSubmitURL";
+
+        public static final String USERNAME_FIELD = "usernameField";
+
+        public static final String PASSWORD_FIELD = "passwordField";
+
+        public static final String ENCRYPTED_USERNAME = "encryptedUsername";
+
+        public static final String ENCRYPTED_PASSWORD = "encryptedPassword";
+
+        public static final String ENC_TYPE = "encType";
+
+        public static final String TIME_CREATED = "timeCreated";
+
+        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 {
+        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 ID = "id";
+
+        public static final String GUID = "guid";
+
+        public static final String TIME_DELETED = "timeDeleted";
+    }
 }
--- a/mobile/android/base/db/BrowserProvider.java.in
+++ b/mobile/android/base/db/BrowserProvider.java.in
@@ -51,16 +51,18 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Images;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.DBUtils;
+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.MatrixCursor;
@@ -203,62 +205,16 @@ public class BrowserProvider extends Con
     }
 
     private HashMap<String, DatabaseHelper> mDatabasePerProfile;
 
     static final String qualifyColumn(String table, String column) {
         return table + "." + column;
     }
 
-    public static String generateGuid() {
-        byte[] encodedBytes = GeckoAppShell.encodeBase64(generateRandomBytes(9), GeckoAppShell.BASE64_URL_SAFE);
-        return new String(encodedBytes);
-    }
-
-    private static byte[] generateRandomBytes(int length) {
-        byte[] bytes = new byte[length];
-
-        Random random = new Random(System.nanoTime());
-        random.nextBytes(bytes);
-
-        return bytes;
-    }
-
-    // This is available in Android >= 11. Implemented locally to be
-    // compatible with older versions.
-    public static String concatenateWhere(String a, String b) {
-        if (TextUtils.isEmpty(a)) {
-            return b;
-        }
-
-        if (TextUtils.isEmpty(b)) {
-            return a;
-        }
-
-        return "(" + a + ") AND (" + b + ")";
-    }
-
-    // This is available in Android >= 11. Implemented locally to be
-    // compatible with older versions.
-    public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
-        if (originalValues == null || originalValues.length == 0) {
-            return newValues;
-        }
-
-        if (newValues == null || newValues.length == 0) {
-            return originalValues;
-        }
-
-        String[] result = new String[originalValues.length + newValues.length];
-        System.arraycopy(originalValues, 0, result, 0, originalValues.length);
-        System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
-
-        return result;
-    }
-
     private static boolean hasImagesInProjection(String[] projection) {
         if (projection == null) return true;
         for (int i = 0; i < projection.length; ++i) {
             if (projection[i].equals(Images.FAVICON) ||
                 projection[i].equals(Images.THUMBNAIL))
                 return true;
         }
 
@@ -614,46 +570,46 @@ public class BrowserProvider extends Con
 
         final int match = URI_MATCHER.match(uri);
         int deleted = 0;
 
         switch (match) {
             case BOOKMARKS_ID:
                 Log.d(LOGTAG, "Delete on BOOKMARKS_ID: " + uri);
 
-                selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
-                selectionArgs = appendSelectionArgs(selectionArgs,
+                selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case BOOKMARKS: {
                 Log.d(LOGTAG, "Deleting bookmarks: " + uri);
                 deleted = deleteBookmarks(uri, selection, selectionArgs);
                 deleteUnusedImages(uri);
                 break;
             }
 
             case HISTORY_ID:
                 Log.d(LOGTAG, "Delete on HISTORY_ID: " + uri);
 
-                selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
-                selectionArgs = appendSelectionArgs(selectionArgs,
+                selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case HISTORY: {
                 Log.d(LOGTAG, "Deleting history: " + uri);
                 deleted = deleteHistory(uri, selection, selectionArgs);
                 deleteUnusedImages(uri);
                 break;
             }
 
             case IMAGES_ID:
                 Log.d(LOGTAG, "Delete on IMAGES_ID: " + uri);
 
-                selection = concatenateWhere(selection, TABLE_IMAGES + "._id = ?");
-                selectionArgs = appendSelectionArgs(selectionArgs,
+                selection = DBUtils.concatenateWhere(selection, TABLE_IMAGES + "._id = ?");
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case IMAGES: {
                 Log.d(LOGTAG, "Deleting images: " + uri);
                 deleted = deleteImages(uri, selection, selectionArgs);
                 break;
             }
 
@@ -702,17 +658,17 @@ public class BrowserProvider extends Con
                 Log.d(LOGTAG, "Insert on BOOKMARKS: " + uri);
 
                 long now = System.currentTimeMillis();
                 values.put(Bookmarks.DATE_CREATED, now);
                 values.put(Bookmarks.DATE_MODIFIED, now);
 
                 // Generate GUID for new bookmark. Don't override specified GUIDs.
                 if (!values.containsKey(Bookmarks.GUID)) {
-                  values.put(Bookmarks.GUID, generateGuid());
+                  values.put(Bookmarks.GUID, Utils.generateGuid());
                 }
 
                 if (!values.containsKey(Bookmarks.POSITION)) {
                     Log.d(LOGTAG, "Inserting bookmark with no position for URI");
                     values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
                 }
 
                 String url = values.getAsString(Bookmarks.URL);
@@ -735,17 +691,17 @@ public class BrowserProvider extends Con
                 Log.d(LOGTAG, "Insert on HISTORY: " + uri);
 
                 long now = System.currentTimeMillis();
                 values.put(History.DATE_CREATED, now);
                 values.put(History.DATE_MODIFIED, now);
 
                 // Generate GUID for new history entry. Don't override specified GUIDs.
                 if (!values.containsKey(History.GUID)) {
-                  values.put(History.GUID, generateGuid());
+                  values.put(History.GUID, Utils.generateGuid());
                 }
 
                 String url = values.getAsString(History.URL);
 
                 ContentValues imageValues = extractImageValues(values,
                         values.getAsString(History.URL));
 
                 if (imageValues != null) {
@@ -762,17 +718,17 @@ public class BrowserProvider extends Con
             case IMAGES: {
                 Log.d(LOGTAG, "Insert on IMAGES: " + uri);
 
                 long now = System.currentTimeMillis();
                 values.put(History.DATE_CREATED, now);
                 values.put(History.DATE_MODIFIED, now);
 
                 // Generate GUID for new history entry
-                values.put(History.GUID, generateGuid());
+                values.put(History.GUID, Utils.generateGuid());
 
                 String url = values.getAsString(Images.URL);
 
                 Log.d(LOGTAG, "Inserting image in database with URL: " + url);
                 id = db.insertOrThrow(TABLE_IMAGES, Images.URL, values);
                 break;
             }
 
@@ -820,31 +776,31 @@ public class BrowserProvider extends Con
 
         int match = URI_MATCHER.match(uri);
         int updated = 0;
 
         switch (match) {
             case BOOKMARKS_ID:
                 Log.d(LOGTAG, "Update on BOOKMARKS_ID: " + uri);
 
-                selection = concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
-                selectionArgs = appendSelectionArgs(selectionArgs,
+                selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case BOOKMARKS: {
                 Log.d(LOGTAG, "Updating bookmark: " + uri);
                 updated = updateBookmarks(uri, values, selection, selectionArgs);
                 break;
             }
 
             case HISTORY_ID:
                 Log.d(LOGTAG, "Update on HISTORY_ID: " + uri);
 
-                selection = concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
-                selectionArgs = appendSelectionArgs(selectionArgs,
+                selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case HISTORY: {
                 Log.d(LOGTAG, "Updating history: " + uri);
                 updated = updateHistory(uri, values, selection, selectionArgs);
                 break;
             }
 
@@ -882,27 +838,27 @@ public class BrowserProvider extends Con
 
         switch (match) {
             case BOOKMARKS_FOLDER_ID:
             case BOOKMARKS_ID:
             case BOOKMARKS: {
                 Log.d(LOGTAG, "Query is on bookmarks: " + uri);
 
                 if (match == BOOKMARKS_ID) {
-                    selection = concatenateWhere(selection, Bookmarks._ID + " = ?");
-                    selectionArgs = appendSelectionArgs(selectionArgs,
+                    selection = DBUtils.concatenateWhere(selection, Bookmarks._ID + " = ?");
+                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                             new String[] { Long.toString(ContentUris.parseId(uri)) });
                 } else if (match == BOOKMARKS_FOLDER_ID) {
-                    selection = concatenateWhere(selection, Bookmarks.PARENT + " = ?");
-                    selectionArgs = appendSelectionArgs(selectionArgs,
+                    selection = DBUtils.concatenateWhere(selection, Bookmarks.PARENT + " = ?");
+                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                             new String[] { Long.toString(ContentUris.parseId(uri)) });
                 }
 
                 if (!shouldShowDeleted(uri))
-                    selection = concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
+                    selection = DBUtils.concatenateWhere(Bookmarks.IS_DELETED + " = 0", selection);
 
                 if (TextUtils.isEmpty(sortOrder)) {
                     sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
                 } else {
                     Log.d(LOGTAG, "Using sort order " + sortOrder + ".");
                 }
 
                 qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
@@ -915,23 +871,23 @@ public class BrowserProvider extends Con
                 break;
             }
 
             case HISTORY_ID:
             case HISTORY: {
                 Log.d(LOGTAG, "Query is on history: " + uri);
 
                 if (match == HISTORY_ID) {
-                    selection = concatenateWhere(selection, History._ID + " = ?");
-                    selectionArgs = appendSelectionArgs(selectionArgs,
+                    selection = DBUtils.concatenateWhere(selection, History._ID + " = ?");
+                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                             new String[] { Long.toString(ContentUris.parseId(uri)) });
                 }
 
                 if (!shouldShowDeleted(uri))
-                    selection = concatenateWhere(History.IS_DELETED + " = 0", selection);
+                    selection = DBUtils.concatenateWhere(History.IS_DELETED + " = 0", selection);
 
                 if (TextUtils.isEmpty(sortOrder))
                     sortOrder = DEFAULT_HISTORY_SORT_ORDER;
 
                 qb.setProjectionMap(HISTORY_PROJECTION_MAP);
 
                 if (hasImagesInProjection(projection))
                     qb.setTables(VIEW_HISTORY_WITH_IMAGES);
@@ -941,23 +897,23 @@ public class BrowserProvider extends Con
                 break;
             }
 
             case IMAGES_ID:
             case IMAGES: {
                 Log.d(LOGTAG, "Query is on images: " + uri);
 
                 if (match == IMAGES_ID) {
-                    selection = concatenateWhere(selection, Images._ID + " = ?");
-                    selectionArgs = appendSelectionArgs(selectionArgs,
+                    selection = DBUtils.concatenateWhere(selection, Images._ID + " = ?");
+                    selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                             new String[] { Long.toString(ContentUris.parseId(uri)) });
                 }
 
                 if (!shouldShowDeleted(uri))
-                    selection = concatenateWhere(Images.IS_DELETED + " = 0", selection);
+                    selection = DBUtils.concatenateWhere(Images.IS_DELETED + " = 0", selection);
 
                 qb.setProjectionMap(IMAGES_PROJECTION_MAP);
                 qb.setTables(TABLE_IMAGES);
 
                 break;
             }
 
             case SCHEMA: {
@@ -1176,17 +1132,17 @@ public class BrowserProvider extends Con
             values.put(Images.IS_DELETED, 0);
 
         Log.d(LOGTAG, "Trying to update image for URL: " + url);
         int updated = db.update(TABLE_IMAGES, values, selection, selectionArgs);
 
         if (updated == 0 && insertIfNeeded) {
             // Generate GUID for new image, if one is not already provided.
             if (!values.containsKey(Images.GUID)) {
-              values.put(Images.GUID, generateGuid());
+              values.put(Images.GUID, Utils.generateGuid());
             }
 
             values.put(Images.DATE_CREATED, now);
             values.put(Images.DATE_MODIFIED, now);
 
             Log.d(LOGTAG, "No update, inserting image for URL: " + url);
             db.insert(TABLE_IMAGES, Images.FAVICON, values);
             updated = 1;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/DBUtils.java
@@ -0,0 +1,66 @@
+/* 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 org.mozilla.gecko.sync.Utils;
+
+import android.content.ContentValues;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import java.util.UUID;
+
+import java.util.Random;
+
+public class DBUtils {
+    public static final String qualifyColumn(String table, String column) {
+        return table + "." + column;
+    }
+
+    // This is available in Android >= 11. Implemented locally to be
+    // compatible with older versions.
+    public static String concatenateWhere(String a, String b) {
+        if (TextUtils.isEmpty(a)) {
+            return b;
+        }
+
+        if (TextUtils.isEmpty(b)) {
+            return a;
+        }
+
+        return "(" + a + ") AND (" + b + ")";
+    }
+
+    // This is available in Android >= 11. Implemented locally to be
+    // compatible with older versions.
+    public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
+        if (originalValues == null || originalValues.length == 0) {
+            return newValues;
+        }
+
+        if (newValues == null || newValues.length == 0) {
+            return originalValues;
+        }
+
+        String[] result = new String[originalValues.length + newValues.length];
+        System.arraycopy(originalValues, 0, result, 0, originalValues.length);
+        System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
+
+        return result;
+    }
+
+    public static void replaceKey(ContentValues aValues, String aOriginalKey,
+                                  String aNewKey, String aDefault) {
+        String value = aDefault;
+        if (aOriginalKey != null && aValues.containsKey(aOriginalKey)) {
+            value = aValues.get(aOriginalKey).toString();
+            aValues.remove(aOriginalKey);
+        }
+
+        if (!aValues.containsKey(aOriginalKey)) {
+            aValues.put(aNewKey, value);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/PasswordsProvider.java.in
@@ -0,0 +1,412 @@
+/* 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.GeckoDirProvider;
+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;
+
+public class PasswordsProvider extends ContentProvider {
+    private Context mContext = null;
+    private static final String LOGTAG = "GeckoPasswordsProvider";
+
+    static final String DATABASE_NAME = "signons.sqlite";
+
+    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;
+
+    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;
+
+    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>();
+        PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD);
+        PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME);
+        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(new GeckoEvent("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 = null;
+        try {
+            profileDir = GeckoDirProvider.getProfileDir(mContext, profile);
+        } catch (IOException ex) {
+            Log.e(LOGTAG, "Error getting profile dir", ex);
+        }
+
+        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;
+    }
+
+    @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;
+        }
+
+        Log.d(LOGTAG, "URI has unrecognized type: " + uri);
+        return null;
+    }
+
+    private String getTable(Uri uri) {
+        final int match = URI_MATCHER.match(uri);
+        switch (match) {
+            case DELETED_PASSWORDS:
+                return TABLE_DELETED_PASSWORDS;
+            case PASSWORDS: {
+                return TABLE_PASSWORDS;
+            }
+            default:
+                throw new UnsupportedOperationException("Unknown table " + uri);
+        }
+    }
+
+    private 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: {
+                return DEFAULT_PASSWORDS_SORT_ORDER;
+            }
+            default:
+                throw new UnsupportedOperationException("Unknown delete 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)
+            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: {
+                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();
+                DBUtils.replaceKey(values, CommonColumns._ID, Passwords.ID, "");
+                DBUtils.replaceKey(values, SyncColumns.DATE_CREATED, Passwords.TIME_CREATED, nowString);
+                DBUtils.replaceKey(values, SyncColumns.DATE_MODIFIED, Passwords.TIME_PASSWORD_CHANGED, nowString);
+                DBUtils.replaceKey(values, null, Passwords.HOSTNAME, "");
+                DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, "");
+                DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, "");
+                DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, "");
+                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);
+        }
+    }
+
+    @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;
+    }
+}
--- a/mobile/android/base/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/sqlite/SQLiteBridge.java
@@ -1,53 +1,28 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla Android code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2012
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Gian-Carlo Pascutto <gpascutto@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* 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.sqlite;
 
 import org.mozilla.gecko.sqlite.SQLiteBridgeException;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.lang.String;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
 
 /*
  * This class allows using the mozsqlite3 library included with Firefox
  * to read SQLite databases, instead of the Android SQLiteDataBase API,
  * which might use whatever outdated DB is present on the Android system.
  */
 public class SQLiteBridge {
     private static final String LOGTAG = "SQLiteBridge";
@@ -63,16 +38,227 @@ public class SQLiteBridge {
                                           ArrayList<Object[]> aRes)
         throws SQLiteBridgeException;
 
     // Takes the path to the database we want to access.
     public SQLiteBridge(String aDb) throws SQLiteBridgeException {
         mDb = aDb;
     }
 
+    // Executes a simple line of sql.
+    public void execSQL(String sql)
+                throws SQLiteBridgeException {
+        try {
+            query(sql, null);
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+    }
+
+    // Executes a simple line of sql. Allow you to bind arguments
+    public void execSQL(String sql, String[] bindArgs)
+                throws SQLiteBridgeException {
+        try {
+            query(sql, bindArgs);
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+    }
+
+    // Executes a DELETE statement on the database
+    public int delete(String table, String whereClause, String[] whereArgs)
+               throws SQLiteBridgeException {
+        StringBuilder sb = new StringBuilder("DELETE from ");
+        sb.append(table);
+        if (whereClause != null) {
+            sb.append(" WHERE " + whereClause);
+        }
+
+        try {
+            return getIntResult(sb.toString(), whereArgs, 1);
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+    }
+
+    public Cursor query(String table,
+                        String[] columns,
+                        String selection,
+                        String[] selectionArgs,
+                        String groupBy,
+                        String having,
+                        String orderBy,
+                        String limit)
+               throws SQLiteBridgeException {
+        StringBuilder sb = new StringBuilder("SELECT ");
+        if (columns != null)
+            sb.append(TextUtils.join(", ", columns));
+        else
+            sb.append(" * ");
+
+        sb.append(" FROM ");
+        sb.append(table);
+
+        if (selection != null) {
+            sb.append(" WHERE " + selection);
+        }
+
+        if (groupBy != null) {
+            sb.append(" GROUP BY " + groupBy);
+        }
+
+        if (having != null) {
+            sb.append(" HAVING " + having);
+        }
+
+        if (orderBy != null) {
+            sb.append(" ORDER BY " + orderBy);
+        }
+
+        if (limit != null) {
+            sb.append(" " + limit);
+        }
+
+        ArrayList<Object[]> results;
+        try {
+            mColumns = null;
+
+            results = query(sb.toString(), selectionArgs);
+
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+
+        MatrixCursor cursor = new MatrixCursor(mColumns.toArray(new String[0]));
+        try {
+            for (Object resultRow: results) {
+                Object[] resultColumns = (Object[])resultRow;
+                if (resultColumns.length == mColumns.size())
+                    cursor.addRow(resultColumns);
+            }
+        } catch(IllegalArgumentException ex) {
+            Log.e(LOGTAG, "Error getting rows", ex);
+        }
+
+        return cursor;
+    }
+
+    public long insert(String table, String nullColumnHack, ContentValues values)
+               throws SQLiteBridgeException {
+        Set<Entry<String, Object>> valueSet = values.valueSet();
+        Iterator<Entry<String, Object>> valueIterator = valueSet.iterator();
+        ArrayList<String> valueNames = new ArrayList<String>();
+        ArrayList<String> valueBinds = new ArrayList<String>();
+        ArrayList<String> keyNames = new ArrayList<String>();
+
+        while(valueIterator.hasNext()) {
+            Entry<String, Object> value = valueIterator.next();
+            keyNames.add(value.getKey());
+            valueNames.add("?");
+            valueBinds.add(value.getValue().toString());
+        }
+
+        StringBuilder sb = new StringBuilder("INSERT into ");
+        sb.append(table);
+
+        sb.append(" (");
+        sb.append(TextUtils.join(", ", keyNames));
+        sb.append(")");
+
+        // XXX - Do we need to bind these values?
+        sb.append(" VALUES (");
+        sb.append(TextUtils.join(", ", valueNames));
+        sb.append(") ");
+
+        String[] binds = new String[valueBinds.size()];
+        valueBinds.toArray(binds);
+        try {
+            return getIntResult(sb.toString(), binds, 0);
+        } catch (SQLiteBridgeException ex) {
+            throw ex;
+        }
+    }
+
+    public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
+               throws SQLiteBridgeException {
+        Set<Entry<String, Object>> valueSet = values.valueSet();
+        Iterator<Entry<String, Object>> valueIterator = valueSet.iterator();
+        ArrayList<String> valueNames = new ArrayList<String>();
+
+        StringBuilder sb = new StringBuilder("UPDATE ");
+        sb.append(table);
+
+        sb.append(" SET ");
+        while(valueIterator.hasNext()) {
+            Entry<String, Object> value = valueIterator.next();
+            sb.append(value.getKey() + " = ?");
+            valueNames.add(value.getValue().toString());
+            if (valueIterator.hasNext())
+                sb.append(", ");
+            else
+                sb.append(" ");
+        }
+
+        if (whereClause != null) {
+            sb.append(" WHERE ");
+            sb.append(whereClause);
+            for (int i = 0; i < whereArgs.length; i++) {
+                valueNames.add(whereArgs[i]);
+            }
+        }
+
+        String[] binds = new String[valueNames.size()];
+        valueNames.toArray(binds);
+        try {
+            return getIntResult(sb.toString(), binds, 1);
+        } catch (SQLiteBridgeException ex) {
+            throw ex;
+        }
+    }
+
+    private int getIntResult(String query, String[] params, int resultIndex)
+               throws SQLiteBridgeException {
+        ArrayList<Object[]> results = null;
+        try {
+            mColumns = null;
+            results = query(query, params);
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+
+        if (results != null) {
+            for (Object resultRow: results) {
+                Object[] resultColumns = (Object[])resultRow;
+                return ((Number)resultColumns[resultIndex]).intValue();
+            }
+        }
+        return -1;
+    }
+
+    public int getVersion()
+               throws SQLiteBridgeException {
+        ArrayList<Object[]> results = null;
+        try {
+            mColumns = null;
+            results = query("PRAGMA user_version");
+        } catch(SQLiteBridgeException ex) {
+            throw ex;
+        }
+        int ret = -1;
+        if (results != null) {
+            for (Object resultRow: results) {
+                Object[] resultColumns = (Object[])resultRow;
+                String version = (String)resultColumns[0];
+                ret = Integer.parseInt(version);
+            }
+        }
+        return ret;
+    }
+
+
     // Do an SQL query without parameters
     public ArrayList<Object[]> query(String aQuery) throws SQLiteBridgeException {
         String[] params = new String[0];
         return query(aQuery, params);
     }
 
     // Do an SQL query, substituting the parameters in the query with the passed
     // parameters. The parameters are subsituded in order, so named parameters
@@ -90,12 +276,11 @@ public class SQLiteBridge {
     }
 
     // Gets the index in the row Object[] for the given column name.
     // Returns -1 if not found.
     public int getColumnIndex(String aColumnName) {
         return mColumns.lastIndexOf(aColumnName);
     }
 
-    public void close() {
-        // nop, provided for API compatibility with SQLiteDatabase.
-    }
+    // nop, provided for API compatibility with SQLiteDatabase.
+    public void close() { }
 }
\ No newline at end of file
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -216,16 +216,17 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Preferences:Get", false);
     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);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
       sendMessageToJava({
         gecko: {
@@ -965,16 +966,22 @@ var BrowserApp = {
       Sanitizer.sanitize();
     } else if (aTopic == "FullScreen:Exit") {
       browser.contentDocument.mozCancelFullScreen();
     } else if (aTopic == "Viewport:Change") {
       this.selectedTab.viewport = JSON.parse(aData);
       ViewportHandler.onResize();
     } 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" }});
     }
   },
 
   get defaultBrowserWidth() {
     delete this.defaultBrowserWidth;
     let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
     return this.defaultBrowserWidth = width;
   }
--- a/mozglue/android/SQLiteBridge.cpp
+++ b/mozglue/android/SQLiteBridge.cpp
@@ -30,16 +30,17 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #include <stdlib.h>
+#include <stdio.h>
 #include <jni.h>
 #include <android/log.h>
 #include "dlfcn.h"
 #include "APKOpen.h"
 #ifndef MOZ_OLD_LINKER
 #include "ElfLoader.h"
 #endif
 #include "SQLiteBridge.h"
@@ -61,16 +62,18 @@ SQLITE_WRAPPER_INT(sqlite3_step)
 SQLITE_WRAPPER_INT(sqlite3_column_count)
 SQLITE_WRAPPER_INT(sqlite3_finalize)
 SQLITE_WRAPPER_INT(sqlite3_close)
 SQLITE_WRAPPER_INT(sqlite3_column_name)
 SQLITE_WRAPPER_INT(sqlite3_column_type)
 SQLITE_WRAPPER_INT(sqlite3_column_blob)
 SQLITE_WRAPPER_INT(sqlite3_column_bytes)
 SQLITE_WRAPPER_INT(sqlite3_column_text)
+SQLITE_WRAPPER_INT(sqlite3_changes)
+SQLITE_WRAPPER_INT(sqlite3_last_insert_rowid)
 
 void setup_sqlite_functions(void *sqlite_handle)
 {
 #define GETFUNC(name) f_ ## name = (name ## _t) __wrap_dlsym(sqlite_handle, #name)
   GETFUNC(sqlite3_open);
   GETFUNC(sqlite3_errmsg);
   GETFUNC(sqlite3_prepare_v2);
   GETFUNC(sqlite3_bind_parameter_count);
@@ -79,16 +82,18 @@ void setup_sqlite_functions(void *sqlite
   GETFUNC(sqlite3_column_count);
   GETFUNC(sqlite3_finalize);
   GETFUNC(sqlite3_close);
   GETFUNC(sqlite3_column_name);
   GETFUNC(sqlite3_column_type);
   GETFUNC(sqlite3_column_blob);
   GETFUNC(sqlite3_column_bytes);
   GETFUNC(sqlite3_column_text);
+  GETFUNC(sqlite3_changes);
+  GETFUNC(sqlite3_last_insert_rowid);
 #undef GETFUNC
 }
 
 static bool initialized = false;
 static jclass stringClass;
 static jclass objectClass;
 static jclass byteBufferClass;
 static jclass arrayListClass;
@@ -152,102 +157,134 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid
                                                       jstring jDb,
                                                       jstring jQuery,
                                                       jobjectArray jParams,
                                                       jobject jColumns,
                                                       jobject jArrayList)
 {
     JNI_Setup(jenv);
 
+    char* errorMsg;
+    jsize numPars = 0;
+
     const char* queryStr;
     queryStr = jenv->GetStringUTFChars(jQuery, NULL);
 
     const char* dbPath;
     dbPath = jenv->GetStringUTFChars(jDb, NULL);
 
     const char *pzTail;
     sqlite3_stmt *ppStmt;
     sqlite3 *db;
     int rc;
     rc = f_sqlite3_open(dbPath, &db);
     jenv->ReleaseStringUTFChars(jDb, dbPath);
 
     if (rc != SQLITE_OK) {
-        LOG("Can't open database: %s\n", f_sqlite3_errmsg(db));
+        asprintf(&errorMsg, "Can't open database: %s\n", f_sqlite3_errmsg(db));
         goto error_close;
     }
 
     rc = f_sqlite3_prepare_v2(db, queryStr, -1, &ppStmt, &pzTail);
     if (rc != SQLITE_OK || ppStmt == NULL) {
-        LOG("Can't prepare statement: %s\n", f_sqlite3_errmsg(db));
+        asprintf(&errorMsg, "Can't prepare statement: %s\n", f_sqlite3_errmsg(db));
         goto error_close;
     }
     jenv->ReleaseStringUTFChars(jQuery, queryStr);
 
     // Check if number of parameters matches
-    jsize numPars;
-    numPars = jenv->GetArrayLength(jParams);
+    if (jParams != NULL) {
+        numPars = jenv->GetArrayLength(jParams);
+    }
     int sqlNumPars;
     sqlNumPars = f_sqlite3_bind_parameter_count(ppStmt);
     if (numPars != sqlNumPars) {
-        LOG("Passed parameter count (%d) doesn't match SQL parameter count (%d)\n",
+        asprintf(&errorMsg, "Passed parameter count (%d) doesn't match SQL parameter count (%d)\n",
             numPars, sqlNumPars);
         goto error_close;
     }
-    // Bind parameters, if any
-    if (numPars > 0) {
-        for (int i = 0; i < numPars; i++) {
-            jobject jObjectParam = jenv->GetObjectArrayElement(jParams, i);
-            // IsInstanceOf or isAssignableFrom? String is final, so IsInstanceOf
-            // should be OK.
-            jboolean isString = jenv->IsInstanceOf(jObjectParam, stringClass);
-            if (isString != JNI_TRUE) {
-                LOG("Parameter is not of String type");
-                goto error_close;
-            }
-            jstring jStringParam = (jstring)jObjectParam;
-            const char* paramStr = jenv->GetStringUTFChars(jStringParam, NULL);
-            // SQLite parameters index from 1.
-            rc = f_sqlite3_bind_text(ppStmt, i + 1, paramStr, -1, SQLITE_TRANSIENT);
-            jenv->ReleaseStringUTFChars(jStringParam, paramStr);
-            if (rc != SQLITE_OK) {
-                LOG("Error binding query parameter");
-                goto error_close;
+
+    if (jParams != NULL) {
+        // Bind parameters, if any
+        if (numPars > 0) {
+            for (int i = 0; i < numPars; i++) {
+                jobject jObjectParam = jenv->GetObjectArrayElement(jParams, i);
+                // IsInstanceOf or isAssignableFrom? String is final, so IsInstanceOf
+                // should be OK.
+                jboolean isString = jenv->IsInstanceOf(jObjectParam, stringClass);
+                if (isString != JNI_TRUE) {
+                    asprintf(&errorMsg, "Parameter is not of String type");
+                    goto error_close;
+                }
+                jstring jStringParam = (jstring)jObjectParam;
+                const char* paramStr = jenv->GetStringUTFChars(jStringParam, NULL);
+                // SQLite parameters index from 1.
+                rc = f_sqlite3_bind_text(ppStmt, i + 1, paramStr, -1, SQLITE_TRANSIENT);
+                jenv->ReleaseStringUTFChars(jStringParam, paramStr);
+                if (rc != SQLITE_OK) {
+                    asprintf(&errorMsg, "Error binding query parameter");
+                    goto error_close;
+                }
             }
         }
     }
 
     // Execute the query and step through the results
     rc = f_sqlite3_step(ppStmt);
     if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
-        LOG("Can't step statement: (%d) %s\n", rc, f_sqlite3_errmsg(db));
+        asprintf(&errorMsg, "Can't step statement: (%d) %s\n", rc, f_sqlite3_errmsg(db));
         goto error_close;
     }
 
     // Get the column names
     int cols;
     cols = f_sqlite3_column_count(ppStmt);
     for (int i = 0; i < cols; i++) {
         const char* colName = f_sqlite3_column_name(ppStmt, i);
         jstring jStr = jenv->NewStringUTF(colName);
         jenv->CallBooleanMethod(jColumns, jArrayListAdd, jStr);
         jenv->DeleteLocalRef(jStr);
     }
 
+    // if the statement doesn't return any results, instead return the id and number of changed rows
+    if (rc == SQLITE_DONE) {
+        jclass integerClass = jenv->FindClass("java/lang/Integer");
+        jmethodID intConstructor = jenv->GetMethodID(integerClass, "<init>", "(I)V");
+        
+        jobjectArray jRow = jenv->NewObjectArray(2, objectClass, NULL);
+        if (jRow == NULL) {
+            asprintf(&errorMsg, "Can't allocate jRow Object[]\n");
+            goto error_close;
+        }
+
+        int id = f_sqlite3_last_insert_rowid(db);
+        jobject jId = jenv->NewObject(integerClass, intConstructor, id);
+        jenv->SetObjectArrayElement(jRow, 0, jId);
+        jenv->DeleteLocalRef(jId);
+
+        int changed = f_sqlite3_changes(db);
+        jobject jChanged = jenv->NewObject(integerClass, intConstructor, changed);
+        jenv->SetObjectArrayElement(jRow, 1, jChanged);
+        jenv->DeleteLocalRef(jChanged);
+
+        jenv->CallBooleanMethod(jArrayList, jArrayListAdd, jRow);
+        jenv->DeleteLocalRef(jRow);
+    }
+
     // For each row, add an Object[] to the passed ArrayList,
     // with that containing either String or ByteArray objects
     // containing the columns
     while (rc != SQLITE_DONE) {
         // Process row
         // Construct Object[]
         jobjectArray jRow = jenv->NewObjectArray(cols,
                                                  objectClass,
                                                  NULL);
         if (jRow == NULL) {
-            LOG("Can't allocate jRow Object[]\n");
+            asprintf(&errorMsg, "Can't allocate jRow Object[]\n");
             goto error_close;
         }
 
         for (int i = 0; i < cols; i++) {
             int colType = f_sqlite3_column_type(ppStmt, i);
             if (colType == SQLITE_BLOB) {
                 // Treat as blob
                 const void* blob = f_sqlite3_column_blob(ppStmt, i);
@@ -260,17 +297,17 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid
                                                  colLen);
                 if (jByteBuffer == NULL) {
                     goto error_close;
                 }
 
                 // Get its backing array
                 void* bufferArray = jenv->GetDirectBufferAddress(jByteBuffer);
                 if (bufferArray == NULL) {
-                    LOG("Failure calling GetDirectBufferAddress\n");
+                    asprintf(&errorMsg, "Failure calling GetDirectBufferAddress\n");
                     goto error_close;
                 }
                 memcpy(bufferArray, blob, colLen);
 
                 jenv->SetObjectArrayElement(jRow, i, jByteBuffer);
                 jenv->DeleteLocalRef(jByteBuffer);
             } else if (colType == SQLITE_NULL) {
                 jenv->SetObjectArrayElement(jRow, i, jNull);
@@ -289,27 +326,29 @@ Java_org_mozilla_gecko_sqlite_SQLiteBrid
 
         // Clean up
         jenv->DeleteLocalRef(jRow);
 
         // Get next row
         rc = f_sqlite3_step(ppStmt);
         // Real error?
         if (rc != SQLITE_ROW && rc != SQLITE_DONE) {
-            LOG("Can't re-step statement:(%d) %s\n", rc, f_sqlite3_errmsg(db));
+            asprintf(&errorMsg, "Can't re-step statement:(%d) %s\n", rc, f_sqlite3_errmsg(db));
             goto error_close;
         }
     }
 
     rc = f_sqlite3_finalize(ppStmt);
     if (rc != SQLITE_OK) {
-        LOG("Can't finalize statement: %s\n", f_sqlite3_errmsg(db));
+        asprintf(&errorMsg, "Can't finalize statement: %s\n", f_sqlite3_errmsg(db));
         goto error_close;
     }
 
     f_sqlite3_close(db);
     return;
 
 error_close:
     f_sqlite3_close(db);
-    JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", "SQLite error");
+    LOG("Error in SQLiteBridge: %s\n", errorMsg);
+    JNI_Throw(jenv, "org/mozilla/gecko/sqlite/SQLiteBridgeException", errorMsg);
+    free(errorMsg);
     return;
 }
--- a/mozglue/android/SQLiteBridge.h
+++ b/mozglue/android/SQLiteBridge.h
@@ -54,10 +54,12 @@ SQLITE_WRAPPER(sqlite3_step, int, sqlite
 SQLITE_WRAPPER(sqlite3_column_count, int, sqlite3_stmt*)
 SQLITE_WRAPPER(sqlite3_finalize, int, sqlite3_stmt*)
 SQLITE_WRAPPER(sqlite3_close, int, sqlite3*)
 SQLITE_WRAPPER(sqlite3_column_name, const char*, sqlite3_stmt*, int)
 SQLITE_WRAPPER(sqlite3_column_type, int, sqlite3_stmt*, int)
 SQLITE_WRAPPER(sqlite3_column_blob, const void*, sqlite3_stmt*, int)
 SQLITE_WRAPPER(sqlite3_column_bytes, int, sqlite3_stmt*, int)
 SQLITE_WRAPPER(sqlite3_column_text, const unsigned char*, sqlite3_stmt*, int)
+SQLITE_WRAPPER(sqlite3_changes, int, sqlite3*)
+SQLITE_WRAPPER(sqlite3_last_insert_rowid, sqlite3_int64, sqlite3*)
 
 #endif /* SQLiteBridge_h */