commit 310af0b4f5620c270c0f86a195745c1eb4946d3c
parent 1961e713f2117ce4123e0e9199a9e656b029b5b8
Author: Jeff Boek <j@jboek.com>
Date: Fri, 17 Oct 2025 11:18:08 +0000
Bug 1993390 - Adds new APIs to retrieve the address structure for a given region r=geckoview-reviewers,geckoview-api-reviewers,owlish,tcampbell
Differential Revision: https://phabricator.services.mozilla.com/D268063
Diffstat:
6 files changed, 353 insertions(+), 1 deletion(-)
diff --git a/mobile/android/geckoview/api.txt b/mobile/android/geckoview/api.txt
@@ -82,6 +82,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.json.JSONObject;
+import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.geckoview.AllowOrDeny;
import org.mozilla.geckoview.Autocomplete;
import org.mozilla.geckoview.Autofill;
@@ -193,6 +194,31 @@ package org.mozilla.geckoview {
field public static final int NONE = 0;
}
+ public static class Autocomplete.AddressStructure {
+ method @AnyThread @NonNull public static GeckoResult<List<Autocomplete.AddressStructure.Field>> getAddressStructure(@NonNull String);
+ }
+
+ public static interface Autocomplete.AddressStructure.Field {
+ method @AnyThread @NonNull default public String getId();
+ method @AnyThread @NonNull default public String getLocalizationKey();
+ }
+
+ public static class Autocomplete.AddressStructure.Field.SelectField implements Autocomplete.AddressStructure.Field {
+ ctor public SelectField(String, String, @NonNull String, @NonNull List<Autocomplete.AddressStructure.Field.SelectField.Option>);
+ field @NonNull public final String defaultValue;
+ field @NonNull public final List<Autocomplete.AddressStructure.Field.SelectField.Option> options;
+ }
+
+ public static class Autocomplete.AddressStructure.Field.SelectField.Option {
+ ctor public Option(@NonNull String, @NonNull String);
+ field @NonNull public final String key;
+ field @NonNull public final String value;
+ }
+
+ public static final class Autocomplete.AddressStructure.Field.TextField implements Autocomplete.AddressStructure.Field {
+ ctor public TextField(String, String);
+ }
+
public static class Autocomplete.CreditCard {
ctor @AnyThread protected CreditCard();
field @NonNull public final String expirationMonth;
diff --git a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt
@@ -9,12 +9,14 @@ import android.os.Looper
import android.view.KeyEvent
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import junit.framework.TestCase.assertTrue
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.isEmptyOrNullString
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.junit.Test
import org.junit.runner.RunWith
+import org.mozilla.geckoview.Autocomplete
import org.mozilla.geckoview.Autocomplete.Address
import org.mozilla.geckoview.Autocomplete.AddressSelectOption
import org.mozilla.geckoview.Autocomplete.CreditCard
@@ -30,6 +32,7 @@ import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.PromptDelegate
import org.mozilla.geckoview.GeckoSession.PromptDelegate.AutocompleteRequest
+import org.mozilla.geckoview.TranslationsController
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
@@ -2602,4 +2605,69 @@ class AutocompleteTest : BaseSessionTest() {
mainSession.evaluateJS("document.querySelector('#user1').blur()")
sessionRule.waitForResult(result)
}
+
+ @Test
+ fun testAddressStructureGetFieldsForUS() {
+ val structureResult = Autocomplete.AddressStructure.getAddressStructure("US")
+
+ try {
+ sessionRule.waitForResult(structureResult)
+ assertTrue("Should not be able to retreive a structure.", true)
+ } catch (e: Exception) {
+ assertTrue("Should not have an exception.", false)
+ }
+
+ sessionRule.waitForResult(structureResult).let { fields ->
+ val expectedResult = listOf(
+ Pair("name", "autofill-address-name"),
+ Pair("organization", "autofill-address-organization"),
+ Pair("street-address", "autofill-address-street"),
+ Pair("address-level2", "autofill-address-city"),
+ Pair("address-level1", "autofill-address-state"),
+ Pair("postal-code", "autofill-address-zip"),
+ Pair("country", "autofill-address-country"),
+ Pair("tel", "autofill-address-tel"),
+ Pair("email", "autofill-address-email"),
+ )
+
+ expectedResult.forEachIndexed { index, pair ->
+ assertTrue(
+ "Result should have id: ${pair.first}, localizationKey: ${pair.second}",
+ fields[index].id == pair.first && fields[index].localizationKey == pair.second,
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testAddressStructureGetFieldsForJP() {
+ val structureResult = Autocomplete.AddressStructure.getAddressStructure("JP")
+
+ try {
+ sessionRule.waitForResult(structureResult)
+ assertTrue("Should not be able to retreive a structure.", true)
+ } catch (e: Exception) {
+ assertTrue("Should not have an exception.", false)
+ }
+
+ sessionRule.waitForResult(structureResult).let { fields ->
+ val expectedResult = listOf(
+ Pair("postal-code", "autofill-address-postal-code"),
+ Pair("address-level1", "autofill-address-prefecture"),
+ Pair("street-address", "autofill-address-street"),
+ Pair("organization", "autofill-address-organization"),
+ Pair("name", "autofill-address-name"),
+ Pair("country", "autofill-address-country"),
+ Pair("tel", "autofill-address-tel"),
+ Pair("email", "autofill-address-email"),
+ )
+
+ expectedResult.forEachIndexed { index, pair ->
+ assertTrue(
+ "Result should have id: ${pair.first}, localizationKey: ${pair.second}",
+ fields[index].id == pair.first && fields[index].localizationKey == pair.second,
+ )
+ }
+ }
+ }
}
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
@@ -14,6 +14,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
@@ -652,6 +654,229 @@ public class Autocomplete {
}
}
+ /**
+ * Address structure coordinates runtime messaging between the form autofill toolkit utils and
+ * GeckoView.
+ */
+ public static class AddressStructure {
+ private static final String GET_ADDRESS_STRUCTURE = "GeckoView:Autofill:GetAddressStructure";
+ private static final String FIELD_ID_KEY = "fieldId";
+ private static final String L10N_ID_KEY = "l10nId";
+
+ /** Private constructor for AddressStructure */
+ private AddressStructure() {}
+
+ /**
+ * Returns a list of Fields in order for a given country.
+ *
+ * @param countryCode The country you want an address structure for.
+ * @return a GeckoResult with a list of Fields for the given country or an exception.
+ */
+ @AnyThread
+ public static @NonNull GeckoResult<List<Field>> getAddressStructure(
+ @NonNull final String countryCode) {
+ final GeckoBundle param = new GeckoBundle();
+ param.putString("country", countryCode);
+ return EventDispatcher.getInstance()
+ .queryBundle(GET_ADDRESS_STRUCTURE, param)
+ .map(AddressStructure::fromBundle);
+ }
+
+ @NonNull
+ private static List<Field> fromBundle(final GeckoBundle bundle) {
+ if (bundle == null)
+ throw new IllegalStateException(
+ "AddressStructure.fromBundle expects non-null bundle, " + "but got null value");
+
+ final List<Field> fields = new ArrayList<>();
+
+ GeckoBundle[] bundleFields = bundle.getBundleArray("fields");
+ if (bundleFields == null) {
+ bundleFields = new GeckoBundle[] {};
+ }
+ for (final GeckoBundle field : bundleFields) {
+ final Field addressField = Field.fromBundle(field);
+ fields.add(addressField);
+ }
+
+ return fields;
+ }
+
+ /** Describes how UI clients should render a single row (field) in an address */
+ public interface Field {
+ /**
+ * The ID for the field. Maps 1:1 to a field on an Address.
+ *
+ * @return the id.
+ */
+ @NonNull
+ @AnyThread
+ default String getId() {
+ return "";
+ }
+
+ /**
+ * Returns the localization key used to obtain a human-readable label for this field.
+ *
+ * <p>This key allows the embedding application to look up the display string in its own
+ * resources (e.g., `autofill-address-name`). It is not a user-visible string.
+ *
+ * @return the localization key.
+ */
+ @NonNull
+ @AnyThread
+ default String getLocalizationKey() {
+ return "";
+ }
+
+ @NonNull
+ private static Field fromBundle(final GeckoBundle bundle) {
+ if (bundle.containsKey("options")) {
+ return SelectField.fromBundle(bundle);
+ } else {
+ return TextField.fromBundle(bundle);
+ }
+ }
+
+ /** A simple text input field (e.g., name, city). */
+ final class TextField implements Field {
+ private final String mId;
+ private final String mLocalizationKey;
+
+ /**
+ * Constructor for TextField.
+ *
+ * @param id The id for this field
+ * @param localizationKey The localization key for this field
+ */
+ public TextField(final String id, final String localizationKey) {
+ this.mId = id;
+ this.mLocalizationKey = localizationKey;
+ }
+
+ @Override
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @Override
+ @NonNull
+ public String getLocalizationKey() {
+ return mLocalizationKey;
+ }
+
+ @NonNull
+ static TextField fromBundle(final GeckoBundle bundle) {
+ return new TextField(
+ bundle.getString(AddressStructure.FIELD_ID_KEY),
+ bundle.getString(AddressStructure.L10N_ID_KEY));
+ }
+ }
+
+ /**
+ * A select (dropdown) input field with a constrained set of options (e.g., state, country).
+ */
+ class SelectField implements Field {
+ private final String mId;
+ private final String mLocalizationKey;
+
+ /** The default value to use if one has not already been selected */
+ @NonNull public final String defaultValue;
+
+ /** The default value to use if one has not already been selected */
+ @NonNull public final List<Option> options;
+
+ /**
+ * Constructor for SelectField.
+ *
+ * @param id The id for this field
+ * @param localizationKey The localization key for this field
+ * @param defaultValue The default value to use if the user hasn't selected an option
+ * @param options a list of Options that a user can select
+ */
+ public SelectField(
+ final String id,
+ final String localizationKey,
+ @NonNull final String defaultValue,
+ @NonNull final List<Option> options) {
+ this.mId = id;
+ this.mLocalizationKey = localizationKey;
+ this.defaultValue = defaultValue;
+ this.options = options;
+ }
+
+ @Override
+ @NonNull
+ public String getId() {
+ return mId;
+ }
+
+ @Override
+ @NonNull
+ public String getLocalizationKey() {
+ return mLocalizationKey;
+ }
+
+ @NonNull
+ private static SelectField fromBundle(final GeckoBundle bundle) {
+ final String id = bundle.getString(AddressStructure.FIELD_ID_KEY);
+ final String localizationKey = bundle.getString(AddressStructure.L10N_ID_KEY);
+ final String defaultValue = bundle.getString("value", "");
+
+ final List<Option> options = new ArrayList<>();
+ GeckoBundle[] bundleOptions = bundle.getBundleArray("options");
+ if (bundleOptions == null) bundleOptions = new GeckoBundle[] {};
+
+ for (final GeckoBundle bundleOption : bundleOptions) {
+ options.add(Option.fromBundle(bundleOption));
+ }
+
+ return new SelectField(id, localizationKey, defaultValue, options);
+ }
+
+ /** A single option in a SelectField. */
+ public static class Option {
+ /** The key of the option */
+ @NonNull public final String key;
+
+ /** The value of the option */
+ @NonNull public final String value;
+
+ /**
+ * Constructor for Option.
+ *
+ * @param key The key for this option
+ * @param value The value for this option
+ */
+ public Option(@NonNull final String key, @NonNull final String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Nullable
+ private static Option fromBundle(final GeckoBundle bundle) {
+ if (bundle == null) return null;
+ try {
+ final String text = bundle.getString("text", "");
+ final String value = bundle.getString("value", "");
+
+ if (text.isEmpty() || value.isEmpty()) {
+ throw new IllegalStateException(
+ "AddressFieldOption text or value should not be null");
+ }
+
+ return new Option(value, text);
+ } catch (final Exception e) {
+ Log.e(LOGTAG, "Could not deserialize AddressFieldOption: " + e);
+ return null;
+ }
+ }
+ }
+ }
+ }
+ }
+
/** Holds login information for a specific entry. */
public static class LoginEntry {
private static final String GUID_KEY = "guid";
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -15,6 +15,7 @@ exclude: true
## v146
- Added `getSafeBrowsingV5Enabled` and `setSafeBrowsingV5Enabled` to [`ContentBlocking.Settings`][146.1] to control whether to use the SafeBrowsing V5 protocol to access the Google SafeBrowsing service.
+- Added [`Autocomplete.AddressStructure`][146.2] API used to retrieve the structure of an address for a given country.
[146.1]: {{javadoc_uri}}/ContentBlocking.html
@@ -1851,4 +1852,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
-[api-version]: 934d10cc9408a3ac9eed888409108ac06b540280
+[api-version]: dcffdbd2bcd2bf198da5645acf5fb0908beac9f7
diff --git a/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs b/mobile/shared/components/geckoview/GeckoViewStartup.sys.mjs
@@ -221,6 +221,11 @@ export class GeckoViewStartup {
],
});
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewAutofillRuntime", {
+ module: "resource://gre/modules/GeckoViewAutofill.sys.mjs",
+ ged: ["GeckoView:Autofill:GetAddressStructure"],
+ });
+
GeckoViewUtils.addLazyGetter(this, "GeckoViewPreferences", {
module: "resource://gre/modules/GeckoViewPreferences.sys.mjs",
ged: [
diff --git a/mobile/shared/modules/geckoview/GeckoViewAutofill.sys.mjs b/mobile/shared/modules/geckoview/GeckoViewAutofill.sys.mjs
@@ -2,8 +2,14 @@
* 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 lazy = {};
+
import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs";
+ChromeUtils.defineESModuleGetters(lazy, {
+ FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
+});
+
class Autofill {
constructor(sessionId, eventDispatcher) {
this.eventDispatcher = eventDispatcher;
@@ -93,4 +99,25 @@ class AutofillManager {
export var gAutofillManager = new AutofillManager();
+// Runtime functionality
+export const GeckoViewAutofillRuntime = {
+ async onEvent(aEvent, aData, aCallback) {
+ debug`onEvent: event=${aEvent}, data=${aData}`;
+
+ switch (aEvent) {
+ case "GeckoView:Autofill:GetAddressStructure": {
+ debug`onEvent: event=${aEvent}, data=${aData}`;
+ const country = aData.country ? aData.country : "US";
+ const layout = lazy.FormAutofillUtils.getFormLayout({
+ country: `${country}`,
+ });
+ aCallback.onSuccess({
+ fields: layout,
+ });
+ break;
+ }
+ }
+ },
+};
+
const { debug, warn } = GeckoViewUtils.initLogging("Autofill");