Bug 946857 - part 2: JNI based password manager storage r?nalexander,jchen draft
authorvivek <vivekb.balakrishnan@gmail.com>
Tue, 29 Dec 2015 01:07:42 +0200
changeset 319379 ef6d060714e220e32f7461532548e0baf29fd149
parent 319352 ed2b990adc5f908f15f1e74d197974f714b0eb2f
child 319381 deade657be91833deec678958b1aa9b0362ac36f
push id9020
push userbmo:vivekb.balakrishnan@gmail.com
push dateWed, 06 Jan 2016 18:30:33 +0000
reviewersnalexander, jchen
bugs946857
milestone46.0a1
Bug 946857 - part 2: JNI based password manager storage r?nalexander,jchen Patch changes: Exposed an accessor for LoginsProvider New passwordmgr storage that uses JNI to store data through LoginsProvider CP Fixed getLoginsSavedEnabled() return type mis-match. Fixed potential memory leak in fennecStorage.js Test toolkit/components/passwordmgr changes Yet to do: Use LoginsProvider with Sync Password RepoSessions
mobile/android/b2gdroid/installer/package-manifest.in
mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/LocalLoginsAccessor.java
mobile/android/base/java/org/mozilla/gecko/db/LoginsAccessor.java
mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
mobile/android/base/moz.build
mobile/android/installer/package-manifest.in
mobile/android/tests/browser/chrome/chrome.ini
mobile/android/tests/browser/chrome/test_fennecStorage.html
toolkit/components/passwordmgr/moz.build
toolkit/components/passwordmgr/nsLoginManager.js
toolkit/components/passwordmgr/passwordmgr.manifest
toolkit/components/passwordmgr/storage-fennecStorage.js
--- a/mobile/android/b2gdroid/installer/package-manifest.in
+++ b/mobile/android/b2gdroid/installer/package-manifest.in
@@ -333,17 +333,17 @@
 @BINPATH@/components/nsSetDefaultBrowser.js
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSearchSuggestions.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
 @BINPATH@/components/nsLoginManagerPrompter.js
-@BINPATH@/components/storage-mozStorage.js
+@BINPATH@/components/storage-fennecStorage.js
 @BINPATH@/components/crypto-SDR.js
 @BINPATH@/components/jsconsole-clhandler.manifest
 @BINPATH@/components/jsconsole-clhandler.js
 @BINPATH@/components/nsHelperAppDlg.manifest
 @BINPATH@/components/nsHelperAppDlg.js
 @BINPATH@/components/NetworkGeolocationProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.js
 @BINPATH@/components/nsSidebar.manifest
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -32,16 +32,18 @@ import java.util.StringTokenizer;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.LoginsAccessor;
+import org.mozilla.gecko.db.StubBrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PanZoomController;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.prompts.PromptService;
@@ -198,16 +200,23 @@ public class GeckoAppShell
         }
     };
 
     public static CrashHandler ensureCrashHandling() {
         // Crash handling is automatically enabled when GeckoAppShell is loaded.
         return CRASH_HANDLER;
     }
 
+    @JNITarget
+    public static LoginsAccessor getLoginsAccessor() {
+        final Context context = getApplicationContext();
+        final BrowserDB db = GeckoProfile.get(context).getDB();
+        return db.getLoginsAccessor();
+    }
+
     private static final Map<String, String> ALERT_COOKIES = new ConcurrentHashMap<String, String>();
 
     private static volatile boolean locationHighAccuracyEnabled;
 
     // Accessed by NotificationHelper. This should be encapsulated.
     /* package */ static NotificationClient notificationClient;
 
     // See also HardwareUtils.LOW_MEMORY_THRESHOLD_MB.
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -38,16 +38,17 @@ public interface BrowserDB {
     public static enum FilterFlags {
         EXCLUDE_PINNED_SITES
     }
 
     public abstract Searches getSearches();
     public abstract TabsAccessor getTabsAccessor();
     public abstract URLMetadata getURLMetadata();
     public abstract ReadingListAccessor getReadingListAccessor();
+    public abstract LoginsAccessor getLoginsAccessor();
 
     /**
      * Add default bookmarks to the database.
      * Takes an offset; returns a new offset.
      */
     public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -96,16 +96,17 @@ public class LocalBrowserDB implements B
     private final Uri mFaviconsUriWithProfile;
     private final Uri mThumbnailsUriWithProfile;
     private final Uri mSearchHistoryUri;
 
     private LocalSearches searches;
     private LocalTabsAccessor tabsAccessor;
     private LocalURLMetadata urlMetadata;
     private LocalReadingListAccessor readingListAccessor;
+    private LoginsAccessor loginsAccessor;
 
     private static final String[] DEFAULT_BOOKMARK_COLUMNS =
             new String[] { Bookmarks._ID,
                            Bookmarks.GUID,
                            Bookmarks.URL,
                            Bookmarks.TITLE,
                            Bookmarks.TYPE,
                            Bookmarks.PARENT };
@@ -129,16 +130,17 @@ public class LocalBrowserDB implements B
                                       .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
                                       .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
                                       .build();
 
         searches = new LocalSearches(mProfile);
         tabsAccessor = new LocalTabsAccessor(mProfile);
         urlMetadata = new LocalURLMetadata(mProfile);
         readingListAccessor = new LocalReadingListAccessor(mProfile);
+        loginsAccessor = new LocalLoginsAccessor(mProfile);
     }
 
     @Override
     public Searches getSearches() {
         return searches;
     }
 
     @Override
@@ -151,16 +153,21 @@ public class LocalBrowserDB implements B
         return urlMetadata;
     }
 
     @Override
     public ReadingListAccessor getReadingListAccessor() {
         return readingListAccessor;
     }
 
