tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmobile/android/geckoview/api.txt | 26++++++++++++++++++++++++++
Mmobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AutocompleteTest.kt | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/geckoview/src/main/java/org/mozilla/geckoview/Autocomplete.java | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md | 3++-
Mmobile/shared/components/geckoview/GeckoViewStartup.sys.mjs | 5+++++
Mmobile/shared/modules/geckoview/GeckoViewAutofill.sys.mjs | 27+++++++++++++++++++++++++++
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");