Car Settings search indexing

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 inside CarSettings.
  • 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 if IntentTargetPackage or IntentTargetClass is not defined. Defines the action that the search result intent is to take.
  • IntentTargetPackage. Required if IntentAction is not defined. Defines the package that the search result intent is to resolve to.
  • IntentTargetClass. Required if IntentAction 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:

  1. Define a SearchIndexablesProvider for your app by extending the android.provider.SearchIndexablesProvider class.
  2. 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>
    
  3. Add indexable data to your provider. The implementation depends on the needs of the app. Two different data types can be indexed: SearchIndexableResource and SearchIndexableRaw.

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;
        }
    }
}