+    @Override
+    public LoginsAccessor getLoginsAccessor() {
+        return loginsAccessor;
+    }
+
     /**
      * Not thread safe. A helper to allocate new IDs for arbitrary strings.
      */
     private static class NameCounter {
         private final HashMap<String, Integer> names = new HashMap<String, Integer>();
         private int counter;
         private final int increment;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalLoginsAccessor.java
@@ -0,0 +1,102 @@
+/* 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 android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import org.mozilla.gecko.GeckoAppShell;
+
+import static org.mozilla.gecko.db.BrowserContract.DeletedLogins;
+import static org.mozilla.gecko.db.BrowserContract.DisabledHosts;
+import static org.mozilla.gecko.db.BrowserContract.Logins;
+
+public class LocalLoginsAccessor implements LoginsAccessor {
+    private static final String LOGTAG = "GeckoLoginsAccessor";
+
+    private final Uri loginsUriWithProfile;
+    private final Uri deletedLoginsUriWithProfile;
+    private final Uri disabledHostsUriWithProfile;
+
+    public LocalLoginsAccessor(String profileName) {
+        loginsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, Logins.CONTENT_URI);
+        deletedLoginsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, DeletedLogins.CONTENT_URI);
+        disabledHostsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, DisabledHosts.CONTENT_URI);
+    }
+
+    private Context getContext() {
+        return GeckoAppShell.getGeckoInterface().getActivity();
+    }
+
+    @Override
+    public void addLogin(ContentValues values) {
+        getContext().getContentResolver().insert(loginsUriWithProfile, values);
+    }
+
+    @Override
+    public void removeLogin(long id) {
+        getContext().getContentResolver().delete(loginsUriWithProfile.buildUpon().appendPath(String.valueOf(id)).build(), null, null);
+    }
+
+    @Override
+    public void modifyLogin(long id, ContentValues values) {
+        getContext().getContentResolver().update(loginsUriWithProfile.buildUpon().appendPath(String.valueOf(id)).build(), values, null, null);
+    }
+
+    @Override
+    public void removeAllLogins() {
+        getContext().getContentResolver().delete(loginsUriWithProfile, null, null);
+        getContext().getContentResolver().delete(deletedLoginsUriWithProfile, null, null);
+    }
+
+    @Override
+    public int countLogins() {
+        Cursor cursor = getContext().getContentResolver().query(loginsUriWithProfile, null, null, null, null);
+        try {
+            return cursor == null ? 0 : cursor.getCount();
+        } finally {
+            cursor.close();
+        }
+    }
+
+    @Override
+    public Cursor searchLogins(String selection, String[] selectionArgs) {
+        final Cursor cursor = getContext().getContentResolver().query(loginsUriWithProfile, null, selection, selectionArgs, null);
+        return cursor;
+    }
+
+    @Override
+    public Cursor getAllDisabledHosts() {
+        final Cursor cursor = getContext().getContentResolver().query(disabledHostsUriWithProfile, null, null, null, null);
+        return cursor;
+    }
+
+    @Override
+    public Cursor getLoginsSavedEnabled(String hostname) {
+        final Uri uri = disabledHostsUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT, "1").build();
+        boolean isHostnameEmpty = TextUtils.isEmpty(hostname);
+        return getContext()
+                .getContentResolver()
+                .query(uri,
+                       null,
+                       isHostnameEmpty ? null : DisabledHosts.HOSTNAME + "= ?",
+                       isHostnameEmpty ? null : new String[] {hostname},
+                       null);
+    }
+
+    @Override
+    public void setLoginSavingEnabled(String hostname, boolean isEnabled) {
+        if (isEnabled) {
+            getContext().getContentResolver().delete(disabledHostsUriWithProfile, DisabledHosts.HOSTNAME + "=?", new String[]{hostname});
+        } else {
+            final ContentValues values = new ContentValues();
+            values.put(DisabledHosts.HOSTNAME, hostname);
+            getContext().getContentResolver().insert(disabledHostsUriWithProfile, values);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LoginsAccessor.java
@@ -0,0 +1,87 @@
+/* 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.annotation.JNITarget;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+/**
+ * Interface for interactions with {@link LoginsProvider} that is accessed from
+ * {@link nsILoginManagerStorage.idl} implementation through JNI.
+ *
+ * Nota bene: The concrete implementor of this interface does not validate the parameters and the parameters are passed
+ * to {@link android.content.ContentProvider} as it is. It is the responsibility of the caller to validate
+ * the arguments.
+ */
+
+@JNITarget
+public interface LoginsAccessor {
+    /**
+     * Store a new login entry from the content values after deleting the entry from deletedLogins table if needed.
+     *
+     * @param values The ContentValues to use.
+     */
+    void addLogin(ContentValues values);
+
+    /**
+     * Remove the login from logins table and store it as a deletedLogins.
+     *
+     * @param id login id to delete.
+     */
+    void removeLogin(long id);
+
+    /**
+     * Update the login from content values.
+     *
+     * @param id login id to update.
+     * @param values The ContentValues to use.
+     */
+    void modifyLogin(long id, ContentValues values);
+
+    // Wipe all stored logins and deletedLogins.
+    void removeAllLogins();
+
+    // Count the number of all stored logins.
+    int countLogins();
+
+    /**
+     * Search for logins based on selection condition.
+     * The resultant cursor has all columns for {@link org.mozilla.gecko.db.BrowserContract.Logins} selected.
+     *
+     * @param selection The SQL selection where clause.
+     * @param selectionArgs The conditional values to substitute in place of format parameters.
+     * @return A cursor representing the contents of the logins table filtered according to the arguments.
+     * Can return <code>null</code>.
+     */
+    Cursor searchLogins(String selection, String[] selectionArgs);
+
+    /**
+     * Query for hostname of all disabled hosts.
+     *
+     * @return A cursor representing the contents of the disabledHosts table.
+     * Can return <code>null</code>.
+     */
+    Cursor getAllDisabledHosts();
+
+    /**
+     * Check if hostname has entry in disabledHosts table.
+     *
+     * @param hostname The hostname for querying. <code>null</code> will return all the rows.
+     * @return A cursor representing the contents of the disabledHosts table.
+     * Can return <code>null</code>.
+     */
+    Cursor getLoginsSavedEnabled(String hostname);
+
+    /**
+     * Store/remove hostname from disabledHosts table based on boolean parameter. The login saving is
+     * dependent on hostname not present in disabledHosts table.
+     *
+     * @param hostname The hostname to store/remove from disabledHosts table
+     * @param isEnabled The hostname is removed from disabledHosts table if true and stored if false.
+     */
+    void setLoginSavingEnabled(String hostname, boolean isEnabled);
+}
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -145,25 +145,71 @@ class StubTabsAccessor implements TabsAc
     }
     public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
         listener.onQueryTabsComplete(new ArrayList<RemoteClient>());
     }
 
     public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) { }
 }
 
