summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java1445
1 files changed, 1445 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java
new file mode 100644
index 0000000000..48ef71b6d6
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java
@@ -0,0 +1,1445 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * vim: ts=4 sw=4 expandtab:
+ * 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.geckoview;
+
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+
+/**
+ * The Autocomplete API provides a way to leverage Gecko's input form handling for autocompletion.
+ *
+ * <p>The API is split into two parts: 1. Storage-level delegates. 2. User-prompt delegates.
+ *
+ * <p>The storage-level delegates connect Gecko mechanics to the app's storage, e.g., retrieving and
+ * storing of login entries.
+ *
+ * <p>The user-prompt delegates propagate decisions to the app that could require user choice, e.g.,
+ * saving or updating of login entries or the selection of a login entry out of multiple options.
+ *
+ * <p>Throughout the documentation, we will refer to the filling out of input forms using two terms:
+ * 1. Autofill: automatic filling without user interaction. 2. Autocomplete: semi-automatic filling
+ * that requires user prompting for the selection.
+ *
+ * <h2>Examples</h2>
+ *
+ * <h3>Autocomplete/Fetch API</h3>
+ *
+ * <p>GeckoView loads <code>https://example.com</code> which contains (for the purpose of this
+ * example) elements resembling a login form, e.g.,
+ *
+ * <pre><code>
+ * &lt;form&gt;
+ * &lt;input type=&quot;text&quot; placeholder=&quot;username&quot;&gt;
+ * &lt;input type=&quot;password&quot; placeholder=&quot;password&quot;&gt;
+ * &lt;input type=&quot;submit&quot; value=&quot;submit&quot;&gt;
+ * &lt;/form&gt;
+ * </code></pre>
+ *
+ * <p>With the document parsed and the login input fields identified, GeckoView dispatches a <code>
+ * StorageDelegate.onLoginFetch(&quot;example.com&quot;)</code> request to fetch logins for the
+ * given domain.
+ *
+ * <p>Based on the provided login entries, GeckoView will attempt to autofill the login input
+ * fields, if there is only one suitable login entry option.
+ *
+ * <p>In the case of multiple valid login entry options, GeckoView dispatches a <code>
+ * GeckoSession.PromptDelegate.onLoginSelect</code> request, which allows for user-choice
+ * delegation.
+ *
+ * <p>Based on the returned login entries, GeckoView will attempt to autofill/autocomplete the login
+ * input fields.
+ *
+ * <h3>Update API</h3>
+ *
+ * <p>When the user submits some login input fields, GeckoView dispatches another <code>
+ * StorageDelegate.onLoginFetch(&quot;example.com&quot;)</code> request to check whether the
+ * submitted login exists or whether it's a new or updated login entry.
+ *
+ * <p>If the submitted login is already contained as-is in the collection returned by <code>
+ * onLoginFetch</code>, then GeckoView dispatches <code>StorageDelegate.onLoginUsed</code> with the
+ * submitted login entry.
+ *
+ * <p>If the submitted login is a new or updated entry, GeckoView dispatches a sequence of requests
+ * to save/update the login entry, see the Save API example.
+ *
+ * <h3>Save API</h3>
+ *
+ * <p>The user enters new or updated (password) login credentials in some login input fields and
+ * submits explicitely (submit action) or by navigation. GeckoView identifies the entered
+ * credentials and dispatches a <code>GeckoSession.PromptDelegate.onLoginSave(session, request)
+ * </code> with the provided credentials.
+ *
+ * <p>The app may dismiss the prompt request via <code>
+ * return GeckoResult.fromValue(prompt.dismiss())</code> which terminates this saving request, or
+ * confirm it via <code>return GeckoResult.fromValue(prompt.confirm(login))</code> where <code>login
+ * </code> either holds the credentials originally provided by the prompt request (<code>
+ * prompt.logins[0]</code>) or a new or modified login entry.
+ *
+ * <p>The login entry returned in a confirmed save prompt is used to request for saving in the
+ * runtime delegate via <code>StorageDelegate.onLoginSave(login)</code>. If the app has already
+ * stored the entry during the prompt request handling, it may ignore this storage saving request.
+ * <br>
+ *
+ * @see GeckoRuntime#setAutocompleteStorageDelegate <br>
+ * @see GeckoSession#setPromptDelegate <br>
+ * @see GeckoSession.PromptDelegate#onLoginSave <br>
+ * @see GeckoSession.PromptDelegate#onLoginSelect
+ */
+public class Autocomplete {
+ private static final String LOGTAG = "Autocomplete";
+ private static final boolean DEBUG = false;
+
+ protected Autocomplete() {}
+
+ /** Holds credit card information for a specific entry. */
+ public static class CreditCard {
+ private static final String GUID_KEY = "guid";
+ private static final String NAME_KEY = "name";
+ private static final String NUMBER_KEY = "number";
+ private static final String EXP_MONTH_KEY = "expMonth";
+ private static final String EXP_YEAR_KEY = "expYear";
+
+ /** The unique identifier for this login entry. */
+ public final @Nullable String guid;
+
+ /** The full name as it appears on the credit card. */
+ public final @NonNull String name;
+
+ /** The credit card number. */
+ public final @NonNull String number;
+
+ /** The expiration month. */
+ public final @NonNull String expirationMonth;
+
+ /** The expiration year. */
+ public final @NonNull String expirationYear;
+
+ // For tests only.
+ @AnyThread
+ protected CreditCard() {
+ guid = null;
+ name = "";
+ number = "";
+ expirationMonth = "";
+ expirationYear = "";
+ }
+
+ @AnyThread
+ /* package */ CreditCard(final @NonNull GeckoBundle bundle) {
+ guid = bundle.getString(GUID_KEY);
+ name = bundle.getString(NAME_KEY, "");
+ number = bundle.getString(NUMBER_KEY, "");
+ expirationMonth = bundle.getString(EXP_MONTH_KEY, "");
+ expirationYear = bundle.getString(EXP_YEAR_KEY, "");
+ }
+
+ @Override
+ @AnyThread
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("CreditCard {");
+ builder
+ .append("guid=")
+ .append(guid)
+ .append(", name=")
+ .append(name)
+ .append(", number=")
+ .append(number)
+ .append(", expirationMonth=")
+ .append(expirationMonth)
+ .append(", expirationYear=")
+ .append(expirationYear)
+ .append("}");
+ return builder.toString();
+ }
+
+ @AnyThread
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(7);
+ bundle.putString(GUID_KEY, guid);
+ bundle.putString(NAME_KEY, name);
+ bundle.putString(NUMBER_KEY, number);
+ if (expirationMonth != null) {
+ bundle.putString(EXP_MONTH_KEY, expirationMonth);
+ }
+ if (expirationYear != null) {
+ bundle.putString(EXP_YEAR_KEY, expirationYear);
+ }
+
+ return bundle;
+ }
+
+ public static class Builder {
+ private final GeckoBundle mBundle;
+
+ @AnyThread
+ /* package */ Builder(final @NonNull GeckoBundle bundle) {
+ mBundle = new GeckoBundle(bundle);
+ }
+
+ @AnyThread
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public Builder() {
+ mBundle = new GeckoBundle(7);
+ }
+
+ /**
+ * Finalize the {@link CreditCard} instance.
+ *
+ * @return The {@link CreditCard} instance.
+ */
+ @AnyThread
+ public @NonNull CreditCard build() {
+ return new CreditCard(mBundle);
+ }
+
+ /**
+ * Set the unique identifier for this credit card entry.
+ *
+ * @param guid The unique identifier string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder guid(final @Nullable String guid) {
+ mBundle.putString(GUID_KEY, guid);
+ return this;
+ }
+
+ /**
+ * Set the name for this credit card entry.
+ *
+ * @param name The full name as it appears on the credit card.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder name(final @Nullable String name) {
+ mBundle.putString(NAME_KEY, name);
+ return this;
+ }
+
+ /**
+ * Set the number for this credit card entry.
+ *
+ * @param number The credit card number string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder number(final @Nullable String number) {
+ mBundle.putString(NUMBER_KEY, number);
+ return this;
+ }
+
+ /**
+ * Set the expiration month for this credit card entry.
+ *
+ * @param expMonth The expiration month string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder expirationMonth(final @Nullable String expMonth) {
+ mBundle.putString(EXP_MONTH_KEY, expMonth);
+ return this;
+ }
+
+ /**
+ * Set the expiration year for this credit card entry.
+ *
+ * @param expYear The expiration year string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder expirationYear(final @Nullable String expYear) {
+ mBundle.putString(EXP_YEAR_KEY, expYear);
+ return this;
+ }
+ }
+ }
+
+ /** Holds address information for a specific entry. */
+ public static class Address {
+ private static final String GUID_KEY = "guid";
+ private static final String NAME_KEY = "name";
+ private static final String GIVEN_NAME_KEY = "givenName";
+ private static final String ADDITIONAL_NAME_KEY = "additionalName";
+ private static final String FAMILY_NAME_KEY = "familyName";
+ private static final String ORGANIZATION_KEY = "organization";
+ private static final String STREET_ADDRESS_KEY = "streetAddress";
+ private static final String ADDRESS_LEVEL1_KEY = "addressLevel1";
+ private static final String ADDRESS_LEVEL2_KEY = "addressLevel2";
+ private static final String ADDRESS_LEVEL3_KEY = "addressLevel3";
+ private static final String POSTAL_CODE_KEY = "postalCode";
+ private static final String COUNTRY_KEY = "country";
+ private static final String TEL_KEY = "tel";
+ private static final String EMAIL_KEY = "email";
+ private static final byte bundleCapacity = 14;
+
+ /** The unique identifier for this address entry. */
+ public final @Nullable String guid;
+
+ /** The full name. */
+ public final @NonNull String name;
+
+ /** The given (first) name. */
+ public final @NonNull String givenName;
+
+ /** An additional name, if available. */
+ public final @NonNull String additionalName;
+
+ /** The family name. */
+ public final @NonNull String familyName;
+
+ /** The name of the company, if applicable. */
+ public final @NonNull String organization;
+
+ /** The (multiline) street address. */
+ public final @NonNull String streetAddress;
+
+ /** The level 1 (province) address. Note: Only use if streetAddress is not provided. */
+ public final @NonNull String addressLevel1;
+
+ /** The level 2 (city/town) address. Note: Only use if streetAddress is not provided. */
+ public final @NonNull String addressLevel2;
+
+ /**
+ * The level 3 (suburb/sublocality) address. Note: Only use if streetAddress is not provided.
+ */
+ public final @NonNull String addressLevel3;
+
+ /** The postal code. */
+ public final @NonNull String postalCode;
+
+ /** The country string in ISO 3166. */
+ public final @NonNull String country;
+
+ /** The telephone number string. */
+ public final @NonNull String tel;
+
+ /** The email address. */
+ public final @NonNull String email;
+
+ // For tests only.
+ @AnyThread
+ protected Address() {
+ guid = null;
+ name = "";
+ givenName = "";
+ additionalName = "";
+ familyName = "";
+ organization = "";
+ streetAddress = "";
+ addressLevel1 = "";
+ addressLevel2 = "";
+ addressLevel3 = "";
+ postalCode = "";
+ country = "";
+ tel = "";
+ email = "";
+ }
+
+ @AnyThread
+ /* package */ Address(final @NonNull GeckoBundle bundle) {
+ guid = bundle.getString(GUID_KEY);
+ name = bundle.getString(NAME_KEY, "");
+ givenName = bundle.getString(GIVEN_NAME_KEY, "");
+ additionalName = bundle.getString(ADDITIONAL_NAME_KEY, "");
+ familyName = bundle.getString(FAMILY_NAME_KEY, "");
+ organization = bundle.getString(ORGANIZATION_KEY, "");
+ streetAddress = bundle.getString(STREET_ADDRESS_KEY, "");
+ addressLevel1 = bundle.getString(ADDRESS_LEVEL1_KEY, "");
+ addressLevel2 = bundle.getString(ADDRESS_LEVEL2_KEY, "");
+ addressLevel3 = bundle.getString(ADDRESS_LEVEL3_KEY, "");
+ postalCode = bundle.getString(POSTAL_CODE_KEY, "");
+ country = bundle.getString(COUNTRY_KEY, "");
+ tel = bundle.getString(TEL_KEY, "");
+ email = bundle.getString(EMAIL_KEY, "");
+ }
+
+ @Override
+ @AnyThread
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("Address {");
+ builder
+ .append("guid=")
+ .append(guid)
+ .append(", givenName=")
+ .append(givenName)
+ .append(", additionalName=")
+ .append(additionalName)
+ .append(", familyName=")
+ .append(familyName)
+ .append(", organization=")
+ .append(organization)
+ .append(", streetAddress=")
+ .append(streetAddress)
+ .append(", addressLevel1=")
+ .append(addressLevel1)
+ .append(", addressLevel2=")
+ .append(addressLevel2)
+ .append(", addressLevel3=")
+ .append(addressLevel3)
+ .append(", postalCode=")
+ .append(postalCode)
+ .append(", country=")
+ .append(country)
+ .append(", tel=")
+ .append(tel)
+ .append(", email=")
+ .append(email)
+ .append("}");
+ return builder.toString();
+ }
+
+ @AnyThread
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(bundleCapacity);
+ bundle.putString(GUID_KEY, guid);
+ bundle.putString(NAME_KEY, name);
+ bundle.putString(GIVEN_NAME_KEY, givenName);
+ bundle.putString(ADDITIONAL_NAME_KEY, additionalName);
+ bundle.putString(FAMILY_NAME_KEY, familyName);
+ bundle.putString(ORGANIZATION_KEY, organization);
+ bundle.putString(STREET_ADDRESS_KEY, streetAddress);
+ bundle.putString(ADDRESS_LEVEL1_KEY, addressLevel1);
+ bundle.putString(ADDRESS_LEVEL2_KEY, addressLevel2);
+ bundle.putString(ADDRESS_LEVEL3_KEY, addressLevel3);
+ bundle.putString(POSTAL_CODE_KEY, postalCode);
+ bundle.putString(COUNTRY_KEY, country);
+ bundle.putString(TEL_KEY, tel);
+ bundle.putString(EMAIL_KEY, email);
+
+ return bundle;
+ }
+
+ public static class Builder {
+ private final GeckoBundle mBundle;
+
+ @AnyThread
+ /* package */ Builder(final @NonNull GeckoBundle bundle) {
+ mBundle = new GeckoBundle(bundle);
+ }
+
+ @AnyThread
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public Builder() {
+ mBundle = new GeckoBundle(bundleCapacity);
+ }
+
+ /**
+ * Finalize the {@link Address} instance.
+ *
+ * @return The {@link Address} instance.
+ */
+ @AnyThread
+ public @NonNull Address build() {
+ return new Address(mBundle);
+ }
+
+ /**
+ * Set the unique identifier for this address entry.
+ *
+ * @param guid The unique identifier string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder guid(final @Nullable String guid) {
+ mBundle.putString(GUID_KEY, guid);
+ return this;
+ }
+
+ /**
+ * Set the full name for this address entry.
+ *
+ * @param name The full name string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder name(final @Nullable String name) {
+ mBundle.putString(NAME_KEY, name);
+ return this;
+ }
+
+ /**
+ * Set the given name for this address entry.
+ *
+ * @param givenName The given name string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder givenName(final @Nullable String givenName) {
+ mBundle.putString(GIVEN_NAME_KEY, givenName);
+ return this;
+ }
+
+ /**
+ * Set the additional name for this address entry.
+ *
+ * @param additionalName The additional name string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder additionalName(final @Nullable String additionalName) {
+ mBundle.putString(ADDITIONAL_NAME_KEY, additionalName);
+ return this;
+ }
+
+ /**
+ * Set the family name for this address entry.
+ *
+ * @param familyName The family name string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder familyName(final @Nullable String familyName) {
+ mBundle.putString(FAMILY_NAME_KEY, familyName);
+ return this;
+ }
+
+ /**
+ * Set the company name for this address entry.
+ *
+ * @param organization The company name string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder organization(final @Nullable String organization) {
+ mBundle.putString(ORGANIZATION_KEY, organization);
+ return this;
+ }
+
+ /**
+ * Set the street address for this address entry.
+ *
+ * @param streetAddress The street address string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder streetAddress(final @Nullable String streetAddress) {
+ mBundle.putString(STREET_ADDRESS_KEY, streetAddress);
+ return this;
+ }
+
+ /**
+ * Set the level 1 address for this address entry.
+ *
+ * @param addressLevel1 The level 1 address string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder addressLevel1(final @Nullable String addressLevel1) {
+ mBundle.putString(ADDRESS_LEVEL1_KEY, addressLevel1);
+ return this;
+ }
+
+ /**
+ * Set the level 2 address for this address entry.
+ *
+ * @param addressLevel2 The level 2 address string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder addressLevel2(final @Nullable String addressLevel2) {
+ mBundle.putString(ADDRESS_LEVEL2_KEY, addressLevel2);
+ return this;
+ }
+
+ /**
+ * Set the level 3 address for this address entry.
+ *
+ * @param addressLevel3 The level 3 address string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder addressLevel3(final @Nullable String addressLevel3) {
+ mBundle.putString(ADDRESS_LEVEL3_KEY, addressLevel3);
+ return this;
+ }
+
+ /**
+ * Set the postal code for this address entry.
+ *
+ * @param postalCode The postal code string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder postalCode(final @Nullable String postalCode) {
+ mBundle.putString(POSTAL_CODE_KEY, postalCode);
+ return this;
+ }
+
+ /**
+ * Set the country code for this address entry.
+ *
+ * @param country The country string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder country(final @Nullable String country) {
+ mBundle.putString(COUNTRY_KEY, country);
+ return this;
+ }
+
+ /**
+ * Set the telephone number for this address entry.
+ *
+ * @param tel The telephone number string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder tel(final @Nullable String tel) {
+ mBundle.putString(TEL_KEY, tel);
+ return this;
+ }
+
+ /**
+ * Set the email address for this address entry.
+ *
+ * @param email The email address string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder email(final @Nullable String email) {
+ mBundle.putString(EMAIL_KEY, email);
+ return this;
+ }
+ }
+ }
+
+ /** Holds login information for a specific entry. */
+ public static class LoginEntry {
+ private static final String GUID_KEY = "guid";
+ private static final String ORIGIN_KEY = "origin";
+ private static final String FORM_ACTION_ORIGIN_KEY = "formActionOrigin";
+ private static final String HTTP_REALM_KEY = "httpRealm";
+ private static final String USERNAME_KEY = "username";
+ private static final String PASSWORD_KEY = "password";
+
+ /** The unique identifier for this login entry. */
+ public final @Nullable String guid;
+
+ /** The origin this login entry applies to. */
+ public final @NonNull String origin;
+
+ /**
+ * The origin this login entry was submitted to. This only applies to form-based login entries.
+ * It's derived from the action attribute set on the form element.
+ */
+ public final @Nullable String formActionOrigin;
+
+ /**
+ * The HTTP realm this login entry was requested for. This only applies to non-form-based login
+ * entries. It's derived from the WWW-Authenticate header set in a HTTP 401 response, see
+ * RFC2617 for details.
+ */
+ public final @Nullable String httpRealm;
+
+ /** The username for this login entry. */
+ public final @NonNull String username;
+
+ /** The password for this login entry. */
+ public final @NonNull String password;
+
+ // For tests only.
+ @AnyThread
+ protected LoginEntry() {
+ guid = null;
+ origin = "";
+ formActionOrigin = null;
+ httpRealm = null;
+ username = "";
+ password = "";
+ }
+
+ @AnyThread
+ /* package */ LoginEntry(final @NonNull GeckoBundle bundle) {
+ guid = bundle.getString(GUID_KEY);
+ origin = bundle.getString(ORIGIN_KEY, "");
+ formActionOrigin = bundle.getString(FORM_ACTION_ORIGIN_KEY);
+ httpRealm = bundle.getString(HTTP_REALM_KEY);
+ username = bundle.getString(USERNAME_KEY, "");
+ password = bundle.getString(PASSWORD_KEY, "");
+ }
+
+ @Override
+ @AnyThread
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("LoginEntry {");
+ builder
+ .append("guid=")
+ .append(guid)
+ .append(", origin=")
+ .append(origin)
+ .append(", formActionOrigin=")
+ .append(formActionOrigin)
+ .append(", httpRealm=")
+ .append(httpRealm)
+ .append(", username=")
+ .append(username)
+ .append(", password=")
+ .append(password)
+ .append("}");
+ return builder.toString();
+ }
+
+ @AnyThread
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(6);
+ bundle.putString(GUID_KEY, guid);
+ bundle.putString(ORIGIN_KEY, origin);
+ bundle.putString(FORM_ACTION_ORIGIN_KEY, formActionOrigin);
+ bundle.putString(HTTP_REALM_KEY, httpRealm);
+ bundle.putString(USERNAME_KEY, username);
+ bundle.putString(PASSWORD_KEY, password);
+
+ return bundle;
+ }
+
+ public static class Builder {
+ private final GeckoBundle mBundle;
+
+ @AnyThread
+ /* package */ Builder(final @NonNull GeckoBundle bundle) {
+ mBundle = new GeckoBundle(bundle);
+ }
+
+ @AnyThread
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public Builder() {
+ mBundle = new GeckoBundle(6);
+ }
+
+ /**
+ * Finalize the {@link LoginEntry} instance.
+ *
+ * @return The {@link LoginEntry} instance.
+ */
+ @AnyThread
+ public @NonNull LoginEntry build() {
+ return new LoginEntry(mBundle);
+ }
+
+ /**
+ * Set the unique identifier for this login entry.
+ *
+ * @param guid The unique identifier string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder guid(final @Nullable String guid) {
+ mBundle.putString(GUID_KEY, guid);
+ return this;
+ }
+
+ /**
+ * Set the origin this login entry applies to.
+ *
+ * @param origin The origin string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder origin(final @NonNull String origin) {
+ mBundle.putString(ORIGIN_KEY, origin);
+ return this;
+ }
+
+ /**
+ * Set the origin this login entry was submitted to.
+ *
+ * @param formActionOrigin The form action origin string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder formActionOrigin(final @Nullable String formActionOrigin) {
+ mBundle.putString(FORM_ACTION_ORIGIN_KEY, formActionOrigin);
+ return this;
+ }
+
+ /**
+ * Set the HTTP realm this login entry was requested for.
+ *
+ * @param httpRealm The HTTP realm string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder httpRealm(final @Nullable String httpRealm) {
+ mBundle.putString(HTTP_REALM_KEY, httpRealm);
+ return this;
+ }
+
+ /**
+ * Set the username for this login entry.
+ *
+ * @param username The username string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder username(final @NonNull String username) {
+ mBundle.putString(USERNAME_KEY, username);
+ return this;
+ }
+
+ /**
+ * Set the password for this login entry.
+ *
+ * @param password The password string.
+ * @return This {@link Builder} instance.
+ */
+ @AnyThread
+ public @NonNull Builder password(final @NonNull String password) {
+ mBundle.putString(PASSWORD_KEY, password);
+ return this;
+ }
+ }
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {UsedField.PASSWORD})
+ public @interface LSUsedField {}
+
+ // Sync with UsedField in GeckoViewAutocomplete.sys.mjs.
+ /** Possible login entry field types for {@link StorageDelegate#onLoginUsed}. */
+ public static class UsedField {
+ /** The password field of a login entry. */
+ public static final int PASSWORD = 1;
+
+ protected UsedField() {}
+ }
+
+ /**
+ * Implement this interface to handle runtime login storage requests. Login storage events include
+ * login entry requests for autofill and autocompletion of login input fields. This delegate is
+ * attached to the runtime via {@link GeckoRuntime#setAutocompleteStorageDelegate}.
+ */
+ public interface StorageDelegate {
+ /**
+ * Request login entries for a given domain. While processing the web document, we have
+ * identified elements resembling login input fields suitable for autofill. We will attempt to
+ * match the provided login information to the identified input fields.
+ *
+ * @param domain The domain string for the requested logins.
+ * @return A {@link GeckoResult} that completes with an array of {@link LoginEntry} containing
+ * the existing logins for the given domain.
+ */
+ @UiThread
+ default @Nullable GeckoResult<LoginEntry[]> onLoginFetch(@NonNull final String domain) {
+ return null;
+ }
+
+ /**
+ * Request login entries for all domains.
+ *
+ * @return A {@link GeckoResult} that completes with an array of {@link LoginEntry} containing
+ * the existing logins.
+ */
+ @UiThread
+ default @Nullable GeckoResult<LoginEntry[]> onLoginFetch() {
+ return null;
+ }
+
+ /**
+ * Request credit card entries. While processing the web document, we have identified elements
+ * resembling credit card input fields suitable for autofill. We will attempt to match the
+ * provided credit card information to the identified input fields.
+ *
+ * @return A {@link GeckoResult} that completes with an array of {@link CreditCard} containing
+ * the existing credit cards.
+ */
+ @UiThread
+ default @Nullable GeckoResult<CreditCard[]> onCreditCardFetch() {
+ return null;
+ }
+
+ /**
+ * Request address entries. While processing the web document, we have identified elements
+ * resembling address input fields suitable for autofill. We will attempt to match the provided
+ * address information to the identified input fields.
+ *
+ * @return A {@link GeckoResult} that completes with an array of {@link Address} containing the
+ * existing addresses.
+ */
+ @UiThread
+ default @Nullable GeckoResult<Address[]> onAddressFetch() {
+ return null;
+ }
+
+ /**
+ * Request saving or updating of the given login entry. This is triggered by confirming a {@link
+ * GeckoSession.PromptDelegate#onLoginSave onLoginSave} request.
+ *
+ * @param login The {@link LoginEntry} as confirmed by the prompt request.
+ */
+ @UiThread
+ default void onLoginSave(@NonNull final LoginEntry login) {}
+
+ /**
+ * Request saving or updating of the given credit card entry. This is triggered by confirming a
+ * {@link GeckoSession.PromptDelegate#onCreditCardSave onCreditCardSave} request.
+ *
+ * @param creditCard The {@link CreditCard} as confirmed by the prompt request.
+ */
+ @UiThread
+ default void onCreditCardSave(@NonNull CreditCard creditCard) {}
+
+ /**
+ * Request saving or updating of the given address entry. This is triggered by confirming a
+ * {@link GeckoSession.PromptDelegate#onAddressSave onAddressSave} request.
+ *
+ * @param address The {@link Address} as confirmed by the prompt request.
+ */
+ @UiThread
+ default void onAddressSave(@NonNull Address address) {}
+
+ /**
+ * Notify that the given login was used to autofill login input fields. This is triggered by
+ * autofilling elements with unmodified login entries as provided via {@link #onLoginFetch}.
+ *
+ * @param login The {@link LoginEntry} that was used for the autofilling.
+ * @param usedFields The login entry fields used for autofilling. A combination of {@link
+ * UsedField}.
+ */
+ @UiThread
+ default void onLoginUsed(@NonNull final LoginEntry login, @LSUsedField final int usedFields) {}
+ }
+
+ /**
+ * Abstract base class for Autocomplete options. Extended by {@link Autocomplete.SaveOption} and
+ * {@link Autocomplete.SelectOption}.
+ */
+ public abstract static class Option<T> {
+ /* package */ static final String VALUE_KEY = "value";
+ /* package */ static final String HINT_KEY = "hint";
+
+ public final @NonNull T value;
+ public final int hint;
+
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public Option(final @NonNull T value, final int hint) {
+ this.value = value;
+ this.hint = hint;
+ }
+
+ @AnyThread
+ /* package */ abstract @NonNull GeckoBundle toBundle();
+ }
+
+ /** Abstract base class for saving options. Extended by {@link Autocomplete.LoginSaveOption}. */
+ public abstract static class SaveOption<T> extends Option<T> {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {Hint.NONE, Hint.GENERATED, Hint.LOW_CONFIDENCE})
+ public @interface SaveOptionHint {}
+
+ /** Hint types for login saving requests. */
+ public static class Hint {
+ public static final int NONE = 0;
+
+ /** Auto-generated password. Notify but do not prompt the user for saving. */
+ public static final int GENERATED = 1 << 0;
+
+ /**
+ * Potentially non-login data. The form data entered may be not login credentials but other
+ * forms of input like credit card numbers. Note that this could be valid login data in same
+ * cases, e.g., some banks may expect credit card numbers in the username field.
+ */
+ public static final int LOW_CONFIDENCE = 1 << 1;
+
+ protected Hint() {}
+ }
+
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public SaveOption(final @NonNull T value, final @SaveOptionHint int hint) {
+ super(value, hint);
+ }
+ }
+
+ /** Abstract base class for saving options. Extended by {@link Autocomplete.LoginSelectOption}. */
+ public abstract static class SelectOption<T> extends Option<T> {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ Hint.NONE,
+ Hint.GENERATED,
+ Hint.INSECURE_FORM,
+ Hint.DUPLICATE_USERNAME,
+ Hint.MATCHING_ORIGIN
+ })
+ public @interface SelectOptionHint {}
+
+ /** Hint types for selection requests. */
+ public static class Hint {
+ public static final int NONE = 0;
+
+ /**
+ * Auto-generated password. A new password-only login entry containing a secure generated
+ * password.
+ */
+ public static final int GENERATED = 1 << 0;
+
+ /**
+ * Insecure context. The form or transmission mechanics are considered insecure. This is the
+ * case when the form is served via http or submitted insecurely.
+ */
+ public static final int INSECURE_FORM = 1 << 1;
+
+ /**
+ * The username is shared with another login entry. There are multiple login entries in the
+ * options that share the same username. You may have to disambiguate the login entry, e.g.,
+ * using the last date of modification and its origin.
+ */
+ public static final int DUPLICATE_USERNAME = 1 << 2;
+
+ /**
+ * The login entry's origin matches the login form origin. The login was saved from the same
+ * origin it is being requested for, rather than for a subdomain.
+ */
+ public static final int MATCHING_ORIGIN = 1 << 3;
+ }
+
+ @SuppressWarnings("checkstyle:javadocmethod")
+ public SelectOption(final @NonNull T value, final @SelectOptionHint int hint) {
+ super(value, hint);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder("SelectOption {");
+ builder.append("value=").append(value).append(", ").append("hint=").append(hint).append("}");
+ return builder.toString();
+ }
+ }
+
+ /** Holds information required to process login saving requests. */
+ public static class LoginSaveOption extends SaveOption<LoginEntry> {
+ /**
+ * Construct a login save option.
+ *
+ * @param value The {@link LoginEntry} login entry to be saved.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ LoginSaveOption(final @NonNull LoginEntry value, final @SaveOptionHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct a login save option.
+ *
+ * @param value The {@link LoginEntry} login entry to be saved.
+ */
+ public LoginSaveOption(final @NonNull LoginEntry value) {
+ this(value, Hint.NONE);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /** Holds information required to process address saving requests. */
+ public static class AddressSaveOption extends SaveOption<Address> {
+ /**
+ * Construct a address save option.
+ *
+ * @param value The {@link Address} address entry to be saved.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ AddressSaveOption(final @NonNull Address value, final @SaveOptionHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct an address save option.
+ *
+ * @param value The {@link Address} address entry to be saved.
+ */
+ public AddressSaveOption(final @NonNull Address value) {
+ this(value, Hint.NONE);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /** Holds information required to process credit card saving requests. */
+ public static class CreditCardSaveOption extends SaveOption<CreditCard> {
+ /**
+ * Construct a credit card save option.
+ *
+ * @param value The {@link CreditCard} credit card entry to be saved.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ CreditCardSaveOption(
+ final @NonNull CreditCard value, final @SaveOptionHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct a credit card save option.
+ *
+ * @param value The {@link CreditCard} credit card entry to be saved.
+ */
+ public CreditCardSaveOption(final @NonNull CreditCard value) {
+ this(value, Hint.NONE);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /** Holds information required to process login selection requests. */
+ public static class LoginSelectOption extends SelectOption<LoginEntry> {
+ /**
+ * Construct a login select option.
+ *
+ * @param value The {@link LoginEntry} login entry selection option.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ LoginSelectOption(
+ final @NonNull LoginEntry value, final @SelectOptionHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct a login select option.
+ *
+ * @param value The {@link LoginEntry} login entry selection option.
+ */
+ public LoginSelectOption(final @NonNull LoginEntry value) {
+ this(value, Hint.NONE);
+ }
+
+ /* package */ static @NonNull LoginSelectOption fromBundle(final @NonNull GeckoBundle bundle) {
+ final int hint = bundle.getInt("hint");
+ final LoginEntry value = new LoginEntry(bundle.getBundle("value"));
+
+ return new LoginSelectOption(value, hint);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /** Holds information required to process credit card selection requests. */
+ public static class CreditCardSelectOption extends SelectOption<CreditCard> {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {Hint.NONE, Hint.INSECURE_FORM})
+ public @interface CreditCardSelectHint {}
+
+ /** Hint types for credit card selection requests. */
+ public static class Hint {
+ public static final int NONE = 0;
+
+ /**
+ * Insecure context. The form or transmission mechanics are considered insecure. This is the
+ * case when the form is served via http or submitted insecurely.
+ */
+ public static final int INSECURE_FORM = 1 << 1;
+ }
+
+ /**
+ * Construct a credit card select option.
+ *
+ * @param value The {@link LoginEntry} credit card entry selection option.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ CreditCardSelectOption(
+ final @NonNull CreditCard value, final @CreditCardSelectHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct a credit card select option.
+ *
+ * @param value The {@link CreditCard} credit card entry selection option.
+ */
+ public CreditCardSelectOption(final @NonNull CreditCard value) {
+ this(value, Hint.NONE);
+ }
+
+ /* package */ static @NonNull CreditCardSelectOption fromBundle(
+ final @NonNull GeckoBundle bundle) {
+ final int hint = bundle.getInt("hint");
+ final CreditCard value = new CreditCard(bundle.getBundle("value"));
+
+ return new CreditCardSelectOption(value, hint);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /** Holds information required to process address selection requests. */
+ public static class AddressSelectOption extends SelectOption<Address> {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {Hint.NONE, Hint.INSECURE_FORM})
+ public @interface AddressSelectHint {}
+
+ /** Hint types for credit card selection requests. */
+ public static class Hint {
+ public static final int NONE = 0;
+
+ /**
+ * Insecure context. The form or transmission mechanics are considered insecure. This is the
+ * case when the form is served via http or submitted insecurely.
+ */
+ public static final int INSECURE_FORM = 1 << 1;
+ }
+
+ /**
+ * Construct a credit card select option.
+ *
+ * @param value The {@link LoginEntry} credit card entry selection option.
+ * @param hint The {@link Hint} detailing the type of the option.
+ */
+ /* package */ AddressSelectOption(
+ final @NonNull Address value, final @AddressSelectHint int hint) {
+ super(value, hint);
+ }
+
+ /**
+ * Construct a address select option.
+ *
+ * @param value The {@link Address} address entry selection option.
+ */
+ public AddressSelectOption(final @NonNull Address value) {
+ this(value, Hint.NONE);
+ }
+
+ /* package */ static @NonNull AddressSelectOption fromBundle(
+ final @NonNull GeckoBundle bundle) {
+ final int hint = bundle.getInt("hint");
+ final Address value = new Address(bundle.getBundle("value"));
+
+ return new AddressSelectOption(value, hint);
+ }
+
+ @Override
+ /* package */ @NonNull
+ GeckoBundle toBundle() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putBundle(VALUE_KEY, value.toBundle());
+ bundle.putInt(HINT_KEY, hint);
+ return bundle;
+ }
+ }
+
+ /* package */ static final class StorageProxy implements BundleEventListener {
+ private static final String FETCH_LOGIN_EVENT = "GeckoView:Autocomplete:Fetch:Login";
+ private static final String FETCH_CREDIT_CARD_EVENT = "GeckoView:Autocomplete:Fetch:CreditCard";
+ private static final String FETCH_ADDRESS_EVENT = "GeckoView:Autocomplete:Fetch:Address";
+ private static final String SAVE_LOGIN_EVENT = "GeckoView:Autocomplete:Save:Login";
+ private static final String SAVE_CREDIT_CARD_EVENT = "GeckoView:Autocomplete:Save:CreditCard";
+ private static final String SAVE_ADDRESS_EVENT = "GeckoView:Autocomplete:Save:Address";
+ private static final String USED_LOGIN_EVENT = "GeckoView:Autocomplete:Used:Login";
+
+ private @Nullable StorageDelegate mDelegate;
+
+ public StorageProxy() {}
+
+ private void registerListener() {
+ EventDispatcher.getInstance().dispatch("GeckoView:StorageDelegate:Attached", null);
+ EventDispatcher.getInstance()
+ .registerUiThreadListener(
+ this,
+ FETCH_LOGIN_EVENT,
+ FETCH_CREDIT_CARD_EVENT,
+ FETCH_ADDRESS_EVENT,
+ SAVE_LOGIN_EVENT,
+ SAVE_CREDIT_CARD_EVENT,
+ SAVE_ADDRESS_EVENT,
+ USED_LOGIN_EVENT);
+ }
+
+ private void unregisterListener() {
+ EventDispatcher.getInstance()
+ .unregisterUiThreadListener(
+ this,
+ FETCH_LOGIN_EVENT,
+ FETCH_CREDIT_CARD_EVENT,
+ FETCH_ADDRESS_EVENT,
+ SAVE_LOGIN_EVENT,
+ SAVE_CREDIT_CARD_EVENT,
+ SAVE_ADDRESS_EVENT,
+ USED_LOGIN_EVENT);
+ }
+
+ public synchronized void setDelegate(final @Nullable StorageDelegate delegate) {
+ if (mDelegate == delegate) {
+ return;
+ }
+ if (mDelegate != null) {
+ unregisterListener();
+ }
+
+ mDelegate = delegate;
+
+ if (mDelegate != null) {
+ registerListener();
+ }
+ }
+
+ public synchronized @Nullable StorageDelegate getDelegate() {
+ return mDelegate;
+ }
+
+ @Override // BundleEventListener
+ public synchronized void handleMessage(
+ final String event, final GeckoBundle message, final EventCallback callback) {
+ if (DEBUG) {
+ Log.d(LOGTAG, "handleMessage " + event);
+ }
+
+ if (mDelegate == null) {
+ if (callback != null) {
+ callback.sendError("No StorageDelegate attached");
+ }
+ return;
+ }
+
+ if (FETCH_LOGIN_EVENT.equals(event)) {
+ final String domain = message.getString("domain");
+ final GeckoResult<Autocomplete.LoginEntry[]> result =
+ domain != null ? mDelegate.onLoginFetch(domain) : mDelegate.onLoginFetch();
+
+ if (result == null) {
+ callback.sendSuccess(new GeckoBundle[0]);
+ return;
+ }
+
+ callback.resolveTo(
+ result.map(
+ logins -> {
+ if (logins == null) {
+ return new GeckoBundle[0];
+ }
+
+ // This is a one-liner with streams (API level 24).
+ final GeckoBundle[] loginBundles = new GeckoBundle[logins.length];
+ for (int i = 0; i < logins.length; ++i) {
+ loginBundles[i] = logins[i].toBundle();
+ }
+
+ return loginBundles;
+ }));
+ } else if (FETCH_CREDIT_CARD_EVENT.equals(event)) {
+ final GeckoResult<Autocomplete.CreditCard[]> result = mDelegate.onCreditCardFetch();
+
+ if (result == null) {
+ callback.sendSuccess(new GeckoBundle[0]);
+ return;
+ }
+
+ callback.resolveTo(
+ result.map(
+ creditCards -> {
+ if (creditCards == null) {
+ return new GeckoBundle[0];
+ }
+
+ // This is a one-liner with streams (API level 24).
+ final GeckoBundle[] creditCardBundles = new GeckoBundle[creditCards.length];
+ for (int i = 0; i < creditCards.length; ++i) {
+ creditCardBundles[i] = creditCards[i].toBundle();
+ }
+
+ return creditCardBundles;
+ }));
+ } else if (FETCH_ADDRESS_EVENT.equals(event)) {
+ final GeckoResult<Autocomplete.Address[]> result = mDelegate.onAddressFetch();
+
+ if (result == null) {
+ callback.sendSuccess(new GeckoBundle[0]);
+ return;
+ }
+
+ callback.resolveTo(
+ result.map(
+ addresses -> {
+ if (addresses == null) {
+ return new GeckoBundle[0];
+ }
+
+ // This is a one-liner with streams (API level 24).
+ final GeckoBundle[] addressBundles = new GeckoBundle[addresses.length];
+ for (int i = 0; i < addresses.length; ++i) {
+ addressBundles[i] = addresses[i].toBundle();
+ }
+
+ return addressBundles;
+ }));
+ } else if (SAVE_LOGIN_EVENT.equals(event)) {
+ final GeckoBundle loginBundle = message.getBundle("login");
+ final LoginEntry login = new LoginEntry(loginBundle);
+
+ mDelegate.onLoginSave(login);
+ } else if (SAVE_CREDIT_CARD_EVENT.equals(event)) {
+ final GeckoBundle creditCardBundle = message.getBundle("creditCard");
+ final CreditCard creditCard = new CreditCard(creditCardBundle);
+
+ mDelegate.onCreditCardSave(creditCard);
+ } else if (SAVE_ADDRESS_EVENT.equals(event)) {
+ final GeckoBundle addressBundle = message.getBundle("address");
+ final Address address = new Address(addressBundle);
+
+ mDelegate.onAddressSave(address);
+ } else if (USED_LOGIN_EVENT.equals(event)) {
+ final GeckoBundle loginBundle = message.getBundle("login");
+ final LoginEntry login = new LoginEntry(loginBundle);
+ final int fields = message.getInt("usedFields");
+
+ mDelegate.onLoginUsed(login, fields);
+ }
+ }
+ }
+}