通过设置搜索,您可以快速轻松地搜索和更改汽车设置应用中的特定设置,而无需浏览应用菜单来查找。搜索是查找特定设置的最有效方法。默认情况下,搜索仅查找 AOSP 设置。其他设置(无论是否注入)都需要进行其他更改才能编入索引。
要求
要使设置可由“设置”搜索编入索引,数据必须来自
CarSettings
中的SearchIndexable
片段。- 系统级应用。
定义数据
通用字段
Key
。(必需)用于标识结果的唯一人类可读字符串键。IconResId
。可选。如果图标显示在您的应用中结果旁边,则添加资源 ID,否则忽略。IntentAction
。如果未定义IntentTargetPackage
或IntentTargetClass
,则为必需。定义搜索结果 intent 要执行的操作。IntentTargetPackage
。如果未定义IntentAction
,则为必需。定义搜索结果 intent 要解析到的软件包。IntentTargetClass
。如果未定义IntentAction
,则为必需。定义搜索结果 intent 要解析到的类(Activity)。
SearchIndexableResource
仅限
XmlResId
。(必需)定义包含要编入索引的结果的页面的 XML 资源 ID。
SearchIndexableRaw
仅限
Title
。(必需)搜索结果的标题。SummaryOn
。(可选)搜索结果的摘要。Keywords
。(可选)与搜索结果关联的字词列表。将查询与您的结果匹配。ScreenTitle
。(可选)包含您的搜索结果的页面的标题。
隐藏数据
除非另有标记,否则每个搜索结果都会显示在“搜索”中。虽然静态搜索结果会被缓存,但每次打开搜索时都会检索新的不可编入索引的键列表。隐藏结果的原因可能包括:
- 重复。例如,出现在多个页面上。
- 仅在有条件时显示。例如,仅在存在 SIM 卡时显示移动数据设置)。
- 模板化页面。例如,单个应用的详情页面。
- 设置需要的上下文超出标题和副标题。例如,“设置”设置,仅与屏幕标题相关。
要隐藏设置,您的提供程序或 SEARCH_INDEX_DATA_PROVIDER
应从 getNonIndexableKeys
返回搜索结果的键。该键始终可以返回(重复、模板化页面情况),或者有条件地添加(无移动数据情况)。
静态索引
如果您的索引数据始终相同,请使用静态索引。例如,XML 数据的标题和摘要或硬编码的原始数据。静态数据仅在首次启动“设置”搜索时索引一次。
对于设置中的可索引项,请实现 getXmlResourcesToIndex
和/或 getRawDataToIndex
。对于注入的设置,请实现 queryXmlResources
和/或 queryRawData
方法。
动态索引
如果可索引数据可以相应地更新,请使用动态方法来索引您的数据。“设置”搜索会在启动时更新此动态列表。
对于设置中的可索引项,请实现 getDynamicRawDataToIndex
。对于注入的设置,请实现 queryDynamicRawData methods
。
在汽车设置中索引
要索引要包含在搜索功能中的不同设置,请参阅内容提供程序。SettingsLib
和 android.provider
软件包已定义用于扩展的接口和抽象类,以便提供要索引的新条目。在 AOSP 设置中,这些类的实现用于索引结果。要满足的主要接口是 SearchIndexablesProvider
,SettingsIntelligence
使用它来索引数据。
public abstract class SearchIndexablesProvider extends ContentProvider { public abstract Cursor queryXmlResources(String[] projection); public abstract Cursor queryRawData(String[] projection); public abstract Cursor queryNonIndexableKeys(String[] projection); }
理论上,每个片段都可以添加到 SearchIndexablesProvider
中的结果中,在这种情况下,SettingsIntelligence
将成为内容。为了使该过程易于维护并轻松添加新片段,请使用 SettingsLib
代码生成 SearchIndexableResources
。特定于汽车设置,每个可索引片段都使用 @SearchIndexable
进行注释,然后具有一个静态 SearchIndexProvider
字段,该字段为该片段提供相关数据。带有注释但缺少 SearchIndexProvider
的片段会导致编译错误。
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); }
所有这些片段随后都会添加到自动生成的 SearchIndexableResourcesAuto
类中,该类是所有片段的 SearchIndexProvider
字段列表周围的瘦包装器。支持为注释指定特定目标(例如 Auto、TV 和 Wear),但大多数注释都保留为默认值 (All
)。在这种用例中,没有指定 Auto 目标的特定需求,因此它仍然保留为 All
。SearchIndexProvider
的基本实现旨在足以满足大多数片段的需求。
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; } }
索引新片段
通过此设计,可以相对轻松地添加要索引的新 SettingsFragment
,通常是一个两行更新,提供片段的 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
并执行从 SearchIndexProviders
到 SettingsIntelligence
的数据库架构的转换,但与正在索引的片段无关。SettingsIntelligence
支持任意数量的提供程序,因此可以创建新的提供程序来支持专门的用例,从而使每个提供程序都可以专门化并专注于具有相似结构的结果。为片段的 PreferenceScreen
中定义的偏好设置都必须分配一个唯一的键才能编入索引。此外,PreferenceScreen
必须分配一个键才能索引屏幕标题。
索引示例
在某些情况下,片段可能没有与之关联的特定 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; } }; }
根据特定片段的需求,可以覆盖 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; } }; }
索引注入的设置
要注入要索引的设置:
- 通过扩展
android.provider.SearchIndexablesProvider
类,为您的应用定义SearchIndexablesProvider
。 - 使用步骤 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>
- 将可索引数据添加到您的提供程序。实现取决于应用的需求。可以索引两种不同的数据类型:
SearchIndexableResource
和SearchIndexableRaw
。
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; } } }