Settings search enables you to quickly and easily search for and change specific settings in the Automotive Settings app without navigating through app menus to find it. Search is the most effective way to find a specific setting. By default, search finds AOSP settings only. Additional settings, whether injected or not, require additional changes in order to be indexed.
Requirements
For a setting to be indexable by Settings search, data must come from a:
SearchIndexable
fragment insideCarSettings
.- System-level app.
Define the data
Common fields:
Key
. (Required) Unique human readable String key to identify the result.IconResId
. Optional If an icon appears in your app next to the result, then add the resource id, otherwise ignore.IntentAction
. Required ifIntentTargetPackage
orIntentTargetClass
is not defined. Defines the action that the search result intent is to take.IntentTargetPackage
. Required ifIntentAction
is not defined. Defines the package that the search result intent is to resolve to.IntentTargetClass
. Required ifIntentAction
is not defined. Defines the class (activity) that the search result intent is to resolve to.
SearchIndexableResource
only:
XmlResId
. (Required) Defines the XML resource id of the page containing the results to be indexed.
SearchIndexableRaw
only:
Title
. (Required) Title of the search result.SummaryOn
. (Optional) Summary of the search result.Keywords
. (Optional) List of words associated with the search result. Matches the query to your result.ScreenTitle
. (Optional) Title of the page with your search result.
Hide data
Each search result appears in Search unless it's marked otherwise. While static search results are cached, a fresh list of nonindexable keys is retrieved each time search is opened. Reasons for hiding results can include:
- Duplicate. For example, appears on multiple pages.
- Only shown conditionally. For example, only shows mobile data settings when a SIM card is present).
- Templated page. For example, a details page for an individual app.
- Setting needs more context than a Title and Subtitle. For example, a "Settings" setting, which is only relevant to the screen title.
To hide a setting, your provider or SEARCH_INDEX_DATA_PROVIDER
should
return the search result's key from getNonIndexableKeys
. The key can
always be returned (duplicate, templated page cases) or conditionally added (no mobile
data case).
Static index
Use the static index if your index data is always the same. For example, the title and summary of XML data or the hard code raw data. The static data is indexed only once when the Settings search is first launched.
For indexables inside settings, implement getXmlResourcesToIndex
and/or getRawDataToIndex
. For injected settings, implement the
queryXmlResources
and/or queryRawData
methods.
Dynamic index
If the indexable data can be updated accordingly, use the dynamic method to index your data. Settings search update this dynamic list when it's launched.
For indexables inside settings, implement getDynamicRawDataToIndex
.
For injected settings, implement the queryDynamicRawData methods
.
Index in Car Settings
To index different settings to be included in the search feature, see
Content
providers. The SettingsLib
and android.provider
packages already have defined interfaces and abstract classes to extend for
providing new entries to be indexed. In AOSP settings, implementations of these
classes are used to index results. The primary interface to be fulfilled is
SearchIndexablesProvider
, which is used by
SettingsIntelligence
to index data.
public abstract class SearchIndexablesProvider extends ContentProvider { public abstract Cursor queryXmlResources(String[] projection); public abstract Cursor queryRawData(String[] projection); public abstract Cursor queryNonIndexableKeys(String[] projection); }
In theory, each fragment could be added to the results in
SearchIndexablesProvider
, in which case SettingsIntelligence
would be content. To make the process easy to maintain easily to add new fragments,
use SettingsLib
code generation of SearchIndexableResources
.
Specific to Car Settings, each indexable fragment is annotated with
@SearchIndexable
and then has a static SearchIndexProvider
field that provides the relevant data for that fragment. Fragments with the
annotation but missing a SearchIndexProvider
result in a compilation
error.
interface SearchIndexProvider { List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled); List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled); List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled); List<String> getNonIndexableKeys(Context context); }
All of these fragments are then added to the auto-generated
SearchIndexableResourcesAuto
class, which is a thin wrapper
around the list of SearchIndexProvider
fields for all the fragments.
Support is provided for specifying a specific target for an annotation (such as
Auto, TV, and Wear) however most annotations are left at the default (All
).
In this use case, no specific need exists to specify the Auto target, so it remains
as All
. The base implementation of SearchIndexProvider
is intended to be sufficient for most fragments.
public class CarBaseSearchIndexProvider implements Indexable.SearchIndexProvider { private static final Logger LOG = new Logger(CarBaseSearchIndexProvider.class); private final int mXmlRes; private final String mIntentAction; private final String mIntentClass; public CarBaseSearchIndexProvider(@XmlRes int xmlRes, String intentAction) { mXmlRes = xmlRes; mIntentAction = intentAction; mIntentClass = null; } public CarBaseSearchIndexProvider(@XmlRes int xmlRes, @NonNull Class intentClass) { mXmlRes = xmlRes; mIntentAction = null; mIntentClass = intentClass.getName(); } @Override public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) { SearchIndexableResource sir = new SearchIndexableResource(context); sir.xmlResId = mXmlRes; sir.intentAction = mIntentAction; sir.intentTargetPackage = context.getPackageName(); sir.intentTargetClass = mIntentClass; return Collections.singletonList(sir); } @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { return null; } @Override public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) { return null; } @Override public List<String> getNonIndexableKeys(Context context) { if (!isPageSearchEnabled(context)) { try { return PreferenceXmlParser.extractMetadata(context, mXmlRes, FLAG_NEED_KEY) .stream() .map(bundle -> bundle.getString(METADATA_KEY)) .collect(Collectors.toList()); } catch (IOException | XmlPullParserException e) { LOG.w("Error parsing non-indexable XML - " + mXmlRes); } } return null; } /** * Returns true if the page should be considered in search query. If return false, entire page is suppressed during search query. */ protected boolean isPageSearchEnabled(Context context) { return true; } }
Index a new fragment
With this design, it's relatively easy to add a new SettingsFragment
to be indexed, usually a two-line update providing the XML for the fragment and the
intent to be followed. With WifiSettingsFragment
as an example:
@SearchIndexable public class WifiSettingsFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.wifi_list_fragment, Settings.ACTION_WIFI_SETTINGS); }
The AAOS implementation of the SearchIndexablesProvider
, which
uses SearchIndexableResources
and does the translation from
SearchIndexProviders
into the database schema for
SettingsIntelligence
, but is agnostic to what fragments are
being indexed. SettingsIntelligence
supports any number of
providers, so new providers can be created to support specialized use
cases, allowing each to be specialized and focused on results with similar
structures. The preferences defined in the PreferenceScreen
for the fragment must each have a unique key assigned to them in order to be
indexed. Additionally, the PreferenceScreen
must have a key
assigned to for the screen title to be indexed.
Index examples
In some cases, a fragment may not have a specific intent action associated
with it. In such cases, it's possible to pass in the activity class into the
CarBaseSearchIndexProvider
intent using a component rather than
an action. This still requires the activity to be present in the manifest file
and for it to be exported.
@SearchIndexable public class LanguagesAndInputFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.languages_and_input_fragment, LanguagesAndInputActivity.class); }
In some special cases, some methods of CarBaseSearchIndexProvider
may need to be overridden to get the desired results to be indexed. For example, in
NetworkAndInternetFragment
, preferences related to mobile network were
not to be indexed on devices without a mobile network. In this case, override the
getNonIndexableKeys
method and mark the appropriate keys as
non-indexable when a device doesn't have a mobile network.
@SearchIndexable public class NetworkAndInternetFragment extends SettingsFragment { [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.network_and_internet_fragment, Settings.Panel.ACTION_INTERNET_CONNECTIVITY) { @Override public List<String> getNonIndexableKeys(Context context) { if (!NetworkUtils.hasMobileNetwork( context.getSystemService(ConnectivityManager.class))) { List<String> nonIndexableKeys = new ArrayList<>(); nonIndexableKeys.add(context.getString( R.string.pk_mobile_network_settings_entry)); nonIndexableKeys.add(context.getString( R.string.pk_data_usage_settings_entry)); return nonIndexableKeys; } return null; } }; }
Depending on the needs of the particular fragment, other methods of the
CarBaseSearchIndexProvider
may be overridden to include other
indexable data, such as static and dynamic raw data.
@SearchIndexable public class RawIndexDemoFragment extends SettingsFragment { public static final String KEY_CUSTOM_RESULT = "custom_result_key"; [...] public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new CarBaseSearchIndexProvider(R.xml.raw_index_demo_fragment, RawIndexDemoActivity.class) { @Override public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { List<SearchIndexableRaw> rawData = new ArrayList<>(); SearchIndexableRaw customResult = new SearchIndexableRaw(context); customResult.key = KEY_CUSTOM_RESULT; customResult.title = context.getString(R.string.my_title); customResult.screenTitle = context.getString(R.string.my_screen_title); rawData.add(customResult); return rawData; } @Override public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context, boolean enabled) { List<SearchIndexableRaw> rawData = new ArrayList<>(); SearchIndexableRaw customResult = new SearchIndexableRaw(context); if (hasIndexData()) { customResult.key = KEY_CUSTOM_RESULT; customResult.title = context.getString(R.string.my_title); customResult.screenTitle = context.getString(R.string.my_screen_title); } rawData.add(customResult); return rawData; } }; }
Index injected settings
To inject a setting to be indexed:
- Define a
SearchIndexablesProvider
for your app by extending theandroid.provider.SearchIndexablesProvider
class. - Update the app's
AndroidManifest.xml
with the provider in Step 1. The format is:<provider android:name="PROVIDER_CLASS_NAME" android:authorities="PROVIDER_AUTHORITY" android:multiprocess="false" android:grantUriPermissions="true" android:permission="android.permission.READ_SEARCH_INDEXABLES" android:exported="true"> <intent-filter> <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" /> </intent-filter> </provider>
-
Add indexable data to your provider. The implementation depends on the needs of
the app. Two different data types can be indexed:
SearchIndexableResource
andSearchIndexableRaw
.
SearchIndexablesProvider example
public class SearchDemoProvider extends SearchIndexablesProvider { /** * Key for Auto brightness setting. */ public static final String KEY_AUTO_BRIGHTNESS = "auto_brightness"; /** * Key for my magic preference. */ public static final String KEY_MY_PREFERENCE = "my_preference_key"; /** * Key for my custom search result. */ public static final String KEY_CUSTOM_RESULT = "custom_result_key"; private String mPackageName; @Override public boolean onCreate() { mPackageName = getContext().getPackageName(); return true; } @Override public Cursor queryXmlResources(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS); cursor.addRow(getResourceRow(R.xml.demo_xml)); return cursor; } @Override public Cursor queryRawData(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); Context context = getContext(); Object[] raw = new Object[INDEXABLES_RAW_COLUMNS.length]; raw[COLUMN_INDEX_RAW_TITLE] = context.getString(R.string.my_title); raw[COLUMN_INDEX_RAW_SUMMARY_ON] = context.getString(R.string.my_summary); raw[COLUMN_INDEX_RAW_KEYWORDS] = context.getString(R.string.my_keywords); raw[COLUMN_INDEX_RAW_SCREEN_TITLE] = context.getString(R.string.my_screen_title); raw[COLUMN_INDEX_RAW_KEY] = KEY_CUSTOM_RESULT; raw[COLUMN_INDEX_RAW_INTENT_ACTION] = Intent.ACTION_MAIN; raw[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = mPackageName; raw[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = MyDemoFragment.class.getName(); cursor.addRow(raw); return cursor; } @Override public Cursor queryDynamicRawData(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); DemoObject object = getDynamicIndexData(); Object[] raw = new Object[INDEXABLES_RAW_COLUMNS.length]; raw[COLUMN_INDEX_RAW_KEY] = object.key; raw[COLUMN_INDEX_RAW_TITLE] = object.title; raw[COLUMN_INDEX_RAW_KEYWORDS] = object.keywords; raw[COLUMN_INDEX_RAW_INTENT_ACTION] = object.intentAction; raw[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = object.mPackageName; raw[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = object.className; cursor.addRow(raw); return cursor; } @Override public Cursor queryNonIndexableKeys(String[] projection) { MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS); cursor.addRow(getNonIndexableRow(KEY_AUTO_BRIGHTNESS)); if (!Utils.isMyPreferenceAvailable) { cursor.addRow(getNonIndexableRow(KEY_MY_PREFERENCE)); } return cursor; } private Object[] getResourceRow(int xmlResId) { Object[] row = new Object[INDEXABLES_XML_RES_COLUMNS.length]; row[COLUMN_INDEX_XML_RES_RESID] = xmlResId; row[COLUMN_INDEX_XML_RES_ICON_RESID] = 0; row[COLUMN_INDEX_XML_RES_INTENT_ACTION] = Intent.ACTION_MAIN; row[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = mPackageName; row[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = SearchResult.class.getName(); return row; } private Object[] getNonIndexableRow(String key) { final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length]; ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = key; return ref; } private DemoObject getDynamicIndexData() { if (hasIndexData) { DemoObject object = new DemoObject(); object.key = "demo key"; object.title = "demo title"; object.keywords = "demo, keywords"; object.intentAction = "com.demo.DYNAMIC_INDEX"; object.packageName = "com.demo"; object.className = "DemoClass"; return object; } } }