+class StubLoginsAccessor implements LoginsAccessor {
+    public StubLoginsAccessor() {
+    }
+
+    @Override
+    public void addLogin(ContentValues values) {
+    }
+
+    @Override
+    public void removeLogin(long id) {
+    }
+
+    @Override
+    public void modifyLogin(long id, ContentValues values) {
+    }
+
+    @Override
+    public void removeAllLogins() {
+    }
+
+    @Override
+    public int countLogins() {
+        return 0;
+    }
+
+    @Override
+    public Cursor searchLogins(String selection, String[] selectionArgs) {
+        return null;
+    }
+
+    @Override
+    public Cursor getAllDisabledHosts() {
+        return null;
+    }
+
+    @Override
+    public Cursor getLoginsSavedEnabled(String hostname) {
+        return null;
+    }
+
+    @Override
+    public void setLoginSavingEnabled(String hostname, boolean isEnabled) {
+    }
+}
+
 /*
  * This base implementation just stubs all methods. For the
  * real implementations, see LocalBrowserDB.java.
  */
 public class StubBrowserDB implements BrowserDB {
     private final StubSearches searches = new StubSearches();
     private final StubTabsAccessor tabsAccessor = new StubTabsAccessor();
     private final StubURLMetadata urlMetadata = new StubURLMetadata();
     private final StubReadingListAccessor readingListAccessor = new StubReadingListAccessor();
+    private final StubLoginsAccessor loginsAccessor = new StubLoginsAccessor();
 
     @Override
     public Searches getSearches() {
         return searches;
     }
 
     @Override
     public TabsAccessor getTabsAccessor() {
@@ -175,16 +221,21 @@ public class StubBrowserDB implements Br
         return urlMetadata;
     }
 
     @Override
     public ReadingListAccessor getReadingListAccessor() {
         return readingListAccessor;
     }
 
+    @Override
+    public LoginsAccessor getLoginsAccessor() {
+        return loginsAccessor;
+    }
+
     protected static final Integer FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
 
     public StubBrowserDB(String profile) {
     }
 
     public void invalidate() { }
 
     public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -216,20 +216,22 @@ gbjar.sources += ['java/org/mozilla/geck
     'db/BaseTable.java',
     'db/BrowserDatabaseHelper.java',
     'db/BrowserDB.java',
     'db/BrowserProvider.java',
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
+    'db/LocalLoginsAccessor.java',
     'db/LocalReadingListAccessor.java',
     'db/LocalSearches.java',
     'db/LocalTabsAccessor.java',
     'db/LocalURLMetadata.java',
+    'db/LoginsAccessor.java',
     'db/LoginsProvider.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListAccessor.java',
     'db/ReadingListProvider.java',
     'db/RemoteClient.java',
     'db/RemoteTab.java',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -294,17 +294,17 @@
 @BINPATH@/components/nsDNSServiceDiscovery.js
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsSearchService.js
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
 @BINPATH@/components/nsLoginManagerPrompter.js
-@BINPATH@/components/storage-mozStorage.js
+@BINPATH@/components/storage-fennecStorage.js
 @BINPATH@/components/crypto-SDR.js
 @BINPATH@/components/NetworkGeolocationProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.js
 @BINPATH@/components/extensions.manifest
 @BINPATH@/components/addonManager.js
 @BINPATH@/components/amContentHandler.js
 @BINPATH@/components/amInstallTrigger.js
 @BINPATH@/components/amWebInstallListener.js
--- a/mobile/android/tests/browser/chrome/chrome.ini
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -13,16 +13,17 @@ support-files =
 
 [test_about_logins.html]
 [test_accounts.html]
 [test_android_log.html]
 [test_app_constants.html]
 [test_debugger_server.html]
 [test_desktop_useragent.html]
 [test_device_search_engine.html]
+[test_fennecStorage.html]
 [test_get_last_visited.html]
 [test_home_provider.html]
 [test_java_addons.html]
 [test_jni.html]
 [test_migrate_ui.html]
 [test_network_manager.html]
 [test_offline_page.html]
 [test_reader_view.html]
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_fennecStorage.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1136477
+Migrated from Robocop: https://bugzilla.mozilla.org/show_bug.cgi?id=1184186
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1136477</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8" src="head.js"></script>
+  <script type="application/javascript;version=1.8">
+
+  "use strict";
+
+  const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+  const LoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1", "nsILoginInfo", "init");
+  const storage = Cc["@mozilla.org/login-manager/storage/fennecStorage;1"]
+                  .getService(Ci.nsILoginManagerStorage);
+
+  var testLogin1 = new LoginInfo("http://test.com", null, "http://test.com",
+                                "testLogin1", "testpass1", "u1", "p1");
+
+  var testLogin2 = new LoginInfo("http://test.org", "https://test.org", null,
+                                "testLogin2", "testpass2", "u2", "p2");
+
+  /**
+   * Checks that the two provided arrays of nsILoginInfo have the same length,
+   * and every login in "expected" is also found in "actual".  The comparison
+   * uses the "equals" method of nsILoginInfo, that does not include
+   * nsILoginMetaInfo properties in the test.
+   */
+  function assertLoginListsEqual(actual, expected) {
+    is(expected.length, actual.length, "loginlist length equals");
+    ok(expected.every(e => actual.some(a => a.equals(e))), "loginlist elements equals");
+  }
+
+  /**
+   * Checks that every login in "expected" matches one in "actual".
+   * The comparison uses the "matches" method of nsILoginInfo.
+   */
+  function assertLoginListsMatches(actual, expected, ignorePassword) {
+    is(expected.length, actual.length, "loginlist length equals");
+    ok(expected.every(e => actual.some(a => a.matches(e, ignorePassword))), "loginlist elements matches");
+  }
+
+  /**
+   * Checks that the two provided arrays of strings contain the same values,
+   * maybe in a different order, case-sensitively.
+   */
+  function assertDisabledHostsEqual(actual, expected) {
+    is(expected.length, actual.length, "disabledHosts length equals");
+    ok(expected.every(e => actual.some(a => (a === e))), "disabledHosts elements equals");
+  }
+
+  function checkStorageData(storage, ref_disabledHosts, ref_logins)
+  {
+    assertLoginListsEqual(storage.getAllLogins(), ref_logins);
+    assertDisabledHostsEqual(storage.getAllDisabledHosts(), ref_disabledHosts);
+  }
+
+  function retrieveAndVerifyLoginByProps(propName, propValue, actualLogin) {
+    var prop = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag2);
+    prop.setPropertyAsAUTF8String(propName, propValue);
+    let logins = storage.searchLogins({}, prop);
+    assertLoginListsEqual(logins, [actualLogin]);
+  }
+
+  function test_disabledHosts_Test() {
+    checkStorageData(storage, [], []);
+    // Disable saving logins to localhost and verify.
+    storage.setLoginSavingEnabled("localhost", false); 
+    ok(!storage.getLoginSavingEnabled("localhost"), "localhost is a disabled hostname");
+    checkStorageData(storage, ["localhost"], []);
+    // Enable saving logins to localhost and verify
+    storage.setLoginSavingEnabled("localhost", true); 
+    ok(storage.getLoginSavingEnabled("localhost"), "localhost is not a disabled hostname");
+    checkStorageData(storage, [], []);
+  }
+
+  function test_login_CRUD() {
+    // Create a new login and verify storage data.
+    storage.addLogin(testLogin1);
+    checkStorageData(storage, [], [testLogin1]);
+
+    // Add another login and verify all logins are retrieved.
+    storage.addLogin(testLogin2);
+    checkStorageData(storage, [], [testLogin1, testLogin2]);
+
+    // Verify login counts.
+    var count = storage.countLogins(testLogin2.hostname, testLogin2.formSubmitURL, testLogin2.httpRealm);
+    is(1, count, "countLogins is correct");
+    // Verify formSubmitUrl schema adjustment.
+    count = storage.countLogins(testLogin2.hostname, "http://test.org", testLogin2.httpRealm);
+    is(1, count, "countLogins is correct");
+    //count = storage.countLogins(testLogin2.hostname, "javascript://test.org", testLogin2.httpRealm);
+    //is(0, count, "countLogins is correct");
+    count = storage.countLogins(null, null, null);
+    is(0, count, "countLogins is correct");
+
+    // Search logins by properties test.
+    var logins = storage.findLogins({}, testLogin1.hostname, testLogin1.formSubmitURL, testLogin1.httpRealm);
+    assertLoginListsEqual(logins, [testLogin1]);
+
+    logins[0].QueryInterface(Ci.nsILoginMetaInfo);
+    retrieveAndVerifyLoginByProps("guid", logins[0].guid, testLogin1);
+    retrieveAndVerifyLoginByProps("encryptedUsername", testLogin1.username, testLogin1);
+    retrieveAndVerifyLoginByProps("encryptedPassword", testLogin1.password, testLogin1);
+    retrieveAndVerifyLoginByProps("usernameField", testLogin2.usernameField, testLogin2);
+    retrieveAndVerifyLoginByProps("passwordField", testLogin2.passwordField, testLogin2);
+
+    // Update logins and verify stored data.
+    testLogin1.password = "p3";
+    storage.modifyLogin(logins[0], testLogin1);
+    retrieveAndVerifyLoginByProps("encryptedPassword", "p3", testLogin1);
+
+    // Delete logins test.
+    storage.removeLogin(testLogin2);
+    checkStorageData(storage, [], [testLogin1]);
+    storage.removeAllLogins();
+    checkStorageData(storage, [], []);
+  };
+
+  test_disabledHosts_Test();
+  test_login_CRUD();
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=946857">Mozilla Bug 946857</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -44,17 +44,17 @@ EXTRA_JS_MODULES += [
     'LoginManagerContent.jsm',
     'LoginManagerParent.jsm',
     'LoginRecipes.jsm',
     'OSCrypto.jsm',
 ]
 
 if CONFIG['OS_TARGET'] == 'Android':
     EXTRA_COMPONENTS += [
-        'storage-mozStorage.js',
+        'storage-fennecStorage.js',
     ]
 else:
     EXTRA_COMPONENTS += [
         'storage-json.js',
     ]
     EXTRA_JS_MODULES += [
         'LoginImport.jsm',
         'LoginStore.jsm',
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -103,17 +103,17 @@ LoginManager.prototype = {
     }
 
     Services.obs.addObserver(this._observer, "gather-telemetry", false);
   },
 
 
   _initStorage : function () {
 #ifdef ANDROID
-    var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+    var contractID = "@mozilla.org/login-manager/storage/fennecStorage;1";
 #else
     var contractID = "@mozilla.org/login-manager/storage/json;1";
 #endif
     try {
       var catMan = Cc["@mozilla.org/categorymanager;1"].
                    getService(Ci.nsICategoryManager);
       contractID = catMan.getCategoryEntry("login-manager-storage",
                                            "nsILoginManagerStorage");
--- a/toolkit/components/passwordmgr/passwordmgr.manifest
+++ b/toolkit/components/passwordmgr/passwordmgr.manifest
@@ -2,16 +2,16 @@ component {cb9e0de8-3598-4ed7-857b-827f0
 contract @mozilla.org/login-manager;1 {cb9e0de8-3598-4ed7-857b-827f011ad5d8}
 component {749e62f4-60ae-4569-a8a2-de78b649660e} nsLoginManagerPrompter.js
 contract @mozilla.org/passwordmanager/authpromptfactory;1 {749e62f4-60ae-4569-a8a2-de78b649660e}
 component {8aa66d77-1bbb-45a6-991e-b8f47751c291} nsLoginManagerPrompter.js
 contract @mozilla.org/login-manager/prompter;1 {8aa66d77-1bbb-45a6-991e-b8f47751c291}
 component {0f2f347c-1e4f-40cc-8efd-792dea70a85e} nsLoginInfo.js
 contract @mozilla.org/login-manager/loginInfo;1 {0f2f347c-1e4f-40cc-8efd-792dea70a85e}
 #ifdef ANDROID
-component {8c2023b9-175c-477e-9761-44ae7b549756} storage-mozStorage.js
-contract @mozilla.org/login-manager/storage/mozStorage;1 {8c2023b9-175c-477e-9761-44ae7b549756}
+component {4859e221-74ca-48d2-9ad2-05248f3ec745} storage-fennecStorage.js
+contract @mozilla.org/login-manager/storage/fennecStorage;1 {4859e221-74ca-48d2-9ad2-05248f3ec745}
 #else
 component {c00c432d-a0c9-46d7-bef6-9c45b4d07341} storage-json.js
 contract @mozilla.org/login-manager/storage/json;1 {c00c432d-a0c9-46d7-bef6-9c45b4d07341}
 #endif
 component {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309} crypto-SDR.js
 contract @mozilla.org/login-manager/crypto/SDR;1 {dc6c2976-0f73-4f1f-b9ff-3d72b4e28309}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-fennecStorage.js
@@ -0,0 +1,824 @@
+/* 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+const DB_VERSION = 5; // The database schema version
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/ctypes.jsm")
+Components.utils.import("resource://gre/modules/JNI.jsm");
+Components.utils.import("resource://services-common/async.js");
+
+XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
+                                  "resource://gre/modules/LoginHelper.jsm");
+
+
+function LoginManagerStorage_fennecStorage() { };
+
+LoginManagerStorage_fennecStorage.prototype = {
+
+  classID : Components.ID("{4859e221-74ca-48d2-9ad2-05248f3ec745}"),
+  QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage,
+                                          Ci.nsIInterfaceRequestor]),
+
+
+  __uuidService: null,
+  get _uuidService() {
+    if (!this.__uuidService) {
+      this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
+                           getService(Ci.nsIUUIDGenerator);
+    }
+    return this.__uuidService;
+  },
+
+
+  // Common Java Class signatures.
+  _SIG: {
+    ContentValues: 'Landroid/content/ContentValues;',
+    Cursor: 'Landroid/database/Cursor;',
+    GeckoAppShell: 'Lorg/mozilla/gecko/GeckoAppShell;',
+    LoginsAccessor: 'Lorg/mozilla/gecko/db/LoginsAccessor;',
+    String: 'Ljava/lang/String;',
+    NULL: new ctypes.voidptr_t(null)
+  },
+
+
+  /*
+   * initialize
+   *
+   * Database initialization are done in Android, so return immediately.
+   */
+  initialize : function () {
+    return Promise.resolve();
+  },
+
+
+  /*
+   * terminate
+   *
+   * Internal method used by regression tests only.  It is called before
+   * replacing this storage module with a new instance.
+   */
+  terminate : function () {
+    return Promise.resolve();
+  },
+
+
+  /*
+   * addLogin
+   *
+   */
+  addLogin : function (login) {
+    // Throws if there are bogus values.
+    LoginHelper.checkLoginValues(login);
+
+    // Clone the login, so we don't modify the caller's object.
+    let loginClone = login.clone();
+
+    // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
+    loginClone.QueryInterface(Ci.nsILoginMetaInfo);
+    if (loginClone.guid && !this._isGuidUnique(loginClone.guid)) {
+      throw new Error("specified GUID already exists");
+    } else {
+      loginClone.guid = this._uuidService.generateUUID().toString();
+    }
+
+    // Set timestamps
+    let currentTime = Date.now();
+    if (!loginClone.timeCreated) {
+      loginClone.timeCreated = currentTime;
+    }
+    if (!loginClone.timeLastUsed) {
+      loginClone.timeLastUsed = currentTime;
+    }
+    if (!loginClone.timePasswordChanged) {
+      loginClone.timePasswordChanged = currentTime;
+    }
+    if (!loginClone.timesUsed) {
+      loginClone.timesUsed = 1;
+    }
+
+    this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        // Allocate a local reference frame with enough space (13 for the names and 13 for the values).
+        my_jenv.contents.contents.PushLocalFrame(my_jenv, 26);
+
+        let values = this._populateContentValues(my_jenv, loginClone);
+        loginsAccessor.addLogin(values);
+
+        // Pop local reference frame to clear all the local references.
+        my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+        // Send a notification that a login was added.
+        this._sendNotification("addLogin", loginClone);
+        resolve(null);
+      } catch (ex) {
+        this.log("addLogin JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * removeLogin
+   *
+   */
+  removeLogin : function (login) {
+    let [idToDelete, storedLogin] = this._getIdForLogin(login);
+    if (!idToDelete) {
+      throw new Error("No matching logins");
+    }
+
+    this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        loginsAccessor.removeLogin(idToDelete);
+
+        // Send a notification that a login was removed.
+        this._sendNotification("removeLogin", storedLogin);
+        resolve(null);
+      } catch (ex) {
+        this.log("removeLogin JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * modifyLogin
+   *
+   */
+  modifyLogin : function (oldLogin, newLoginData) {
+    let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
+    if (!idToModify) {
+      throw new Error("No matching logins");
+    }
+
+    let newLogin = LoginHelper.buildModifiedLogin(oldStoredLogin, newLoginData);
+
+    // Check if the new GUID is duplicate.
+    if (newLogin.guid != oldStoredLogin.guid &&
+        !this._isGuidUnique(newLogin.guid)) {
+      throw new Error("specified GUID already exists");
+    }
+
+    // Look for an existing entry in case key properties changed.
+    if (!newLogin.matches(oldLogin, true)) {
+      let logins = this.findLogins({}, newLogin.hostname,
+                                   newLogin.formSubmitURL,
+                                   newLogin.httpRealm);
+
+      if (logins.some(login => newLogin.matches(login, true))) {
+        throw new Error("This login already exists.");
+      }
+    }
+
+    this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        // Allocate a local reference frame with enough space (13 for the names and 13 for the values).
+        my_jenv.contents.contents.PushLocalFrame(my_jenv, 26);
+
+        let values = this._populateContentValues(my_jenv, newLogin);
+        loginsAccessor.modifyLogin(idToModify, values);
+
+        // Pop local reference frame to clear all the local references.
+        my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+        // Send a notification that a login was modified.
+        this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
+        resolve(null);
+      } catch (ex) {
+        this.log("modifyLogin JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * getAllLogins
+   *
+   * Returns an array of nsILoginInfo.
+   */
+  getAllLogins : function (count) {
+    let [logins, ids] = this._searchLogins({});
+    this.log("_getAllLogins: returning " + logins.length + " logins.");
+    if (count) {
+      count.value = logins.length; // needed for XPCOM
+    }
+    return logins;
+  },
+
+
+  /*
+   * searchLogins
+   *
+   * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+   * JavaScript object and decrypt the results.
+   *
+   * Returns an array of decrypted nsILoginInfo.
+   */
+  searchLogins : function(count, matchData) {
+    let realMatchData = {};
+    // Convert nsIPropertyBag to normal JS object
+    let propEnum = matchData.enumerator;
+    while (propEnum.hasMoreElements()) {
+      let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
+      realMatchData[prop.name] = prop.value;
+    }
+
+    let [logins, ids] = this._searchLogins(realMatchData);
+    count.value = logins.length; // needed for XPCOM
+    return logins;
+  },
+
+
+  /*
+   * removeAllLogins
+   *
+   * Removes all logins from storage.
+   */
+  removeAllLogins : function () {
+    this.log("Removing all logins");
+
+    // Disabled hosts kept, as one presumably doesn't want to erase those.
+    // TODO: Add these items to the deleted items table once we've sorted
+    //       out the issues from bug 756701
+    this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        loginsAccessor.removeAllLogins();
+        this._sendNotification("removeAllLogins", null);
+        resolve(null);
+      } catch (ex) {
+        this.log("removeAllLogins JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * getAllDisabledHosts
+   *
+   */
+  getAllDisabledHosts : function (count) {
+    let disabledHosts = this._queryDisabledHosts(null);
+
+    this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
+    if (count) {
+      count.value = disabledHosts.length; // needed for XPCOM
+    }
+    return disabledHosts;
+  },
+
+
+  /*
+   * getLoginSavingEnabled
+   *
+   */
+  getLoginSavingEnabled : function (hostname) {
+    this.log("Getting login saving is enabled for " + hostname);
+    return this._queryDisabledHosts(hostname).length == 0
+  },
+
+
+  /*
+   * setLoginSavingEnabled
+   *
+   */
+  setLoginSavingEnabled : function (hostname, enabled) {
+    // Throws if there are bogus values.
+    LoginHelper.checkHostnameValue(hostname);
+    this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        // Allocate a local reference frame with enough space (1 String read).
+        my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+
+        var jHostName = JNI.NewString(my_jenv, hostname);
+        loginsAccessor.setLoginSavingEnabled(jHostName, enabled);
+
+        // Pop local reference frame to clear all the local references.
+        my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+        this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
+        resolve(null);
+      } catch (ex) {
+        this.log("_setLoginSavingEnabled JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * findLogins
+   *
+   */
+  findLogins : function (count, hostname, formSubmitURL, httpRealm) {
+    let loginData = {
+      hostname: hostname,
+      formSubmitURL: formSubmitURL,
+      httpRealm: httpRealm
+    };
+
+    let matchData = { };
+    for (let field of ["hostname", "formSubmitURL", "httpRealm"]) {
+      if (loginData[field]) {
+        matchData[field] = loginData[field];
+      } else {
+        matchData[field] = null;
+      }
+    }
+
+    let [logins, ids] = this._searchLogins(matchData);
+    this.log("_findLogins: returning " + logins.length + " logins");
+    count.value = logins.length; // needed for XPCOM
+    return logins;
+  },
+
+
+  /*
+   * countLogins
+   *
+   */
+  countLogins : function (hostname, formSubmitURL, httpRealm) {
+    let resultLogins = this.findLogins({}, hostname, formSubmitURL, httpRealm);
+    if (resultLogins.length == 0 && formSubmitURL != null &&
+        formSubmitURL != "" && formSubmitURL != "javascript:") {
+      let formSubmitURI = Services.io.newURI(formSubmitURL, null, null);
+      let newScheme = null;
+      if (formSubmitURI.scheme == "http") {
+        newScheme = "https";
+      } else if (formSubmitURI.scheme == "https") {
+        newScheme = "http";
+      }
+      if (newScheme) {
+        let newFormSubmitURL = newScheme + "://" + formSubmitURI.hostPort;
+        resultLogins = this.findLogins({}, hostname, newFormSubmitURL, httpRealm);
+      }
+    }
+    this.log("_countLogins: counted logins: " + resultLogins.length);
+    return resultLogins.length;
+  },
+
+
+  /*
+   * uiBusy
+   */
+  get uiBusy() {
+    return false;
+  },
+
+
+  /*
+   * isLoggedIn
+   */
+  get isLoggedIn() {
+    return true;
+  },
+
+
+  /*
+   * _waitForSync
+   *
+   * Private method to wait for a promise to resolve synchronously.
+   *
+   * Returns resolved promise value or throw an error if the promised is rejected.
+   */
+  _waitForSync : function(promise) {
+    let cb = Async.makeSyncCallback();
+    Promise.resolve(promise).then(returnValue => cb.apply(null, returnValue ? returnValue : null)).catch(e => {
+      this.log("_waitForSync Promise failure:  " + e.name + " : " + e.message);
+      cb.throw(e);
+    });
+    return Async.waitForSyncCallback(cb);
+  },
+
+  _getLoginsAccessor : function(my_jenv) {
+    if (!my_jenv) {
+      throw new Error('my_jenv pointer is undefined');
+    }
+
+    JNI.LoadClass(my_jenv, this._SIG.Cursor.substr(1, this._SIG.Cursor.length - 2), {
+      methods: [
+        { name: 'moveToFirst', sig: '()Z' },
+        { name: 'moveToNext', sig: '()Z' },
+        { name: 'isAfterLast', sig: '()Z' },
+        { name: 'getColumnIndex', sig: '(' + this._SIG.String + ')I' },
+        { name: 'getString', sig: '(I)' + this._SIG.String },
+        { name: 'getLong', sig: '(I)J' },
+        { name: 'close', sig: '()V' },
+      ]
+    });
+
+    JNI.LoadClass(my_jenv, this._SIG.ContentValues.substr(1, this._SIG.ContentValues.length - 2), {
+      constructors: [{
+        name: "<init>",
+        sig: "()V"
+      }],
+      methods: [
+        { name: 'put', sig: '(' + this._SIG.String + this._SIG.String + ')V' },
+        { name: 'toString', sig: '()' + this._SIG.String },
+        // { name: 'put', sig: '(' + this._SIG.String + 'J)V' },
+      ]
+    });
+
+    JNI.LoadClass(my_jenv, this._SIG.LoginsAccessor.substr(1, this._SIG.LoginsAccessor.length - 2), {
+      methods: [
+        { name: 'searchLogins',
+          sig: '(' +
+               this._SIG.String +
+               '[' + this._SIG.String +
+               ')' +
+               this._SIG.Cursor
+        },
+        { name: 'getAllDisabledHosts', sig: '()' + this._SIG.Cursor },
+        { name: 'addLogin', sig: '(' + this._SIG.ContentValues + ')V' },
+        { name: 'removeLogin', sig: '(J)V' },
+        { name: 'modifyLogin', sig: '(J'+ this._SIG.ContentValues + ')V' },
+        { name: 'removeAllLogins', sig: '()V' },
+        { name: 'getLoginsSavedEnabled', sig: '(' + this._SIG.String + ')' + this._SIG.Cursor },
+        { name: 'setLoginSavingEnabled', sig: '(' + this._SIG.String + 'Z)V' },
+      ]
+    });
+
+    var geckoAppShell = JNI.LoadClass(my_jenv, this._SIG.GeckoAppShell.substr(1, this._SIG.GeckoAppShell.length - 2), {
+      static_methods: [
+        { name: 'getLoginsAccessor', sig: '()' + this._SIG.LoginsAccessor }
+      ]
+    });
+
+    return geckoAppShell.getLoginsAccessor();
+  },
+
+
+  _populateContentValues : function(my_jenv, loginClone) {
+      if (!my_jenv) {
+        throw new Error('my_jenv pointer is undefined');
+      }
+
+      let contentValues = JNI.classes.android.content.ContentValues["new"]();
+      // Test for valid Java string for text columns.
+      var jHostName = loginClone.hostname ? JNI.NewString(my_jenv, loginClone.hostname) : this._SIG.NULL;
+      var jHttpRealm = loginClone.httpRealm ? JNI.NewString(my_jenv, loginClone.httpRealm) : this._SIG.NULL;
+      var jFormSubmitURL = loginClone.formSubmitURL ? JNI.NewString(my_jenv, loginClone.formSubmitURL) : this._SIG.NULL;
+      var jUserNameField = loginClone.usernameField ? JNI.NewString(my_jenv, loginClone.usernameField) : this._SIG.NULL;
+      var jPasswordField = loginClone.passwordField ? JNI.NewString(my_jenv, loginClone.passwordField) : this._SIG.NULL;
+      var jUserName = loginClone.username ? JNI.NewString(my_jenv, loginClone.username) : this._SIG.NULL;
+      var jPassword = loginClone.password ? JNI.NewString(my_jenv, loginClone.password) : this._SIG.NULL;
+
+      // GUID is generated in LoginsProvider if null or empty.
+      var jGUID = loginClone.guid ? JNI.NewString(my_jenv, loginClone.guid) : this._SIG.NULL;
+      var jEncType = JNI.NewString(my_jenv, loginClone.encType ? loginClone.encType : "0");
+
+      // Following is a ugly conversion of long to String but this avoid overloaded put method in ContentValues with long to Long conversion.
+      var jTimeCreated = loginClone.timeCreated ? JNI.NewString(my_jenv, "" + loginClone.timeCreated) : this._SIG.NULL;
+      var jTimeLastUsed = loginClone.timeLastUsed ? JNI.NewString(my_jenv, "" + loginClone.timeLastUsed) : this._SIG.NULL;
+      var jTimePasswordChanged = loginClone.timePasswordChanged ? JNI.NewString(my_jenv, "" + loginClone.timePasswordChanged) : this._SIG.NULL;
+      var jTimesUsed =  JNI.NewString(my_jenv, loginClone.timesUsed ?  "" + loginClone.timesUsed : "0");
+
+      // Keys have to be kept in sync with BrowserContract$Logins class.
+      contentValues.put(JNI.NewString(my_jenv, "hostname"), jHostName);
+      contentValues.put(JNI.NewString(my_jenv, "httpRealm"), jHttpRealm);
+      contentValues.put(JNI.NewString(my_jenv, "formSubmitURL"), jFormSubmitURL);
+      contentValues.put(JNI.NewString(my_jenv, "usernameField"), jUserNameField);
+      contentValues.put(JNI.NewString(my_jenv, "passwordField"), jPasswordField);
+      contentValues.put(JNI.NewString(my_jenv, "encryptedUsername"), jUserName);
+      contentValues.put(JNI.NewString(my_jenv, "encryptedPassword"), jPassword);
+      contentValues.put(JNI.NewString(my_jenv, "guid"), jGUID);
+      contentValues.put(JNI.NewString(my_jenv, "encType"), jEncType);
+      contentValues.put(JNI.NewString(my_jenv, "timeCreated"), jTimeCreated);
+      contentValues.put(JNI.NewString(my_jenv, "timeLastUsed"), jTimeLastUsed);
+      contentValues.put(JNI.NewString(my_jenv, "timePasswordChanged"), jTimePasswordChanged);
+      contentValues.put(JNI.NewString(my_jenv, "timesUsed"), jTimesUsed);
+
+      return contentValues;
+  },
+
+
+  /*
+   * _searchLogins
+   *
+   * Private method to perform arbitrary searches on any field.
+   *
+   * Returns [logins, ids] for logins that match the arguments, where logins
+   * is an array of encrypted nsLoginInfo and ids is an array of associated
+   * ids in the database.
+   */
+  _searchLogins : function(matchData) {
+    let selectionQuery = [], selectionParams = [];
+
+    for (let field in matchData) {
+      let value = matchData[field];
+      switch (field) {
+        // Historical compatibility requires this special case
+        case "formSubmitURL":
+          if (value) {
+              // As we also need to check for different schemes at the URI
+              // this case gets handled by filtering the result of the query.
+              break;
+          }
+        // Android CP id to _id mapping.
+        case "id":
+          if (value) {
+            selectionQuery.push("_id = ?");
+            selectionParams.push(value);
+          }
+          break;
+        // Normal cases.
+        case "hostname":
+        case "httpRealm":
+        case "usernameField":
+        case "passwordField":
+        case "encryptedUsername":
+        case "encryptedPassword":
+        case "guid":
+        case "encType":
+        case "timeCreated":
+        case "timeLastUsed":
+        case "timePasswordChanged":
+        case "timesUsed":
+          if (value) {
+            selectionQuery.push(field + " = ?");
+            selectionParams.push(value);
+          } else {
+            selectionQuery.push(field + " is null");
+          }
+          break;
+        // Fail if caller requests an unknown property.
+        default:
+          throw new Error("Unexpected field: " + field);
+      }
+    }
+
+    return this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        JNI.LoadClass(my_jenv, '[' + this._SIG.String);
+        let StringArray = JNI.classes.java.lang.String.array;
+
+        // Allocate a local reference frame with enough space (2 for the String and StringArray).
+        my_jenv.contents.contents.PushLocalFrame(my_jenv, 2);
+
+        let selectionArgs = selectionParams.length ? StringArray.new(selectionParams.length) : this._SIG.NULL;
+        if (selectionParams.length) {
+          selectionArgs.setElements(0, selectionParams);
+        }
+
+        let selection = selectionQuery.length ? JNI.NewString(my_jenv, selectionQuery.join(" AND ")) : this._SIG.NULL;
+
+        let cursor = loginsAccessor.searchLogins(selection, selectionArgs);
+
+        let logins = [], ids = [], fallbackLogins = [], fallbackIds = [];
+        if (!cursor) {
+          // Defend against null cursor.
+          resolve([[logins, ids]]);
+          return;
+        }
+
+        try {
+          cursor.moveToFirst();
+          while (!cursor.isAfterLast()) {
+            // Allocate a local reference frame with enough space (8 String read).
+            my_jenv.contents.contents.PushLocalFrame(my_jenv, 8);
+
+            // Create the new nsLoginInfo object, push to array
+            let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+                        createInstance(Ci.nsILoginInfo);
+            login.init(JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("hostname"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("formSubmitURL"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("httpRealm"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("encryptedUsername"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("encryptedPassword"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("usernameField"))),
+                       JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("passwordField"))));
+            // set nsILoginMetaInfo values
+            login.QueryInterface(Ci.nsILoginMetaInfo);
+            login.guid = JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("guid")));
+            login.timeCreated = cursor.getLong(cursor.getColumnIndex("timeCreated"));
+            login.timeLastUsed = cursor.getLong(cursor.getColumnIndex("timeLastUsed"));
+            login.timePasswordChanged = cursor.getLong(cursor.getColumnIndex("timePasswordChanged"));
+            login.timesUsed = cursor.getLong(cursor.getColumnIndex("timesUsed"));
+
+            if (login.formSubmitURL == "" || typeof(matchData.formSubmitURL) == "undefined" ||
+                login.formSubmitURL == matchData.formSubmitURL) {
+                logins.push(login);
+                ids.push(cursor.getLong(cursor.getColumnIndex("_id")));
+            } else if (login.formSubmitURL != null &&
+                       login.formSubmitURL != "javascript:" &&
+                       matchData.formSubmitURL != "javascript:") {
+              let loginURI = Services.io.newURI(login.formSubmitURL, null, null);
+              let matchURI = Services.io.newURI(matchData.formSubmitURL, null, null);
+
+              if (loginURI.hostPort == matchURI.hostPort &&
+                  ((loginURI.scheme == "http" && matchURI.scheme == "https") ||
+                  (loginURI.scheme == "https" && matchURI.scheme == "http"))) {
+                fallbackLogins.push(login);
+                fallbackIds.push(cursor.getLong(cursor.getColumnIndex("_id")));
+              }
+            }
+            cursor.moveToNext();
+            // Pop local reference frame to clear all the local references.
+            my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+          }
+        } finally {
+          // Close the cursor.
+          cursor.close();
+        }
+
+        if (!logins.length && fallbackLogins.length) {
+          this.log("_searchLogins: returning " + fallbackLogins.length + " fallback logins");
+          resolve([[fallbackLogins, fallbackIds]]);
+        }
+        this.log("_searchLogins: returning " + logins.length + " logins");
+
+        // Pop local reference frame to clear all the local references.
+        my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+        resolve([[logins, ids]]);
+      } catch (ex) {
+        this.log("_searchLogins JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * _sendNotification
+   *
+   * Send a notification when stored data is changed.
+   */
+  _sendNotification : function (changeType, data) {
+    let dataObject = data;
+    // Can't pass a raw JS string or array though notifyObservers(). :-(
+    if (data instanceof Array) {
+      dataObject = Cc["@mozilla.org/array;1"].
+                   createInstance(Ci.nsIMutableArray);
+      for (let i = 0; i < data.length; i++)
+        dataObject.appendElement(data[i], false);
+    } else if (typeof(data) == "string") {
+      dataObject = Cc["@mozilla.org/supports-string;1"].
+                   createInstance(Ci.nsISupportsString);
+      dataObject.data = data;
+    }
+    Services.obs.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
+  },
+
+
+  /*
+   * _getIdForLogin
+   *
+   * Returns an array with two items: [id, login]. If the login was not
+   * found, both items will be null. The returned login contains the actual
+   * stored login (useful for looking at the actual nsILoginMetaInfo values).
+   */
+  _getIdForLogin : function (login) {
+    let matchData = { };
+    for (let field of ["hostname", "formSubmitURL", "httpRealm"])
+      if (login[field]) {
+        matchData[field] = login[field];
+      }
+    let [logins, ids] = this._searchLogins(matchData);
+
+    let id = null;
+    let foundLogin = null;
+
+    for (let i = 0; i < logins.length; i++) {
+      if (!logins[i].equals(login)) {
+        continue;
+      }
+
+      // We've found a match, set id and break
+      foundLogin = logins[i];
+      id = ids[i];
+      break;
+    }
+
+    return [id, foundLogin];
+  },
+
+
+  /*
+   * _queryDisabledHosts
+   *
+   * Returns an array of hostnames from the database according to the
+   * criteria given in the argument. If the argument hostname is null, the
+   * result array contains all hostnames
+   */
+  _queryDisabledHosts : function (hostname) {
+    return this._waitForSync(new Promise((resolve, reject) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        // Allocate a local reference frame with enough space (1 for the hostname).
+        my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+        let jHostName = hostname ? JNI.NewString(my_jenv, hostname) : this._SIG.NULL
+        let cursor = loginsAccessor.getLoginsSavedEnabled(jHostName);
+        let disabledHosts = [];
+
+        if (!cursor) {
+          // Defend against null cursor.
+          resolve([disabledHosts]);
+          return;
+        }
+
+        try {
+          cursor.moveToFirst();
+          while (!cursor.isAfterLast()) {
+            // Allocate a local reference frame with enough space (1 String read).
+            my_jenv.contents.contents.PushLocalFrame(my_jenv, 1);
+
+            disabledHosts.push(JNI.ReadString(my_jenv, cursor.getString(cursor.getColumnIndex("hostname"))));
+            cursor.moveToNext();
+
+            // Pop local reference frame to clear all the local references.
+            my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+          }
+        } finally {
+          // Close the cursor.
+          cursor.close();
+        }
+
+        // Pop local reference frame to clear all the local references.
+        my_jenv.contents.contents.PopLocalFrame(my_jenv, null);
+
+        resolve([disabledHosts]);
+      } catch (ex) {
+        this.log("_queryDisabledHosts JNI failure:  " + ex.name + " : " + ex.message);
+        reject(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    }));
+  },
+
+
+  /*
+   * _isGuidUnique
+   *
+   * Checks to see if the specified GUID already exists.
+   */
+  _isGuidUnique : function (guid) {
+    let [logins, ids] = this._searchLogins({"guid" : guid});
+    return ids.length == 0;
+  }
+}; // end of nsLoginManagerStorage_fennecStorage implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_fennecStorage.prototype, "log", () => {
+  let logger = LoginHelper.createLogger("Login storage");
+  return logger.log.bind(logger);
+});
+
+var component = [LoginManagerStorage_fennecStorage];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
\ No newline at end of file