diff --git a/android/BUILD.gn b/android/BUILD.gn
index 430baccec1f..b295d036ae8 100644
--- a/android/BUILD.gn
+++ b/android/BUILD.gn
@@ -540,6 +540,7 @@ android_resources("java_resources") {
"java/brave-res/layout/brave_ads_existing_user_dialog_layout.xml",
"java/brave-res/layout/brave_ads_new_user_dialog_layout.xml",
"java/brave-res/layout/brave_ask_play_store_rating_dialog.xml",
+ "java/brave-res/layout/brave_autofill_service_item.xml",
"java/brave-res/layout/brave_bookmark_toolbar.xml",
"java/brave-res/layout/brave_bottom_new_tab_button.xml",
"java/brave-res/layout/brave_custom_tabs_toolbar.xml",
@@ -720,6 +721,7 @@ android_resources("java_resources") {
"java/brave-res/values/brave_styles.xml",
"java/brave-res/values/dimens.xml",
"java/brave-res/values/shimmer_attrs.xml",
+ "java/brave-res/xml/autofill_service_configuration.xml",
"java/brave-res/xml/background_images_preferences.xml",
"java/brave-res/xml/brave_appearance_preferences.xml",
"java/brave-res/xml/brave_download_preferences.xml",
diff --git a/android/brave_java_sources.gni b/android/brave_java_sources.gni
index b1544864c56..591b13fb094 100644
--- a/android/brave_java_sources.gni
+++ b/android/brave_java_sources.gni
@@ -10,6 +10,7 @@ import(
"//brave/android/java/org/chromium/chrome/browser/fullscreen/brave_fullscreen_java_sources.gni")
import("//brave/android/java/org/chromium/chrome/browser/playlist/sources.gni")
import("//brave/base/brave_base_shared_preferences_java.gni")
+import("//brave/browser/autofill/android/java_sources.gni")
import("//brave/browser/bookmarks/android/sources.gni")
import("//brave/browser/brave_ads/android/java_sources.gni")
import("//brave/browser/hub/internal/android/java/java_sources.gni")
@@ -506,6 +507,7 @@ brave_java_sources = [
]
brave_java_sources += brave_ads_java_sources
+brave_java_sources += brave_browser_autofill_service_java_sources
brave_java_sources += brave_components_embedder_support_java_sources
brave_java_sources += brave_components_signin_java_sources
brave_java_sources += brave_feed_java_sources
@@ -543,6 +545,8 @@ brave_java_base_sources = [
]
brave_java_base_module_sources = [
+ "//brave/android/java/org/chromium/chrome/browser/autofill/BraveAutofillService.java",
+ "//brave/android/java/org/chromium/chrome/browser/base/SplitCompatAutofillService.java",
"//brave/android/java/org/chromium/chrome/browser/base/SplitCompatJobIntentService.java",
"//brave/android/java/org/chromium/chrome/browser/media/ui/BraveMediaNotificationControllerServices.java",
"//brave/android/java/org/chromium/chrome/browser/playlist/hls_content/HlsService.java",
diff --git a/android/java/AndroidManifest.xml b/android/java/AndroidManifest.xml
index 1dfc960047c..ac5c7e8d39e 100644
--- a/android/java/AndroidManifest.xml
+++ b/android/java/AndroidManifest.xml
@@ -180,6 +180,19 @@
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/java/brave-res/xml/autofill_service_configuration.xml b/android/java/brave-res/xml/autofill_service_configuration.xml
new file mode 100644
index 00000000000..cc1515cbe49
--- /dev/null
+++ b/android/java/brave-res/xml/autofill_service_configuration.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/java/org/chromium/chrome/browser/autofill/BraveAutofillService.java b/android/java/org/chromium/chrome/browser/autofill/BraveAutofillService.java
new file mode 100644
index 00000000000..75b545dcf79
--- /dev/null
+++ b/android/java/org/chromium/chrome/browser/autofill/BraveAutofillService.java
@@ -0,0 +1,22 @@
+/* Copyright (c) 2026 The Brave Authors. All rights reserved.
+ * 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 https://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser.autofill;
+
+import org.chromium.build.annotations.IdentifierNameString;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.base.SplitCompatAutofillService;
+
+/** See {@link BraveAutofillServiceImpl}. */
+@NullMarked
+public class BraveAutofillService extends SplitCompatAutofillService {
+ @SuppressWarnings("FieldCanBeFinal") // @IdentifierNameString requires non-final
+ private static @IdentifierNameString String sImplClassName =
+ "org.chromium.chrome.browser.autofill.BraveAutofillServiceImpl";
+
+ public BraveAutofillService() {
+ super(sImplClassName);
+ }
+}
diff --git a/android/java/org/chromium/chrome/browser/base/SplitCompatAutofillService.java b/android/java/org/chromium/chrome/browser/base/SplitCompatAutofillService.java
new file mode 100644
index 00000000000..a465e79546c
--- /dev/null
+++ b/android/java/org/chromium/chrome/browser/base/SplitCompatAutofillService.java
@@ -0,0 +1,90 @@
+/* Copyright (c) 2026 The Brave Authors. All rights reserved.
+ * 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 https://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser.base;
+
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.service.autofill.AutofillService;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillRequest;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveRequest;
+
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+
+/**
+ * AutofillService base class which will call through to the given {@link Impl}. This class must be
+ * present in the base module, while the Impl can be in the chrome module.
+ */
+@NullMarked
+public class SplitCompatAutofillService extends AutofillService {
+ private final String mServiceClassName;
+ private Impl mImpl;
+
+ public SplitCompatAutofillService(String serviceClassName) {
+ mServiceClassName = serviceClassName;
+ }
+
+ @Override
+ protected void attachBaseContext(Context baseContext) {
+ mImpl =
+ (Impl)
+ SplitCompatUtils.loadClassAndAdjustContextChrome(
+ baseContext, mServiceClassName);
+ mImpl.setService(this);
+ super.attachBaseContext(baseContext);
+ }
+
+ @Override
+ public void onFillRequest(
+ FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
+ mImpl.onFillRequest(request, cancellationSignal, callback);
+ }
+
+ @Override
+ public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+ mImpl.onSaveRequest(request, callback);
+ }
+
+ @Override
+ public void onConnected() {
+ super.onConnected();
+ mImpl.onConnected();
+ }
+
+ @Override
+ public void onDisconnected() {
+ super.onDisconnected();
+ mImpl.onDisconnected();
+ }
+
+ /**
+ * Holds the implementation of autofill service logic. Will be called by {@link
+ * SplitCompatAutofillService}.
+ */
+ public abstract static class Impl {
+ private SplitCompatAutofillService mService;
+
+ @Initializer
+ protected final void setService(SplitCompatAutofillService service) {
+ mService = service;
+ }
+
+ protected final AutofillService getService() {
+ return mService;
+ }
+
+ public abstract void onFillRequest(
+ FillRequest request, CancellationSignal cancellationSignal, FillCallback callback);
+
+ public abstract void onSaveRequest(SaveRequest request, SaveCallback callback);
+
+ public void onConnected() {}
+
+ public void onDisconnected() {}
+ }
+}
diff --git a/android/junit/BUILD.gn b/android/junit/BUILD.gn
index 1252333a034..e0933e4d348 100644
--- a/android/junit/BUILD.gn
+++ b/android/junit/BUILD.gn
@@ -140,6 +140,16 @@ if (is_android) {
deps = [ "//chrome/android/junit:chrome_junit_tests_helper" ]
}
+ robolectric_library(
+ "brave_junit_tests_org.chromium.chrome.browser.autofill") {
+ sources = [
+ "src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImplUnitTest.java",
+ "src/org/chromium/chrome/browser/autofill/BraveAutofillViewStructureParserUnitTest.java",
+ ]
+
+ deps = [ "//chrome/android/junit:chrome_junit_tests_helper" ]
+ }
+
robolectric_library(
"brave_junit_tests_org.chromium.chrome.browser.custom_search_engines") {
sources = [ "src/org/chromium/brave/browser/custom_search_engines/CustomSearchEnginesManagerTest.java" ]
diff --git a/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImplUnitTest.java b/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImplUnitTest.java
new file mode 100644
index 00000000000..cc1d74951a5
--- /dev/null
+++ b/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImplUnitTest.java
@@ -0,0 +1,218 @@
+/* Copyright (c) 2026 The Brave Authors. All rights reserved.
+ * 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 https://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser.autofill;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+/** Unit tests for pure utility methods in {@link BraveAutofillServiceImpl}. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class BraveAutofillServiceImplUnitTest {
+
+ // --- splitName ---
+
+ @Test
+ @SmallTest
+ public void splitName_fullNameOnly_splitsIntoFirstAndLast() {
+ assertArrayEquals(
+ new String[] {"Jane", "Doe"},
+ BraveAutofillServiceImpl.splitName("Jane Doe", null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_singleWordFullName_firstNameOnly() {
+ assertArrayEquals(
+ new String[] {"Madonna", ""},
+ BraveAutofillServiceImpl.splitName("Madonna", null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_multiWordLastName_preservesRemainder() {
+ assertArrayEquals(
+ new String[] {"Anna", "Maria Van Der Berg"},
+ BraveAutofillServiceImpl.splitName("Anna Maria Van Der Berg", null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_existingPartsNotOverridden() {
+ assertArrayEquals(
+ new String[] {"Jonathan", "Smithson"},
+ BraveAutofillServiceImpl.splitName("John Smith", "Jonathan", "Smithson"));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_allNull_returnsEmptyStrings() {
+ assertArrayEquals(
+ new String[] {"", ""}, BraveAutofillServiceImpl.splitName(null, null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_emptyFullName_returnsEmptyStrings() {
+ assertArrayEquals(
+ new String[] {"", ""}, BraveAutofillServiceImpl.splitName("", null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_fullNameWithExtraWhitespace_trims() {
+ assertArrayEquals(
+ new String[] {"Jane", "Doe"},
+ BraveAutofillServiceImpl.splitName(" Jane Doe ", null, null));
+ }
+
+ @Test
+ @SmallTest
+ public void splitName_firstNameEmpty_lastNameProvided_derivesFirstOnly() {
+ assertArrayEquals(
+ new String[] {"Jane", "Smith"},
+ BraveAutofillServiceImpl.splitName("Jane Doe", "", "Smith"));
+ }
+
+ // --- countryCodeToDisplayName ---
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_us_returnsUnitedStates() {
+ assertEquals("United States", BraveAutofillServiceImpl.countryCodeToDisplayName("US"));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_gb_returnsUnitedKingdom() {
+ assertEquals("United Kingdom", BraveAutofillServiceImpl.countryCodeToDisplayName("GB"));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_de_returnsGermany() {
+ assertEquals("Germany", BraveAutofillServiceImpl.countryCodeToDisplayName("DE"));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_null_returnsEmptyString() {
+ assertEquals("", BraveAutofillServiceImpl.countryCodeToDisplayName(null));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_emptyString_returnsEmpty() {
+ assertEquals("", BraveAutofillServiceImpl.countryCodeToDisplayName(""));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_threeLetterCode_returnsUnchanged() {
+ assertEquals("USA", BraveAutofillServiceImpl.countryCodeToDisplayName("USA"));
+ }
+
+ @Test
+ @SmallTest
+ public void countryCodeToDisplayName_lowercaseCode_returnsDisplayName() {
+ // java.util.Locale handles lowercase country codes.
+ assertEquals("United States", BraveAutofillServiceImpl.countryCodeToDisplayName("us"));
+ }
+
+ // --- matchListOption ---
+
+ @Test
+ @SmallTest
+ public void matchListOption_exactMatch_returnsIndex() {
+ CharSequence[] options = {"Alabama", "California", "Texas"};
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "California"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_exactMatchCaseInsensitive_returnsIndex() {
+ CharSequence[] options = {"Alabama", "California", "Texas"};
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "california"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_optionStartsWithValue_returnsIndex() {
+ // Pass 2: value "CA" matches option "California".
+ CharSequence[] options = {"Alabama", "California", "Texas"};
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "CA"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_valueStartsWithOption_returnsIndex() {
+ // Pass 3: value "California" matches option "CA".
+ CharSequence[] options = {"AL", "CA", "TX"};
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "California"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_noMatch_returnsNegativeOne() {
+ CharSequence[] options = {"Alabama", "California", "Texas"};
+ assertEquals(-1, BraveAutofillServiceImpl.matchListOption(options, "New York"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_nullOptions_returnsNegativeOne() {
+ assertEquals(-1, BraveAutofillServiceImpl.matchListOption(null, "California"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_emptyOptions_returnsNegativeOne() {
+ assertEquals(
+ -1, BraveAutofillServiceImpl.matchListOption(new CharSequence[0], "California"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_singleCharValue_skipsPass2() {
+ // Single character value should not match via prefix (pass 2 requires length >= 2).
+ CharSequence[] options = {"Cat", "Car", "Cap"};
+ assertEquals(-1, BraveAutofillServiceImpl.matchListOption(options, "C"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_nullOptionElement_skipped() {
+ // Null elements in the options array should be safely skipped without NPE.
+ CharSequence[] options = {null, "California", "Texas"};
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "California"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_exactMatchPreferredOverPrefix() {
+ // "CA" should match index 0 (exact) not index 1 (prefix).
+ CharSequence[] options = {"CA", "California"};
+ assertEquals(0, BraveAutofillServiceImpl.matchListOption(options, "CA"));
+ }
+
+ @Test
+ @SmallTest
+ public void matchListOption_singleCharOption_skipsPass3() {
+ // Pass 3 requires option length >= 2, so "C" is skipped.
+ CharSequence[] options = {"C", "CALIF"};
+ // "California" does not exact-match either, does not prefix-match either,
+ // but starts with "CALIF" (pass 3).
+ assertEquals(1, BraveAutofillServiceImpl.matchListOption(options, "California"));
+ }
+}
diff --git a/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillViewStructureParserUnitTest.java b/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillViewStructureParserUnitTest.java
new file mode 100644
index 00000000000..211d58bc8a8
--- /dev/null
+++ b/android/junit/src/org/chromium/chrome/browser/autofill/BraveAutofillViewStructureParserUnitTest.java
@@ -0,0 +1,461 @@
+/* Copyright (c) 2026 The Brave Authors. All rights reserved.
+ * 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 https://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser.autofill;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.view.View;
+import android.view.autofill.AutofillId;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+import org.chromium.base.test.BaseRobolectricTestRunner;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Unit tests for pure methods in {@link BraveAutofillViewStructureParser}. */
+@RunWith(BaseRobolectricTestRunner.class)
+@Config(manifest = Config.NONE)
+public class BraveAutofillViewStructureParserUnitTest {
+
+ private static Map mapWithKeys(String... keys) {
+ Map map = new HashMap<>();
+ for (int i = 0; i < keys.length; i++) {
+ map.put(keys[i], new AutofillId(i + 1));
+ }
+ return map;
+ }
+
+ // --- hasAddressFields ---
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withPostalAddress_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_POSTAL_ADDRESS)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withName_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_NAME)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withFirstName_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(BraveAutofillViewStructureParser.HINT_FIRST_NAME)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withEmail_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_EMAIL_ADDRESS)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withPhone_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_PHONE)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withPostalCode_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_POSTAL_CODE)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withCity_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(BraveAutofillViewStructureParser.HINT_CITY)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withState_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(BraveAutofillViewStructureParser.HINT_STATE)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withCountry_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(BraveAutofillViewStructureParser.HINT_COUNTRY)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withCompany_returnsTrue() {
+ assertTrue(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(BraveAutofillViewStructureParser.HINT_COMPANY)));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_emptyMap_returnsFalse() {
+ assertFalse(BraveAutofillViewStructureParser.hasAddressFields(new HashMap<>()));
+ }
+
+ @Test
+ @SmallTest
+ public void hasAddressFields_withPasswordOnly_returnsFalse() {
+ assertFalse(
+ BraveAutofillViewStructureParser.hasAddressFields(
+ mapWithKeys(View.AUTOFILL_HINT_PASSWORD)));
+ }
+
+ // --- mapAutofillHint: Android standard hints ---
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_androidNameHint_passesThrough() {
+ assertEquals(
+ View.AUTOFILL_HINT_NAME,
+ BraveAutofillViewStructureParser.mapAutofillHint(View.AUTOFILL_HINT_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_androidEmailHint_passesThrough() {
+ assertEquals(
+ View.AUTOFILL_HINT_EMAIL_ADDRESS,
+ BraveAutofillViewStructureParser.mapAutofillHint(View.AUTOFILL_HINT_EMAIL_ADDRESS));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_androidPhoneHint_passesThrough() {
+ assertEquals(
+ View.AUTOFILL_HINT_PHONE,
+ BraveAutofillViewStructureParser.mapAutofillHint(View.AUTOFILL_HINT_PHONE));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_androidPasswordHint_passesThrough() {
+ assertEquals(
+ View.AUTOFILL_HINT_PASSWORD,
+ BraveAutofillViewStructureParser.mapAutofillHint(View.AUTOFILL_HINT_PASSWORD));
+ }
+
+ // --- mapAutofillHint: W3C tokens ---
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cGivenName_returnsFirstName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_FIRST_NAME,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_GIVEN_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cFamilyName_returnsLastName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_LAST_NAME,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_FAMILY_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cAdditionalName_returnsMiddleName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_MIDDLE_NAME,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ADDITIONAL_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cStreetAddress_returnsPostalAddress() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_ADDRESS,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_STREET_ADDRESS));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cAddressLine1_returnsPostalAddress() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_ADDRESS,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ADDRESS_LINE1));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cAddressLine2_returnsAddressLine2() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_ADDRESS_LINE2,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ADDRESS_LINE2));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cPostalCode_returnsPostalCode() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_CODE,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_POSTAL_CODE));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cOrganization_returnsCompany() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_COMPANY,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ORGANIZATION));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cTel_returnsPhone() {
+ assertEquals(
+ View.AUTOFILL_HINT_PHONE,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_TEL));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cTelNational_returnsPhone() {
+ assertEquals(
+ View.AUTOFILL_HINT_PHONE,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_TEL_NATIONAL));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cAddressLevel1_returnsState() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_STATE,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ADDRESS_LEVEL1));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cAddressLevel2_returnsCity() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_CITY,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_ADDRESS_LEVEL2));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cCountry_returnsCountry() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_COUNTRY,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_COUNTRY));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_w3cCountryName_returnsCountry() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_COUNTRY,
+ BraveAutofillViewStructureParser.mapAutofillHint(
+ BraveAutofillViewStructureParser.W3C_COUNTRY_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapAutofillHint_unknownHint_returnsNull() {
+ assertNull(BraveAutofillViewStructureParser.mapAutofillHint("some-unknown-hint"));
+ }
+
+ // --- mapComputedAutofillHint ---
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameFull_returnsName() {
+ assertEquals(
+ View.AUTOFILL_HINT_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_FULL));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameFirst_returnsFirstName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_FIRST_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_FIRST));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameMiddle_returnsMiddleName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_MIDDLE_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_MIDDLE));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameMiddleInitial_returnsMiddleName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_MIDDLE_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_MIDDLE_INITIAL));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameLast_returnsLastName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_LAST_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_LAST));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_nameLastFirst_returnsLastName() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_LAST_NAME,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_NAME_LAST_FIRST));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_emailAddress_returnsEmail() {
+ assertEquals(
+ View.AUTOFILL_HINT_EMAIL_ADDRESS,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_EMAIL_ADDRESS));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_phoneHomeWholeNumber_returnsPhone() {
+ assertEquals(
+ View.AUTOFILL_HINT_PHONE,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_PHONE_HOME_WHOLE_NUMBER));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeLine1_returnsPostalAddress() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_ADDRESS,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_LINE1));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeStreetAddress_returnsPostalAddress() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_ADDRESS,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_STREET_ADDRESS));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeLine2_returnsAddressLine2() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_ADDRESS_LINE2,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_LINE2));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeCity_returnsCity() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_CITY,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_CITY));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeState_returnsState() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_STATE,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_STATE));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeZip_returnsPostalCode() {
+ assertEquals(
+ View.AUTOFILL_HINT_POSTAL_CODE,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_ZIP));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_addressHomeCountry_returnsCountry() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_COUNTRY,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_ADDRESS_HOME_COUNTRY));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_companyName_returnsCompany() {
+ assertEquals(
+ BraveAutofillViewStructureParser.HINT_COMPANY,
+ BraveAutofillViewStructureParser.mapComputedAutofillHint(
+ BraveAutofillViewStructureParser.COMPUTED_COMPANY_NAME));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_null_returnsNull() {
+ assertNull(BraveAutofillViewStructureParser.mapComputedAutofillHint(null));
+ }
+
+ @Test
+ @SmallTest
+ public void mapComputedAutofillHint_unknownValue_returnsNull() {
+ assertNull(BraveAutofillViewStructureParser.mapComputedAutofillHint("UNKNOWN_FIELD_TYPE"));
+ }
+}
diff --git a/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImpl.java b/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImpl.java
new file mode 100644
index 00000000000..a70dc4edc65
--- /dev/null
+++ b/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/BraveAutofillServiceImpl.java
@@ -0,0 +1,612 @@
+/* Copyright (c) 2026 The Brave Authors. All rights reserved.
+ * 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 https://mozilla.org/MPL/2.0/. */
+
+package org.chromium.chrome.browser.autofill;
+
+import android.app.assist.AssistStructure;
+import android.app.assist.AssistStructure.ViewNode;
+import android.os.CancellationSignal;
+import android.service.autofill.Dataset;
+import android.service.autofill.FillCallback;
+import android.service.autofill.FillContext;
+import android.service.autofill.FillRequest;
+import android.service.autofill.FillResponse;
+import android.service.autofill.SaveCallback;
+import android.service.autofill.SaveInfo;
+import android.service.autofill.SaveRequest;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.widget.RemoteViews;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.base.SplitCompatAutofillService;
+import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
+import org.chromium.chrome.browser.profiles.ProfileManager;
+import org.chromium.components.autofill.AutofillProfile;
+import org.chromium.components.autofill.FieldType;
+import org.chromium.components.autofill.RecordType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of the Brave autofill service for filling address data in third-party apps.
+ *
+ *
This class lives in the chrome module (split_chrome.apk) and is loaded at runtime by {@link
+ * SplitCompatAutofillService} from the base module. It extends {@code
+ * SplitCompatAutofillService.Impl} which delegates the Android {@link
+ * android.service.autofill.AutofillService} callbacks here.
+ *
+ *
When the user selects Brave as their autofill provider in Android Settings, the system calls
+ * {@link #onFillRequest} whenever a fillable field is focused in any third-party app (Chrome,
+ * Firefox, native apps, etc.). This class then:
+ *
+ *
+ *
Initializes the Brave browser engine (needed to access saved profiles).
+ *
Parses the app's view hierarchy ({@link AssistStructure}) to detect address fields using
+ * {@link BraveAutofillViewStructureParser}.
+ *
Queries Brave's saved address profiles via {@link PersonalDataManager}.
+ *
Builds a {@link FillResponse} with {@link Dataset} entries (one per saved profile) that the
+ * system shows as a dropdown popup in the requesting app.
+ *
Registers {@link SaveInfo} so the system shows a "Save to Brave?" prompt when the user
+ * manually fills a form and submits it.
+ *
+ *
+ *
The service skips its own package (Brave handles its own autofill internally).
+ */
+@NullMarked
+public class BraveAutofillServiceImpl extends SplitCompatAutofillService.Impl {
+ private static final String TAG = "BraveAutofillSvc";
+
+ /**
+ * Called by the Android framework when a fillable field is focused in a third-party app.
+ *
+ *
Flow: init browser → parse view structure → detect address fields → query saved profiles →
+ * build fill response with datasets and save info → return to framework.
+ */
+ @Override
+ public void onFillRequest(
+ FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) {
+ if (!initBrowser()) {
+ callback.onSuccess(null);
+ return;
+ }
+
+ List fillContexts = request.getFillContexts();
+ if (fillContexts.isEmpty()) {
+ callback.onSuccess(null);
+ return;
+ }
+
+ // Get the most recent AssistStructure (the last one is the current screen).
+ AssistStructure structure = fillContexts.get(fillContexts.size() - 1).getStructure();
+ if (structure == null || structure.getWindowNodeCount() == 0) {
+ callback.onSuccess(null);
+ return;
+ }
+
+ // Skip our own app — Brave handles its own autofill internally.
+ String packageName = structure.getActivityComponent().getPackageName();
+ if (ContextUtils.getApplicationContext().getPackageName().equalsIgnoreCase(packageName)) {
+ callback.onSuccess(null);
+ return;
+ }
+
+ // Parse the view hierarchy to find autofillable fields and keep a reference to each
+ // ViewNode (needed later to determine field types like text vs dropdown).
+ Map nodesMap = new HashMap<>();
+ Map fields =
+ BraveAutofillViewStructureParser.findAutofillableFields(structure, nodesMap);
+ if (fields.isEmpty()) {
+ callback.onSuccess(null);
+ return;
+ }
+
+ if (BraveAutofillViewStructureParser.hasAddressFields(fields)) {
+ fillAddressAutofill(fields, nodesMap, callback);
+ } else {
+ callback.onSuccess(null);
+ }
+ }
+
+ /**
+ * Called by the Android framework after the user fills a form manually and the system shows a
+ * "Save to Brave?" prompt (triggered by the {@link SaveInfo} we set in {@link
+ * #fillAddressAutofill}). Only called if the user taps "Save".
+ *
+ *
Extracts the filled values from the view structure and saves them as a new address profile
+ * in Brave's {@link PersonalDataManager}.
+ */
+ @Override
+ public void onSaveRequest(SaveRequest request, SaveCallback callback) {
+ if (!initBrowser()) {
+ callback.onSuccess();
+ return;
+ }
+
+ List fillContexts = request.getFillContexts();
+ if (fillContexts.isEmpty()) {
+ callback.onSuccess();
+ return;
+ }
+
+ AssistStructure structure = fillContexts.get(fillContexts.size() - 1).getStructure();
+ if (structure == null || structure.getWindowNodeCount() == 0) {
+ callback.onSuccess();
+ return;
+ }
+
+ Map nodesMap = new HashMap<>();
+ Map fields =
+ BraveAutofillViewStructureParser.findAutofillableFields(structure, nodesMap);
+ if (fields.isEmpty()) {
+ callback.onSuccess();
+ return;
+ }
+
+ if (BraveAutofillViewStructureParser.hasAddressFields(fields)) {
+ saveAddressAutofill(fields, nodesMap);
+ }
+
+ callback.onSuccess();
+ }
+
+ /**
+ * Initializes the Brave browser engine. The browser must be running to access saved profiles
+ * via {@link PersonalDataManager}. Returns false if initialization fails.
+ */
+ private boolean initBrowser() {
+ try {
+ ChromeBrowserInitializer.getInstance().handleSynchronousStartup();
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to initialize browser", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Builds a {@link FillResponse} containing one {@link Dataset} per saved address profile.
+ *
+ *
For each profile, maps detected form fields to profile data (name, email, phone, address,
+ * etc.). Each field in the dataset gets an appropriate {@link AutofillValue}: {@code forText()}
+ * for text inputs or {@code forList(index)} for dropdowns/selects.
+ *
+ *
The response also includes {@link SaveInfo} so the system prompts "Save to Brave?" when
+ * the user fills a form manually — even if we had no profiles to suggest.
+ *
+ * @param fields Map of hint key → AutofillId from the view structure parser.
+ * @param nodesMap Map of AutofillId → ViewNode for determining field types.
+ * @param callback The framework callback to deliver the response.
+ */
+ private void fillAddressAutofill(
+ Map fields,
+ Map nodesMap,
+ FillCallback callback) {
+ PersonalDataManager personalDataManager =
+ PersonalDataManagerFactory.getForProfile(
+ // This is a top-level entry point (AutofillService) with no
+ // Profile passed in from the framework.
+ ProfileManager.getLastUsedRegularProfile());
+ ArrayList profileList = personalDataManager.getProfilesToSuggest();
+
+ FillResponse.Builder fillResponse = new FillResponse.Builder();
+ String appPackageName = ContextUtils.getApplicationContext().getPackageName();
+
+ if (profileList == null) {
+ profileList = new ArrayList<>();
+ }
+
+ // Build one Dataset per saved profile. Each dataset fills all matching fields at once
+ // when the user taps the suggestion.
+ for (AutofillProfile profile : profileList) {
+ // Map detected field hints to profile data values. Only fields that exist in
+ // both the form and the profile (non-empty) get included.
+ Map autofillDataMap = new HashMap<>();
+ String fullName = profile.getInfo(FieldType.NAME_FULL);
+ String[] nameParts =
+ splitName(
+ fullName,
+ profile.getInfo(FieldType.NAME_FIRST),
+ profile.getInfo(FieldType.NAME_LAST));
+ String firstName = nameParts[0];
+ String lastName = nameParts[1];
+ mapField(fields, View.AUTOFILL_HINT_NAME, fullName, autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_FIRST_NAME,
+ firstName,
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_MIDDLE_NAME,
+ profile.getInfo(FieldType.NAME_MIDDLE),
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_LAST_NAME,
+ lastName,
+ autofillDataMap);
+ mapField(
+ fields,
+ View.AUTOFILL_HINT_EMAIL_ADDRESS,
+ profile.getInfo(FieldType.EMAIL_ADDRESS),
+ autofillDataMap);
+ // Strip whitespace from phone numbers — profiles may store them as
+ // "+1 234 567 8901" but phone input fields typically expect no spaces.
+ String phone = profile.getInfo(FieldType.PHONE_HOME_WHOLE_NUMBER);
+ if (phone != null) {
+ phone = phone.replaceAll("\\s", "");
+ }
+ mapField(fields, View.AUTOFILL_HINT_PHONE, phone, autofillDataMap);
+ mapField(
+ fields,
+ View.AUTOFILL_HINT_POSTAL_ADDRESS,
+ profile.getInfo(FieldType.ADDRESS_HOME_STREET_ADDRESS),
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_ADDRESS_LINE2,
+ profile.getInfo(FieldType.ADDRESS_HOME_LINE2),
+ autofillDataMap);
+ mapField(
+ fields,
+ View.AUTOFILL_HINT_POSTAL_CODE,
+ profile.getInfo(FieldType.ADDRESS_HOME_ZIP),
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_CITY,
+ profile.getInfo(FieldType.ADDRESS_HOME_CITY),
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_STATE,
+ profile.getInfo(FieldType.ADDRESS_HOME_STATE),
+ autofillDataMap);
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_COMPANY,
+ profile.getInfo(FieldType.COMPANY_NAME),
+ autofillDataMap);
+ String countryValue =
+ countryCodeToDisplayName(profile.getInfo(FieldType.ADDRESS_HOME_COUNTRY));
+ mapField(
+ fields,
+ BraveAutofillViewStructureParser.HINT_COUNTRY,
+ countryValue,
+ autofillDataMap);
+
+ if (!autofillDataMap.isEmpty()) {
+ Dataset.Builder dataset = new Dataset.Builder();
+ for (Map.Entry entry : autofillDataMap.entrySet()) {
+ AutofillId fieldId = entry.getKey();
+ String value = entry.getValue();
+ ViewNode node = nodesMap.get(fieldId);
+
+ // Create the right AutofillValue type: forText() for text inputs,
+ // forList(index) for