汽车设置搜索索引

通过“设置”搜索,您可以在“汽车设置”应用中快速搜索和更改特定设置,而无需通过浏览应用菜单进行查找。搜索是查找特定设置的最有效方式。默认情况下,搜索仅查找 AOSP 设置。无论是否已注入其他设置,都需要执行进一步更改才能编入索引。

要求

如需使设置可通过“设置”搜索编入索引,数据必须来自:

  • CarSettings 内的 SearchIndexable Fragment。
  • 系统级应用。

定义数据

通用字段:

  • Key。(必需)用于标识结果的便于用户阅读的唯一字符串键。
  • IconResId。(可选)如果应用中的结果旁出现图标,就添加资源 ID,否则就忽略。
  • IntentAction。如果未定义 IntentTargetPackageIntentTargetClass,此字段为必需字段。用于定义搜索结果 intent 将执行的操作。
  • IntentTargetPackage。如果未定义 IntentAction,此字段为必需字段。用于定义搜索结果 intent 将要解析到的软件包。
  • IntentTargetClass。如果未定义 IntentAction,此字段为必需字段。用于定义搜索结果 intent 将要解析到的类 (Activity)。

仅限 SearchIndexableResource

  • XmlResId。(必需)用于定义包含要编入索引的结果的页面的 XML 资源 ID。

仅限 SearchIndexableRaw

  • Title。(必需)搜索结果的标题。
  • SummaryOn。(可选)搜索结果的摘要。
  • Keywords。(可选)与搜索结果关联的字词列表。将查询与您的结果进行匹配。
  • ScreenTitle。(可选)包含搜索结果的页面的标题。

隐藏数据

除非另行标记,否则每个搜索结果都会显示在 Google 搜索中。缓存静态搜索结果后,系统会在每次打开搜索时检索不可编入索引的键的最新列表。隐藏结果的原因可能包括:

  • 重复。例如,同一结果出现在多个页面上。
  • 仅有条件地显示。例如,仅显示使用 SIM 卡时的移动数据设置)。
  • 模板化页面。例如,单个应用的详情页面。
  • 与标题和副标题相比,“设置”需要更多的上下文。例如,“设置”设置仅与屏幕标题相关。

如需隐藏设置,您的提供程序或 SEARCH_INDEX_DATA_PROVIDER 应从 getNonIndexableKeys 返回搜索结果的键。键可以随时返回(重复的模板化页面情况下),也可以有条件地添加(无移动数据情况下)。

静态索引

如果索引数据始终相同,请使用静态索引。例如,XML 数据的标题和摘要或硬编码原始数据。首次启动“设置”搜索时,静态数据只会编入索引一次。

对于设置内的可编入索引的内容,请实现 getXmlResourcesToIndex 和/或 getRawDataToIndex。对于注入设置,请实现 queryXmlResources 和/或 queryRawData 方法。

动态索引

如果可以对可编入索引的数据进行相应更新,请使用动态方法将数据编入索引。启动后,“设置”搜索会更新此动态列表。

对于设置内的可编入索引的内容,请实现 getDynamicRawDataToIndex。对于注入设置,请实现 queryDynamicRawData methods

在汽车设置中编入索引

如需将要包含在搜索功能中的不同设置编入索引,请参阅内容提供程序SettingsLibandroid.provider 软件包已经定义了要扩展的接口和抽象类,用于提供要编入索引的新条目。在 AOSP 设置中,这些类的实现用于将结果编入索引。要实现的主接口是 SearchIndexablesProviderSettingsIntelligence 使用该接口将数据编入索引。

public abstract class SearchIndexablesProvider extends ContentProvider {
    public abstract Cursor queryXmlResources(String[] projection);
    public abstract Cursor queryRawData(String[] projection);
    public abstract Cursor queryNonIndexableKeys(String[] projection);
}

理论上,每个 Fragment 都可以添加到 SearchIndexablesProvider 的结果中,其中,SettingsIntelligence 表示内容。为简化维护流程以轻松添加新的 Fragment,请使用 SearchIndexableResourcesSettingsLib 代码生成。对于“汽车设置”,每个可编入索引的 Fragment 均带有 @SearchIndexable 注解,并且具有一个静态的 SearchIndexProvider 字段,用于提供该 Fragment 的相关数据。带有注解但缺少 SearchIndexProvider 的 fragment 会导致编译错误。

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

然后,所有这些 Fragment 都会添加到自动生成的 SearchIndexableResourcesAuto 类中,该类是所有 Fragment 的 SearchIndexProvider 字段列表的瘦封装容器。支持为注解指定特定目标(如 Auto、TV 和 Wear),但大多数注解保留默认设置 (All)。在此用例中,不存在指定 Auto 目标的特定需求,因此仍保留为 AllSearchIndexProvider 的基本实现旨在满足大多数 Fragment 的需求。

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

将新 fragment 编入索引

通过这种设计,添加要编入索引的新 SettingsFragment 相对容易,通常需要两行更新,用于提供该 fragment 的 XML 和要遵循的 intent。以 WifiSettingsFragment 为例:

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

SearchIndexablesProvider 的 AAOS 实现使用 SearchIndexableResources,并执行从 SearchIndexProvidersSettingsIntelligence 的数据库架构的转换,但与要编入索引的 Fragment 无关。SettingsIntelligence 支持任意数量的提供程序,因此可以创建新的提供程序以支持专用用例,使每个提供程序都能专用于具有类似结构的结果。在 PreferenceScreen 中为该 Fragment 定义的偏好设置必须有一个分配给它们的唯一键,以便编入索引。此外,必须为 PreferenceScreen 分配一个键,以便将屏幕标题编入索引。

索引编制示例

在某些情况下,Fragment 可能没有与之关联的特定 intent 操作。在此类情况下,可以使用组件而不是操作将 activity 类传递到 CarBaseSearchIndexProvider intent 中。不过这仍要求清单文件中存在 Activity 以供导出。

@SearchIndexable
public class LanguagesAndInputFragment extends SettingsFragment {
[...]
    public static final CarBaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
        new CarBaseSearchIndexProvider(R.xml.languages_and_input_fragment,
                LanguagesAndInputActivity.class);
}

在某些特殊情况下,可能需要替换 CarBaseSearchIndexProvider 的某些方法,以便将所需的结果编入索引。例如,在 NetworkAndInternetFragment 中,与移动网络相关的偏好设置不会在没有移动网络的设备上编入索引。在这种情况下,如果设备没有移动网络,请替换 getNonIndexableKeys 方法,并将相应的键标记为不可编入索引。

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

根据特定 Fragment 的需求,可以替换 CarBaseSearchIndexProvider 的其他方法,以包含其他可编入索引的数据,例如静态和动态原始数据。

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

将注入的设置编入索引

如需注入待编入索引的设置,请按以下步骤操作:

  1. 通过扩展 android.provider.SearchIndexablesProvider 类为应用定义 SearchIndexablesProvider
  2. 使用第 1 步中的提供程序更新应用的 AndroidManifest.xml。格式如下:
    <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. 将可编入索引的数据添加到提供程序。具体实现取决于应用的需求。以下两个不同的数据类型可以编入索引:SearchIndexableResourceSearchIndexableRaw

SearchIndexablesProvider 示例

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