Bug 946857 - part 2: JNI based password manager storage r?nalexander draft
authorvivek <vivekb.balakrishnan@gmail.com>
Wed, 23 Dec 2015 23:06:27 +0200
changeset 317409 e40bfdb891d7ecc59169425182902febe66c5c5c
parent 317408 c9a846a5259be719bac38fb7b9b461a1014f5854
child 512291 993a3dd6521b9bc688d4f418cd36ba31d75b3232
push id8691
push userbmo:vivekb.balakrishnan@gmail.com
push dateWed, 23 Dec 2015 21:07:27 +0000
reviewersnalexander
bugs946857
milestone46.0a1
Bug 946857 - part 2: JNI based password manager storage r?nalexander Patch changes: Exposed an accessor for LoginsProvider New passwordmgr storage that uses JNI to store data through LoginsProvider CP Yet to do: Test toolkit/components/passwordmgr changes Use LoginsProvider with Sync Password RepoSessions Improve LoginsProvider test
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
toolkit/components/passwordmgr/moz.build
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,101 @@
+/* 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 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 boolean getLoginsSavedEnabled(String hostname) {
+        Cursor cursor = null;
+        try {
+            cursor = getContext().getContentResolver().query(disabledHostsUriWithProfile, null,
+                    DisabledHosts.HOSTNAME + "=?", new String[]{hostname}, null);
+            return cursor.getCount() > 0;
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    @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,31 @@
+/* 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;
+
+@JNITarget
+public interface LoginsAccessor {
+    void addLogin(ContentValues values);
+
+    void removeLogin(long id);
+
+    void modifyLogin(long id, ContentValues values);
+
+    void removeAllLogins();
+
+    int countLogins();
+
+    Cursor searchLogins(String selection, String[] selectionArgs);
+
+    Cursor getAllDisabledHosts();
+
+    boolean getLoginsSavedEnabled(String hostname);
+
+    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 boolean getLoginsSavedEnabled(String hostname) {
+        return false;
+    }
+
+    @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
@@ -335,17 +335,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/nsHelperAppDlg.manifest
 @BINPATH@/components/nsHelperAppDlg.js
 @BINPATH@/components/NetworkGeolocationProvider.manifest
 @BINPATH@/components/NetworkGeolocationProvider.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/extensions.manifest
--- a/toolkit/components/passwordmgr/moz.build
+++ b/toolkit/components/passwordmgr/moz.build
@@ -47,17 +47,17 @@ EXTRA_JS_MODULES += [
     'LoginHelper.jsm',
     'LoginManagerContent.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/passwordmgr.manifest
+++ b/toolkit/components/passwordmgr/passwordmgr.manifest
@@ -2,17 +2,17 @@ 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/mozStorage;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}
 category healthreport-js-provider-default PasswordsMetricsProvider resource://gre/modules/LoginManagerParent.jsm
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/storage-fennecStorage.js
@@ -0,0 +1,827 @@
+/* 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_mozStorage() { };
+
+LoginManagerStorage_mozStorage.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) {
+    let _addLoginHelper = (login, aCallback) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        let values = this._populateContentValues(my_jenv, login);
+        Promise.resolve(loginsAccessor.addLogin(values))
+               .then(() => aCallback.apply(null, null))
+               .catch(e => {
+                  this.log("_addLoginHelper Promise failure:  " + e.name + " : " + e.message)
+                  aCallback.throw(e);
+                });
+      } catch (ex) {
+        this.log("_addLoginHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    // 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) {
+      if (!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;
+
+    let cb = Async.makeSyncCallback();
+    _addLoginHelper(loginClone, cb);
+    Async.waitForSyncCallback(cb);
+
+    // Send a notification that a login was added.
+    this._sendNotification("addLogin", loginClone);
+  },
+
+
+  /*
+   * removeLogin
+   *
+   */
+  removeLogin : function (login) {
+    let _removeLoginsHelper = (id, aCallback) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        Promise.resolve(loginsAccessor.removeLogin(id))
+               .then(() => aCallback.apply(null, null))
+               .catch(e => {
+                  this.log("_removeLoginsHelper Promise failure:  " + e.name + " : " + e.message)
+                  aCallback.throw(e);
+                });
+      } catch (ex) {
+        this.log("_removeLoginsHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    let [idToDelete, storedLogin] = this._getIdForLogin(login);
+    if (!idToDelete)
+      throw new Error("No matching logins");
+
+    let cb = Async.makeSyncCallback();
+    _removeLoginsHelper(idToDelete, cb);
+    Async.waitForSyncCallback(cb);
+
+    // Send a notification that a login was removed.
+    this._sendNotification("removeLogin", storedLogin);
+  },
+
+
+  /*
+   * modifyLogin
+   *
+   */
+  modifyLogin : function (oldLogin, newLoginData) {
+    let _modifyLoginHelper = (login, aCallback) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        let values = this._populateContentValues(my_jenv, login);
+        Promise.resolve(loginsAccessor.modifyLogin(idToModify, values))
+               .then(() => aCallback.apply(null, null))
+               .catch(e => {
+                  this.log("_modifyLoginHelper Promise failure:  " + e.name + " : " + e.message)
+                  aCallback.throw(e);
+                });
+      } catch (ex) {
+        this.log("_modifyLoginHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    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.");
+    }
+
+    let cb = Async.makeSyncCallback();
+    _modifyLoginHelper(newLogin, cb);
+    Async.waitForSyncCallback(cb);
+
+    // Send a notification that a login was modified.
+    this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
+  },
+
+
+  /*
+   * 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");
+    let _removeAllLoginsHelper = (aCallback) => {
+      var my_jenv;
+
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        Promise.resolve(loginsAccessor.removeAllLogin())
+               .then(() => aCallback.apply(null, null))
+               .catch(e => {
+                  this.log("_removeAllLoginsHelper Promise failure:  " + e.name + " : " + e.message)
+                  aCallback.throw(e);
+                });
+      } catch (ex) {
+        this.log("_removeAllLoginsHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    // 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
+    let cb = Async.makeSyncCallback();
+    _removeAllLoginsHelper(cb);
+    Async.waitForSyncCallback(cb);
+
+    this._sendNotification("removeAllLogins", null);
+  },
+
+
+  /*
+   * 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);
+
+    let _setLoginSavingEnabledHelper = (hostname, enabled, aCallback) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        var jHostName = JNI.NewString(my_jenv, hostname);
+        Promise.resolve(loginsAccessor.setLoginSavingEnabled(jHostName, enabled))
+               .then(() => aCallback.apply(null, null))
+               .catch(e => {
+                  this.log("_setLoginSavingEnabledHelper Promise failure:  " + e.name + " : " + e.message)
+                  aCallback.throw(e);
+                });
+      } catch (ex) {
+        this.log("_setLoginSavingEnabledHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    this.log("Setting login saving enabled for " + hostname + " to " + enabled);
+    let cb = Async.makeSyncCallback();
+    _setLoginSavingEnabledHelper(hostname, enabled, cb);
+    Async.waitForSyncCallback(cb);
+
+    this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
+  },
+
+
+  /*
+   * 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];
+    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;
+  },
+
+
+  _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 + ')Z' },
+        { 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"]();
+      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;
+      // Initialize with empty string for mandatory fields.
+      var jUserNameField = JNI.NewString(my_jenv, loginClone.usernameField ? loginClone.usernameField : "");
+      var jPasswordField = JNI.NewString(my_jenv, loginClone.passwordField ? loginClone.passwordField : "");
+      var jUserName = JNI.NewString(my_jenv, loginClone.username ? loginClone.username : "");
+      var jPassword = JNI.NewString(my_jenv, loginClone.password ? loginClone.password : "");
+      // GUID is generated in LoginsProvider if null.
+      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 = loginClone.timesUsed ? JNI.NewString(my_jenv, "" + loginClone.timesUsed) : this._SIG.NULL;
+
+      // 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 != null) {
+              // 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 == null) {
+              throw new Error("id cannot be null");
+          }
+
+          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 == null) {
+              selectionQuery.push(field + " is null")
+          } else {
+              selectionQuery.push(field + " = ?");
+              selectionParams.push(value);
+          }
+          break;
+        // Fail if caller requests an unknown property.
+        default:
+          throw new Error("Unexpected field: " + field);
+      }
+    }
+
+    let cb = Async.makeSyncCallback();
+    this._searchLoginsHelper(matchData, selectionQuery, selectionParams, cb);
+    return Async.waitForSyncCallback(cb);
+  },
+
+
+  _searchLoginsHelper : function(matchData, selectionQuery, selectionParams, aCallback) {
+    var my_jenv;
+    var selection;
+    var selectionArgs;
+    var loginsAccessor;
+
+    try {
+      my_jenv = JNI.GetForThread();
+      loginsAccessor = this._getLoginsAccessor(my_jenv);
+      JNI.LoadClass(my_jenv, '[' + this._SIG.String);
+      let StringArray = JNI.classes.java.lang.String.array;
+
+      selectionArgs = selectionParams.length ? StringArray.new(selectionParams.length) : this._SIG.NULL;
+      if (selectionParams.length) {
+        selectionArgs.setElements(0, selectionParams);
+      }
+
+      selection = selectionQuery.length ? JNI.NewString(my_jenv, selectionQuery.join(" AND ")) : this._SIG.NULL;
+
+      Promise.resolve(loginsAccessor.searchLogins(selection, selectionArgs)).then((cursor) => {
+        let logins = [], ids = [], fallbackLogins = [], fallbackIds = [];
+
+        if (!cursor) {
+          // Defend against null cursor.
+          aCallback.apply(null, [[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("_searchLoginsHelper: returning " + fallbackLogins.length + " fallback logins");
+          aCallback.apply(null, [[fallbackLogins, fallbackIds]]);
+        }
+        this.log("_searchLoginsHelper: returning " + logins.length + " logins");
+        aCallback.apply(null, [[logins, ids]]);
+      }).catch(e => {
+        this.log("_searchLoginsHelper Promise failure:  " + e.name + " : " + e.message)
+        aCallback.throw(e);
+      });
+    } catch (ex) {
+      this.log("_searchLoginsHelper JNI failure:  " + ex.name + " : " + ex.message);
+      aCallback.throw(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) {
+    let _queryDisabledHostsHelper = (hostname, aCallback) => {
+      var my_jenv;
+      try {
+        my_jenv = JNI.GetForThread();
+        let loginsAccessor = this._getLoginsAccessor(my_jenv);
+        var jHostName = hostname ? JNI.NewString(my_jenv, hostname) : this._SIG.NULL
+        Promise.resolve(loginsAccessor.getLoginsSavedEnabled(jHostName)).then((cursor) => {
+            let disabledHosts = [];
+
+            if (!cursor) {
+              // Defend against null cursor.
+              aCallback.apply(null, [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();
+          }
+
+          aCallback.apply(null, [disabledHosts]);
+        }).catch(e => {
+          this.log("_queryDisabledHostsHelper Promise failure:  " + e.name + " : " + e.message)
+          aCallback.throw(e);
+        });
+      } catch (ex) {
+        this.log("_queryDisabledHostsHelper JNI failure:  " + ex.name + " : " + ex.message);
+        aCallback.throw(ex);
+      } finally {
+        if (my_jenv) {
+          JNI.UnloadClasses(my_jenv);
+        }
+      }
+    };
+
+    let cb = Async.makeSyncCallback();
+    _queryDisabledHostsHelper(hostname, cb);
+    return Async.waitForSyncCallback(cb);
+  },
+
+
+  /*
+   * _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_mozStorage implementation
+
+XPCOMUtils.defineLazyGetter(this.LoginManagerStorage_mozStorage.prototype, "log", () => {
+  let logger = LoginHelper.createLogger("Login storage");
+  return logger.log.bind(logger);
+});
+
+var component = [LoginManagerStorage_mozStorage];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
\ No newline at end of file