This is in conformance with the guidelines that `.md` files under `docs/` must use 80-columns.
12 KiB
Android Settings Search Index
Android's settings search feature lets users type in the Settings search bar and
find any setting by name or summary. Every concrete PreferenceFragment
subclass must declare a SEARCH_INDEX_DATA_PROVIDER field and register it in
the central registry. A presubmit check enforcing this was added in Chromium
cr146.
This document explains how to implement this for Brave-specific fragments.
Overview
The indexing pipeline has two phases:
-
initPreferenceXml— runs once at app startup. Parses the fragment's preference XML, creates index entries, and establishes parent–child links so the search results show the correct breadcrumb path. -
updateDynamicPreferences— runs on every search. Removes or updates entries that depend on runtime state (feature flags, user prefs, policies).
Both phases write into a SettingsIndexData object. The search UI then queries
this data.
Choosing the right pattern
Pattern 1: INDEX_OPT_OUT
Use when the fragment has no static preferences to index — e.g. it inflates a custom View layout instead of a preference XML, or all its content is populated programmatically at runtime.
// No static preferences to index — custom view / dynamic content only.
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(
MyFragment.class.getName(), BaseSearchIndexProvider.INDEX_OPT_OUT);
Examples: BraveSyncScreensPreference, BraveWalletNetworksPreferenceFragment,
BlockedCredentialFragmentView, QuickSearchEnginesFragment.
Pattern 2: No initPreferenceXml override (Brave sub-screens in brave_main_preferences.xml)
Use when the fragment is reachable from brave_main_preferences.xml and its
entry has a correct android:fragment attribute.
BraveMainPreferencesBase.SEARCH_INDEX_DATA_PROVIDER processes
brave_main_preferences.xml via PreferenceParser.parseAndPopulate, which
creates the parent entry. resolveIndex() then establishes the child–parent
link automatically from the android:fragment value — no initPreferenceXml
override is needed.
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(
MyFragment.class.getName(), R.xml.my_fragment_preferences) {
@Override
public void updateDynamicPreferences(
Context context, SettingsIndexData indexData) {
// Remove prefs that are hidden by feature flags, policies, etc.
if (!ChromeFeatureList.isEnabled(BraveFeatureList.MY_FEATURE)) {
indexData.removeEntryForKey(MainSettings.class.getName(), PREF_MY_KEY);
return;
}
indexData.removeEntryForKey(MyFragment.class.getName(), "some_widget_key");
}
};
Examples: BraveLeoPreferences, BravePlaylistPreferences,
BravePrivacySettings, BraveSearchEnginesPreferences,
BraveWalletPreferences, BraveVpnPreferences.
Pattern 3: addChildParentLink
Use when the child–parent link cannot be established automatically from XML. This happens in two cases:
- The entry in
brave_main_preferences.xmlhas noandroid:fragment(e.g.brave_originuses a runtime click listener instead of fragment navigation). - The upstream XML references a different class than the Brave fragment
(e.g. the
passwordsentry usesBravePasswordsPreference— a customPreferencesubclass with noandroid:fragment— soresolveIndex()never finds a link toPasswordSettings).
public static final ChromeBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new ChromeBaseSearchIndexProvider(
MyBraveFragment.class.getName(), R.xml.my_brave_preferences) {
@Override
public void initPreferenceXml(
Context context,
Profile profile,
SettingsIndexData indexData,
Map<String, SearchIndexProvider> providerMap) {
super.initPreferenceXml(context, profile, indexData, providerMap);
String parentId = PreferenceParser.createUniqueId(
MainSettings.class.getName(), PREF_MY_KEY);
indexData.addChildParentLink(MyBraveFragment.class.getName(), parentId);
}
};
Use ChromeBaseSearchIndexProvider (instead of BaseSearchIndexProvider) when
you need access to a Profile in initPreferenceXml or
updateDynamicPreferences.
Examples: BraveOriginPreferences, PasswordSettings.
updateDynamicPreferences recipes
Removing a preference gated by a feature flag
if (!ChromeFeatureList.isEnabled(BraveFeatureList.MY_FEATURE)) {
indexData.removeEntryForKey(MyFragment.class.getName(), PREF_MY_KEY);
}
Removing the whole screen when a feature is disabled
Remove the parent entry — resolveIndex() will automatically prune all
child entries as orphans:
if (!ChromeFeatureList.isEnabled(BraveFeatureList.MY_FEATURE)) {
indexData.removeEntryForKey(MainSettings.class.getName(), PREF_MY_KEY);
return;
}
Dynamic summary (On/Off state)
boolean enabled = UserPrefs.get(profile).getBoolean(Pref.MY_PREF);
indexData.updateEntrySummaryForKey(
MyFragment.class.getName(),
PREF_MY_KEY,
enabled ? R.string.text_on : R.string.text_off);
Removing a preference with no title (custom widget)
Widgets that use android:layout but have no android:title appear in the
index with an empty title. Remove them explicitly:
indexData.removeEntryForKey(MyFragment.class.getName(), PREF_MY_WIDGET_KEY);
TextMessagePreference
These are automatically excluded from the index — no explicit removal needed.
Registering in the registry
All Brave providers are added to a single line in
chrome/android/java/src/org/chromium/chrome/browser/settings/search/SearchIndexProviderRegistry.java.
This file lives outside the brave repo and is maintained as a patch, so keeping
all Brave entries on one + line minimises upgrade friction.
Add your provider to the end of the existing Brave one-liner (before
TracingCategoriesSettings):
// existing Brave entries ..., org.chromium.chrome.browser.settings.MyFragment.SEARCH_INDEX_DATA_PROVIDER,
Presubmit check
The check CheckSettingsChanges in src/PRESUBMIT.py verifies:
- Every concrete settings fragment defines
SEARCH_INDEX_DATA_PROVIDER. - The provider is registered in
SearchIndexProviderRegistry.java. - UI patterns in changed lines (
.setSummary,.setVisible,.addPreference, bundle puts/gets) are mirrored by corresponding index API calls in the provider body.
| UI trigger | Required in provider body |
|---|---|
.setSummary( |
updateEntrySummaryForKey, addEntryForKey, or .setSummary |
.setVisible( / .removePreference( |
removeEntry or removeEntryForKey |
.addPreference( |
addEntry, addEntryForKey, updateEntry, or updateEntryForKey |
.put(String|Int|...)( |
getExtras |
getArguments().get / .containsKey( |
getExtras |
Run locally against specific files:
npm run presubmit -- --files "path/to/MyFragment.java"
Run against multiple files (semicolon-separated):
npm run presubmit -- --files "path/to/File1.java;path/to/File2.java"
Pitfalls
Javadoc "This class is responsible"
The presubmit's fallback class-name regex is class\s+(\w+), which matches
anywhere in the file — including inside javadoc comments. The phrase
"This class is responsible" produces a false match, extracting is as the
class name and causing a spurious "Provider not registered" warning.
Fix: reword the javadoc to start with the class name instead:
// Bad — triggers false match:
/** This class is responsible for rendering... */
// Good:
/** FooFragment is responsible for rendering... */
brave_main_preferences.xml requires correct android:fragment
BraveMainPreferencesBase.SEARCH_INDEX_DATA_PROVIDER processes
brave_main_preferences.xml via PreferenceParser.parseAndPopulate. For each
entry with a correct android:fragment, resolveIndex() establishes the
child–parent link automatically. If android:fragment is missing or wrong, the
link is never established and the fragment's entries are pruned as orphans — use
addChildParentLink in initPreferenceXml to register the link manually
(Pattern 3).
Wrong android:fragment class name causes silent non-clickable results
If android:fragment in an XML contains a typo or wrong package (e.g. missing
.settings subpackage), Class.forName() silently swallows the
ClassNotFoundException. The preference still appears in search results but
clicking it does nothing.
Fix: double-check the fully-qualified class name in the XML matches the actual fragment class.
Remove the upstream provider when Brave replaces an upstream fragment
When a Brave fragment fully replaces an upstream one (e.g.
BraveMainPreferencesBase replaces MainSettings, BravePrivacySettings
replaces PrivacySettings), remove the upstream SEARCH_INDEX_DATA_PROVIDER
from SearchIndexProviderRegistry and register only the Brave one.
If both are registered, the upstream provider still runs and its
updateDynamicPreferences may call updateEntrySummaryForKey on entries that
the Brave provider has already removed with removeEntry. At that point
resolveIndex() has pruned those entries and the upstream call throws
IllegalStateException: Existing ID cannot be found.
Concretely:
MainSettings.SEARCH_INDEX_DATA_PROVIDERis replaced byBraveMainPreferencesBase.SEARCH_INDEX_DATA_PROVIDER, which internally calls the upstreamupdateDynamicPreferencesfirst and then applies Brave-specific removals on top.PrivacySettings.SEARCH_INDEX_DATA_PROVIDERis replaced byBravePrivacySettings.SEARCH_INDEX_DATA_PROVIDER. Keeping the upstream one would cause it to callupdateEntrySummaryForKey("privacy_sandbox")afterBraveMainPreferencesBasehas already removed the"privacy"parent entry, crashing the index build.
Leaf prefs with no sub-screen are non-clickable in search results
The search result click handler navigates to a sub-screen fragment. Preferences
that have no android:fragment (switches, simple actions) cannot be navigated
to — clicking the search result does nothing.
Chrome never places such leaf preferences directly in main settings, so there is no upstream infrastructure for handling them. Remove them from the index.
The following prefs in brave_main_preferences.xml are excluded for this reason
in BraveMainPreferencesBase.SEARCH_INDEX_DATA_PROVIDER:
| Key | Description |
|---|---|
closing_all_tabs_closes_brave |
Toggle switch — no sub-screen |
rate_brave |
Rate-app action — no sub-screen |
autofill_private_window |
Toggle switch — no sub-screen |
use_custom_tabs |
Toggle switch — no sub-screen |
home_screen_widget |
Conditionally excluded when isRequestPinAppWidgetSupported() is false |
Upstream XML references upstream class, not Brave subclass
When Brave subclasses an upstream fragment (e.g.
BravePrivacySettings extends PrivacySettings), the upstream XML still has
android:fragment="...PrivacySettings". The recursive link resolution follows
the upstream class, not the Brave one. Use addChildParentLink in
initPreferenceXml to wire up the Brave subclass explicitly.