【无标题】
设置添加搜索索引浅析
1.索引插入流程
根据 Settings 首页 search view 的跳转逻辑,可以看到启动的 action 为 com.android.settings.action.SETTINGS_SEARCH,然后简单过一下 SettingsIntelligence apk 是如何插入数据的。
此处启动了 SearchActivity,然后在 onCreate 方法中添加了一个 SearchFragment,主要看 fragment 中的实现逻辑。
@Override
public void onCreate(Bundle savedInstanceState) {
…
// 此处异步更新索引数据
mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */);
…
}
@Override
public void updateIndexAsync(Context context, IndexingCallback callback) {
…
getIndexingManager(context).indexDatabase(callback);
}
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
public class IndexingTask extends AsyncTask<Void, Void, Void> {
…
@Override
protected Void doInBackground(Void… voids) {
performIndexing();
return null;
}
…
}
可以看到,在 DatabaseIndexingManager 中启动了一个异步任务去执行。
public static final String PROVIDER_INTERFACE =
“android.content.action.SEARCH_INDEXABLES_PROVIDER”;
public void performIndexing() {
// 查询实现了 android.content.action.SEARCH_INDEXABLES_PROVIDER 的 provider
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
final List providers =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers);
if (isFullIndex) {
rebuildDatabase();
}
// 从 providers 中解析 indexData
PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
final long updateDatabaseStartTime = System.currentTimeMillis();
// 更新数据库
updateDatabase(indexData, isFullIndex);
IndexDatabaseHelper.setIndexed(mContext, providers);
}
2.provider 数据提取
从检索到的 provider 中获取索引数据流程。
PreIndexData getIndexDataFromProviders(List providers, boolean isFullIndex) {
if (mCollector == null) {
mCollector = new PreIndexDataCollector(mContext);
}
return mCollector.collectIndexableData(providers, isFullIndex);
}
public PreIndexData collectIndexableData(List providers, boolean isFullIndex) {
mIndexData = new PreIndexData();// 初始化 data,data 的数据结构可自行查看
for (final ResolveInfo info : providers) {
if (!isWellKnownProvider(info)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
if (isFullIndex) {
addIndexablesFromRemoteProvider(packageName, authority);// 查询数据,将其添加到 data
}
final long nonIndexableStartTime = System.currentTimeMillis();
addNonIndexablesKeysFromRemoteProvider(packageName, authority);// 添加需要隐藏的索引
...
}
return mIndexData;
}
private void addIndexablesFromRemoteProvider(String packageName, String authority) {
try {
final Context context = mContext.createPackageContext(packageName, 0);
// 从 provider 查询数据添加到 data
final Uri uriForResources = buildUriForXmlResources(authority);
mIndexData.addDataToUpdate(authority, getIndexablesForXmlResourceUri(
context, packageName, uriForResources,
SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
final Uri uriForRawData = buildUriForRawData(authority);
mIndexData.addDataToUpdate(authority, getIndexablesForRawDataUri(
context, packageName, uriForRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
final Uri uriForSiteMap = buildUriForSiteMap(authority);
mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap));
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
}
}
// 构建 URI
private Uri buildUriForXmlResources(String authority) {
return Uri.parse(“content://” + authority + “/” +
SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
}
private Uri buildUriForRawData(String authority) {
return Uri.parse(“content://” + authority + “/” +
SearchIndexablesContract.INDEXABLES_RAW_PATH);
}
private Uri buildUriForNonIndexableKeys(String authority) {
return Uri.parse(“content://” + authority + “/” +
SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
}
以上就是索引数据的插入流程,接下来分析 provider 如何实现。
3.SearchProvider 实现
以设置中的 provider 为例。
public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
…
}
SettingsSearchIndexablesProvider 继承自 frameworks/base/core/java/android/provider/SearchIndexablesProvider.java。
@SystemApi
public abstract class SearchIndexablesProvider extends ContentProvider {
…
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
try {
switch (mMatcher.match(uri)) {
case MATCH_RES_CODE:
return queryXmlResources(null);
case MATCH_RAW_CODE:
return queryRawData(null);
case MATCH_NON_INDEXABLE_KEYS_CODE:
return queryNonIndexableKeys(null);
case MATCH_SITE_MAP_PAIRS_CODE:
return querySiteMapPairs();
case MATCH_SLICE_URI_PAIRS_CODE:
return querySliceUriPairs();
case MATCH_DYNAMIC_RAW_CODE:
return queryDynamicRawData(null);
default:
throw new UnsupportedOperationException("Unknown Uri " + uri);
}
} catch (UnsupportedOperationException e) {
throw e;
} catch (Exception e) {
Log.e(TAG, “Provider querying exception:”, e);
return null;
}
}
/**
* Returns all {@link android.provider.SearchIndexablesContract.XmlResource}.
*
* Those are Xml resource IDs to some {@link android.preference.PreferenceScreen}.
*
* @param projection list of {@link android.provider.SearchIndexablesContract.XmlResource}
* columns to put into the cursor. If {@code null} all supported columns
* should be included.
*/
public abstract Cursor queryXmlResources(String[] projection);
/**
* Returns all {@link android.provider.SearchIndexablesContract.RawData}.
*
* Those are the raw indexable data.
*
* @param projection list of {@link android.provider.SearchIndexablesContract.RawData} columns
* to put into the cursor. If {@code null} all supported columns should be
* included.
*/
public abstract Cursor queryRawData(String[] projection);
/**
* Returns all {@link android.provider.SearchIndexablesContract.NonIndexableKey}.
*
* Those are the non indexable data keys.
*
* @param projection list of {@link android.provider.SearchIndexablesContract.NonIndexableKey}
* columns to put into the cursor. If {@code null} all supported columns
* should be included.
*/
public abstract Cursor queryNonIndexableKeys(String[] projection);
...
}
其中,如上三个抽象方法是必须实现的,以 queryXmlResources() 方法的实现为例,通过 getSearchIndexableResourcesFromProvider 方法获取数据集。
@Override
public Cursor queryXmlResources(String[] projection) {
final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
final List resources =
getSearchIndexableResourcesFromProvider(getContext());
for (SearchIndexableResource val : resources) {
final Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
cursor.addRow(ref);
}
return cursor;
}
private List getSearchIndexableResourcesFromProvider(Context context) {
// 获取到 data 数据集
final Collection bundles = FeatureFactory.getFactory(context)
.getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
List resourceList = new ArrayList<>();
for (SearchIndexableData bundle : bundles) {
Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
final List<SearchIndexableResource> resList =
provider.getXmlResourcesToIndex(context, true);
if (resList == null) {
continue;
}
for (SearchIndexableResource item : resList) {
item.className = TextUtils.isEmpty(item.className)
? bundle.getTargetClass().getName()
: item.className;
}
resourceList.addAll(resList);
}
return resourceList;
}
@Override
public SearchIndexableResources getSearchIndexableResources() {
if (mSearchIndexableResources == null) {
mSearchIndexableResources = new SearchIndexableResourcesMobile();
}
return mSearchIndexableResources;
}
/**
- Stub for Intellij, not compiled! See {@link IndexableProcessor}
*/
public class SearchIndexableResourcesMobile extends SearchIndexableResourcesBase {
}
SearchIndexableResourcesMobile 生成在 IndexableProcessor中,使用注解 com.android.settingslib.search.SearchIndexable,通过 javapoet 库来实例化 SearchIndexableData,getProviderValues 返回带有 SearchIndexable 注解的集合,IndexableProcessor 的实现逻辑可自行查看。
如上,其余两个方法实现大同小异,最终需要返回一个数据集保存到 cursor 中以供查询。
frameworks/base/core/java/android/provider/SearchIndexablesContract.java 类中定义了数据库常用字段
public static final String[] INDEXABLES_XML_RES_COLUMNS = new String[] {
XmlResource.COLUMN_RANK, // 0
XmlResource.COLUMN_XML_RESID, // 1
XmlResource.COLUMN_CLASS_NAME, // 2
XmlResource.COLUMN_ICON_RESID, // 3
XmlResource.COLUMN_INTENT_ACTION, // 4
XmlResource.COLUMN_INTENT_TARGET_PACKAGE, // 5
XmlResource.COLUMN_INTENT_TARGET_CLASS // 6
};
/**
- Constants related to a {@link SearchIndexableResource}.
- This is a description of
*/
public static final class XmlResource extends BaseColumns {
…
}
public static final String[] INDEXABLES_RAW_COLUMNS = new String[] {
RawData.COLUMN_RANK, // 0
RawData.COLUMN_TITLE, // 1
RawData.COLUMN_SUMMARY_ON, // 2
RawData.COLUMN_SUMMARY_OFF, // 3
RawData.COLUMN_ENTRIES, // 4
RawData.COLUMN_KEYWORDS, // 5
RawData.COLUMN_SCREEN_TITLE, // 6
RawData.COLUMN_CLASS_NAME, // 7
RawData.COLUMN_ICON_RESID, // 8
RawData.COLUMN_INTENT_ACTION, // 9
RawData.COLUMN_INTENT_TARGET_PACKAGE, // 10
RawData.COLUMN_INTENT_TARGET_CLASS, // 11
RawData.COLUMN_KEY, // 12
RawData.COLUMN_USER_ID, // 13
RawData.PAYLOAD_TYPE, // 14
RawData.PAYLOAD // 15
};
/**
- Constants related to a {@link SearchIndexableData}.
- This is the raw data that is stored into an Index. This is related to
- {@link android.preference.Preference} and its attributes like
- {@link android.preference.Preference#getTitle()},
- {@link android.preference.Preference#getSummary()}, etc.
*/
public static final class RawData extends BaseColumns {
…
}
4.总结
需要向 SettingsIntelligence 中插入索引数据,需要实现一个 provider 继承 frameworks/base/core/java/android/provider/SearchIndexablesProvider.java,配置如下action
public abstract Cursor queryXmlResources(String[] projection);
public abstract Cursor queryRawData(String[] projection);
public abstract Cursor queryNonIndexableKeys(String[] projection);
用不到的方法直接返回 null 就可以。
设置里面的实现逻辑相对复杂,可以参考如下急救信息的实现
public class EmergencySearchIndexablesProvider extends SearchIndexablesProvider {
private static final String TAG = “EmergencySearchIndexablesProvider”;
private static final int IGNORED_RANK = 2112;
private static final int NO_ICON_ID = 0;
private static SearchIndexableResource[] INDEXABLE_RES = new SearchIndexableResource[] {
new SearchIndexableResource(IGNORED_RANK, R.xml.edit_emergency_info,
EditInfoActivity.class.getName(),
NO_ICON_ID),
new SearchIndexableResource(IGNORED_RANK, R.xml.edit_medical_info,
EditMedicalInfoActivity.class.getName(),
NO_ICON_ID),
};
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryXmlResources(String[] projection) {
if (getContext().getResources().getBoolean(R.bool.config_search_index_disabled)) {
return null;
}
MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
for (int i = 0, length = INDEXABLE_RES.length; i < length; i++) {
cursor.newRow()
.add(XmlResource.COLUMN_RANK, INDEXABLE_RES[i].rank)
.add(XmlResource.COLUMN_XML_RESID, INDEXABLE_RES[i].xmlResId)
.add(XmlResource.COLUMN_CLASS_NAME, null)
.add(XmlResource.COLUMN_ICON_RESID, INDEXABLE_RES[i].iconResId)
.add(XmlResource.COLUMN_INTENT_ACTION, "android.intent.action.MAIN")// 跳转 action
.add(XmlResource.COLUMN_INTENT_TARGET_PACKAGE, "com.android.emergency")// 包名
.add(XmlResource.COLUMN_INTENT_TARGET_CLASS, INDEXABLE_RES[i].className);// 跳转的具体界面
}
return cursor;
}
@Override
public Cursor queryRawData(String[] projection) {
return null;
}
@Override
public Cursor queryNonIndexableKeys(String[] projection) {
return null;
}
}
设置搜索浅析
- 修改记录
版本 修改日期 作者 修改内容
v1.0 2024.1115 杨腾飞 初版 - 代码路径
源码:packages/apps/SettingsIntelligence/
编译及安装:单编模块名SettingsIntelligence,可直接安装编译出来的 apk。
3.启动页
此处由设置代码易知,搜索启动如上 activity。接下来查看该界面的加载流程。
- 数据加载
SearchActivity 在 onCreate 方法中加载了 SearchFragment,接下来查看 fragment 中的逻辑
@Override
public void onCreate(Bundle savedInstanceState) {
…
final LoaderManager loaderManager = getLoaderManager();
// 顾名思义,搜索结果适配器
mSearchAdapter = new SearchResultsAdapter(this /* fragment /);
mSavedQueryController = new SavedQueryController(
getContext(), loaderManager, mSearchAdapter);
mSearchFeatureProvider.initFeedbackButton();
…
// 异步加载索引数据
mSearchFeatureProvider.updateIndexAsync(getContext(), this / indexingCallback */);
}
@Override
public void updateIndexAsync(Context context, IndexingCallback callback) {
…
getIndexingManager(context).indexDatabase(callback);
}
public void indexDatabase(IndexingCallback callback) {
// 启动异步任务加载数据
IndexingTask task = new IndexingTask(callback);
task.execute();
}
public class IndexingTask extends AsyncTask<Void, Void, Void> {
…
@Override
protected Void doInBackground(Void… voids) {
performIndexing();
return null;
}
…
}
public static final String PROVIDER_INTERFACE =
“android.content.action.SEARCH_INDEXABLES_PROVIDER”;
public void performIndexing() {
// 查询实现了 android.content.action.SEARCH_INDEXABLES_PROVIDER 的 provider
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
final List providers =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final String localeStr = Locale.getDefault().toString();
final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers, localeStr);
// 判断是否需要重构数据库
if (isFullIndex) {
rebuildDatabase();
}
// 从 providers 中解析 indexData
PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
// 更新数据库
updateDatabase(indexData, isFullIndex);
IndexDatabaseHelper.setIndexed(mContext, providers, localeStr);
}
4.1 从 provider 解析数据
PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
PreIndexData getIndexDataFromProviders(List providers, boolean isFullIndex) {
if (mCollector == null) {
mCollector = new PreIndexDataCollector(mContext);
}
return mCollector.collectIndexableData(providers, isFullIndex);
}
public PreIndexData collectIndexableData(List providers, boolean isFullIndex) {
mIndexData = new PreIndexData();
for (final ResolveInfo info : providers) {
// 1,provider 需要添加权限 android.permission.READ_SEARCH_INDEXABLES
// 2,实现 provider 的需要是系统应用
if (!isWellKnownProvider(info)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
// 需要重构数据库时重新加载数据
if (isFullIndex) {
addIndexablesFromRemoteProvider(packageName, authority);
}
// 添加不可索引的数据
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
return mIndexData;
}
// 数据加载
private void addIndexablesFromRemoteProvider(String packageName, String authority) {
try {
final Context context = mContext.createPackageContext(packageName, 0);
// 构建 uri
final Uri uriForResources = buildUriForXmlResources(authority);
// 根据提供的 provider 查询数据,添加到 mDataToUpdate
mIndexData.addDataToUpdate(authority, getIndexablesForXmlResourceUri(
context, packageName, uriForResources,
SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
final Uri uriForRawData = buildUriForRawData(authority);
mIndexData.addDataToUpdate(authority, getIndexablesForRawDataUri(
context, packageName, uriForRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
//*/ freeme.caoguofeng, 20230713. Settings-search: add for Injection Index.
final Uri uriForDynamicRawData = buildUriForDynamicRawData(authority);
mIndexData.addDataToUpdate(authority, getIndexablesForRawDataUri(
context, packageName, uriForDynamicRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
//*/
final Uri uriForSiteMap = buildUriForSiteMap(authority);
mIndexData.addSiteMapPairs(getSiteMapFromProvider(context, uriForSiteMap));
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
}
}
4.2 更新 search_index.dp 数据库
void updateDatabase(PreIndexData preIndexData, boolean isFullIndex) {
// 获取不可索引的数据
final Map<String, Set> nonIndexableKeys = preIndexData.getNonIndexableKeys();
…
try {
database.beginTransaction();
// Convert all Pre-index data to Index data and and insert to db.
List<IndexData> indexData = getIndexData(preIndexData);
...
// 数据插入
insertIndexData(database, indexData);
// Only check for non-indexable key updates after initial index.
// Enabled state with non-indexable keys is checked when items are first inserted.
if (!isFullIndex) {
updateDataInDatabase(database, nonIndexableKeys);
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
}
void updateDataInDatabase(SQLiteDatabase database,
Map<String, Set> nonIndexableKeys) {
// 索引是否可用的条件
final String whereEnabled = ENABLED + " = 1";
final String whereDisabled = ENABLED + " = 0";
// 获取之前可用的索引数据
final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
whereEnabled, null, null, null, null);
final ContentValues enabledToDisabledValue = new ContentValues();
enabledToDisabledValue.put(ENABLED, 0);
String authority;
// TODO Refactor: Move these two loops into one method.
while (enabledResults.moveToNext()) {
authority = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
DATA_AUTHORITY));
final String key = enabledResults.getString(enabledResults.getColumnIndexOrThrow(
DATA_KEY_REF));
final Set<String> authorityKeys = nonIndexableKeys.get(authority);
// 更新当前不可用的 key
// The indexed item is set to Enabled but is now non-indexable
if (authorityKeys != null && authorityKeys.contains(key)) {
final String whereClause = getKeyWhereClause(key);
database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
}
}
enabledResults.close();
// 获取之前禁用的数据
final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
whereDisabled, null, null, null, null);
final ContentValues disabledToEnabledValue = new ContentValues();
disabledToEnabledValue.put(ENABLED, 1);
while (disabledResults.moveToNext()) {
authority = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
DATA_AUTHORITY));
final String key = disabledResults.getString(disabledResults.getColumnIndexOrThrow(
DATA_KEY_REF));
final Set<String> authorityKeys = nonIndexableKeys.get(authority);
// The indexed item is set to Disabled but is no longer non-indexable.
// We do not enable keys when authorityKeys is null because it means the keys came
// from an unrecognized authority and therefore should not be surfaced as results.
// 更新为可用
if (authorityKeys != null && !authorityKeys.contains(key)) {
final String whereClause = getKeyWhereClause(key);
database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
}
}
disabledResults.close();
}
5. 搜索流程
过滤 SearchFragment.java 代码,可以看到
// SearchFragment.java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
…
ActionBar actionBar = activity.getActionBar();
actionBar.setDisplayHomeAsUpEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
View actionbarLayout = LayoutInflater.from(getContext()).inflate(
R.layout.freeme_layout_actionar_searchview, null);
// 加载了一个 SearchView
actionBar.setCustomView(actionbarLayout);
mSearchView = actionbarLayout.findViewById(R.id.searchView);
mSearchView.setQuery(mQuery, false /* submitQuery */);
mSearchView.setOnQueryTextListener(this);
mSearchView.requestFocus();
…
return view;
}
// SearchFragment.java
@Override
public boolean onQueryTextChange(String query) {
if (TextUtils.equals(query, mQuery)) {
return true;
}
final boolean isEmptyQuery = TextUtils.isEmpty(query);
mNeverEnteredQuery = false;
mQuery = query;
mSearchAdapter.changQuery(mQuery);
// If indexing is not finished, register the query text, but don't search.
if (!mSearchFeatureProvider.isIndexingComplete(getActivity())) {
return true;
}
if (isEmptyQuery) {
// 更新视图
mSearchAdapter.clearResults();
mSearchAdapter.clearbackground();
final LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT);
mShowingSavedQuery = true;
mSavedQueryController.loadSavedQueries();
mSearchFeatureProvider.hideFeedbackButton(getView());
} else {
mMetricsFeatureProvider.logEvent(SettingsIntelligenceEvent.PERFORM_SEARCH);
// 重新启动加载器
restartLoaders();
}
return true;
}
// SearchFragment.java
private void restartLoaders() {
mShowingSavedQuery = false;
final LoaderManager loaderManager = getLoaderManager();
loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,
null /* args /, this / callback */);
}
// SearchFragment.java 创建 SearchResultLoader
@Override
public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
final Activity activity = getActivity();
switch (id) {
case SearchCommon.SearchLoaderId.SEARCH_RESULT:
return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
default:
return null;
}
}
// SearchFeatureProviderImpl.java
@Override
public SearchResultLoader getSearchResultLoader(Context context, String query) {
return new SearchResultLoader(context, cleanQuery(query));
}
public class SearchResultLoader extends AsyncLoader<List<? extends SearchResult>> {
…
@Override
public List<? extends SearchResult> loadInBackground() {
SearchResultAggregator aggregator = SearchResultAggregator.getInstance();
// 重点看 SearchResultAggregator 的 fetchResults 方法
return aggregator.fetchResults(getContext(), mQuery);
}
…
}
// SearchResultAggregator.java
public class SearchResultAggregator {
…
@NonNull
public synchronized List<? extends SearchResult> fetchResults(Context context, String query) {
final SearchFeatureProvider mFeatureProvider = FeatureFactory.get(context)
.searchFeatureProvider();
final ExecutorService executorService = mFeatureProvider.getExecutorService();
// 获取异步任务并执行,具体 task 可查看 SearchFeatureProviderImpl.java 中的 getSearchQueryTasks 方法
// 每个 task 的查询规则可自行查看
final List<SearchQueryTask> tasks =
mFeatureProvider.getSearchQueryTasks(context, query);
// Start tasks
for (SearchQueryTask task : tasks) {
executorService.execute(task);
}
// Collect results
final Map<Integer, List<? extends SearchResult>> taskResults = new ArrayMap<>();
final long allTasksStart = System.currentTimeMillis();
for (SearchQueryTask task : tasks) {
final int taskId = task.getTaskId();
try {
taskResults.put(taskId,
task.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Could not retrieve result in time: " + taskId, e);
taskResults.put(taskId, Collections.EMPTY_LIST);
}
}
// Merge results
final List<? extends SearchResult> mergedResults = mergeSearchResults(taskResults);
// 返回搜索结果
return mergedResults;
}
// TODO (b/68255021) scale the dynamic search results ranks
private List<? extends SearchResult> mergeSearchResults(
Map<Integer, List<? extends SearchResult>> taskResults) {
final List<SearchResult> searchResults = new ArrayList<>();
// First add db results as a special case
// 添加 DatabaseResultTask 返回的数据
searchResults.addAll(taskResults.remove(DatabaseResultTask.QUERY_WORKER_ID));
// Merge the rest into result list: add everything to heap then pop them out one by one.
final PriorityQueue<SearchResult> heap = new PriorityQueue<>();
for (List<? extends SearchResult> taskResult : taskResults.values()) {
heap.addAll(taskResult);
}
while (!heap.isEmpty()) {
searchResults.add(heap.poll());
}
return searchResults;
}
}
// SearchFragment.java 数据加载完成后,渲染 UI
@Override
public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
List<? extends SearchResult> data) {
…
mSearchAdapter.postSearchResults(data);
}
设置中 SearchProvider 的实现
- 修改记录
版本 修改日期 作者 修改内容
v1.0 2025.03.27 杨腾飞 初版 - 实现的 provider
根据索引添加文档可知,该 provider 需要继承自 frameworks/base/core/java/android/provider/SearchIndexablesProvider.java 类,并且添加权限 android.permission.READ_SEARCH_INDEXABLES。过滤 Settings 代码,可以轻易找到:
3. queryXmlResources 实现
以 queryXmlResources 方法为例,该方法用于查询 xml 文件添加的索引数据。
@Override
public Cursor queryXmlResources(String[] projection) {
// 构建 cursor
final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
final List resources =
getSearchIndexableResourcesFromProvider(getContext());
// 遍历返回的数组,填充 cursor
for (SearchIndexableResource val : resources) {
final Object[] ref = new Object[INDEXABLES_XML_RES_COLUMNS.length];
ref[COLUMN_INDEX_XML_RES_RANK] = val.rank;
ref[COLUMN_INDEX_XML_RES_RESID] = val.xmlResId;
ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = val.className;
ref[COLUMN_INDEX_XML_RES_ICON_RESID] = val.iconResId;
ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = val.intentAction;
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = val.intentTargetPackage;
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
cursor.addRow(ref);
}
return cursor;
}
可以看到,queryXmlResources 方法返回了一个 MatrixCursor 对象,该对象填充了 getSearchIndexableResourcesFromProvider 方法返回的数组。
private List getSearchIndexableResourcesFromProvider(Context context) {
// 根据 SearchFeatureProvider 对象获取 SearchIndexableResources 对象
// 然后拿到一个 Set 集合
final Collection bundles = FeatureFactory.getFeatureFactory()
.getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
List resourceList = new ArrayList<>();
for (SearchIndexableData bundle : bundles) {
// 通过 SearchIndexableData 获取 SearchIndexProvider 对象
Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
final List<SearchIndexableResource> resList =
// getXmlResourcesToIndex 获取到 SearchIndexableResource
provider.getXmlResourcesToIndex(context, true);
if (resList == null) {
continue;
}
for (SearchIndexableResource item : resList) {
item.className = TextUtils.isEmpty(item.className)
? bundle.getTargetClass().getName()
: item.className;
}
resourceList.addAll(resList);
}
return resourceList;
}
其他查询数据的方法可自行查看源码。
- SearchIndexableResources 生成
可查看文件:frameworks/base/packages/SettingsLib/search/processor-src/com/android/settingslib/search/IndexableProcessor.java
/**
-
Annotation processor for {@link SearchIndexable} that generates {@link SearchIndexableResources}
-
subclasses.
*/
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedOptions(IndexableProcessor.PACKAGE_KEY)
// 声明该 processor 支持处理的注解类型
@SupportedAnnotationTypes({“com.android.settingslib.search.SearchIndexable”})
public class IndexableProcessor extends AbstractProcessor {// 定义生成文件的默认包名和类名
private static final String SETTINGSLIB_SEARCH_PACKAGE = “com.android.settingslib.search”;
private static final String CLASS_BASE = “SearchIndexableResourcesBase”;
private static final String CLASS_MOBILE = “SearchIndexableResourcesMobile”;
private static final String CLASS_TV = “SearchIndexableResourcesTv”;
private static final String CLASS_WEAR = “SearchIndexableResourcesWear”;
private static final String CLASS_AUTO = “SearchIndexableResourcesAuto”;
private static final String CLASS_ARC = “SearchIndexableResourcesArc”;static final String PACKAGE_KEY = “com.android.settingslib.search.processor.package”;
private String mPackage;
private Filer mFiler;
private Messager mMessager;
private boolean mRanOnce;@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnvironment) {
if (mRanOnce) {
// Will get called once per round, but we only want to run on the first one.
return true;
}
mRanOnce = true;// 获取 SearchIndexableData 的 全类名 final ClassName searchIndexableData = ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableData"); // 定义字段 mProviders final FieldSpec providers = FieldSpec.builder( ParameterizedTypeName.get( ClassName.get(Set.class), searchIndexableData), "mProviders", Modifier.PRIVATE, Modifier.FINAL) .initializer("new $T()", HashSet.class) .build(); // 定义方法 addIndex,用于向 mProviders 中添加 SearchIndexableData final MethodSpec addIndex = MethodSpec.methodBuilder("addIndex") .addModifiers(Modifier.PUBLIC) .addParameter(searchIndexableData, "indexClass") .addCode("$N.add(indexClass);\n", providers) .build(); // 定义构造函数 final MethodSpec.Builder baseConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); final MethodSpec.Builder mobileConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); final MethodSpec.Builder tvConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); final MethodSpec.Builder wearConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); final MethodSpec.Builder autoConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); final MethodSpec.Builder arcConstructorBuilder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC); // 遍历带 @SearchIndexable 注解的类,提取 for (Element element : roundEnvironment.getElementsAnnotatedWith(SearchIndexable.class)) { if (element.getKind().isClass()) { // 提取类名 Name className = element.accept(new SimpleElementVisitor8<Name, Void>() { @Override public Name visitType(TypeElement typeElement, Void aVoid) { return typeElement.getQualifiedName(); } }, null); if (className != null) { SearchIndexable searchIndexable = element.getAnnotation(SearchIndexable.class); // 提取 forTarget,指定适用于哪些设备 int forTarget = searchIndexable.forTarget(); MethodSpec.Builder builder = baseConstructorBuilder; if (forTarget == SearchIndexable.ALL) { builder = baseConstructorBuilder; } else if ((forTarget & SearchIndexable.MOBILE) != 0) { builder = mobileConstructorBuilder; } else if ((forTarget & SearchIndexable.TV) != 0) { builder = tvConstructorBuilder; } else if ((forTarget & SearchIndexable.WEAR) != 0) { builder = wearConstructorBuilder; } else if ((forTarget & SearchIndexable.AUTO) != 0) { builder = autoConstructorBuilder; } else if ((forTarget & SearchIndexable.ARC) != 0) { builder = arcConstructorBuilder; } builder.addCode( "$N(new com.android.settingslib.search.SearchIndexableData($L.class, $L" + ".SEARCH_INDEX_DATA_PROVIDER));\n", addIndex, className, className); } else { throw new IllegalStateException("Null classname from " + element); } } } // 定义方法 getProviderValues,用于返回 mProviders final MethodSpec getProviderValues = MethodSpec.methodBuilder("getProviderValues") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .returns(ParameterizedTypeName.get( ClassName.get(Collection.class), searchIndexableData)) .addCode("return $N;\n", providers) .build(); // 基类 final TypeSpec baseClass = TypeSpec.classBuilder(CLASS_BASE) .addModifiers(Modifier.PUBLIC) .addSuperinterface( ClassName.get(SETTINGSLIB_SEARCH_PACKAGE, "SearchIndexableResources")) .addField(providers) .addMethod(baseConstructorBuilder.build()) .addMethod(addIndex) .addMethod(getProviderValues) .build(); final JavaFile searchIndexableResourcesBase = JavaFile.builder(mPackage, baseClass).build(); // 子类 final JavaFile searchIndexableResourcesMobile = JavaFile.builder(mPackage, TypeSpec.classBuilder(CLASS_MOBILE) .addModifiers(Modifier.PUBLIC) .superclass(ClassName.get(mPackage, baseClass.name)) .addMethod(mobileConstructorBuilder.build()) .build()) .build(); ... // 写入文件 try { searchIndexableResourcesBase.writeTo(mFiler); searchIndexableResourcesMobile.writeTo(mFiler); searchIndexableResourcesTv.writeTo(mFiler); searchIndexableResourcesWear.writeTo(mFiler); searchIndexableResourcesAuto.writeTo(mFiler); searchIndexableResourcesArc.writeTo(mFiler); } catch (IOException e) { mMessager.printMessage(Kind.ERROR, "Error while writing file: " + e); } return true;}
// 初始化
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mPackage = processingEnvironment.getOptions()
.getOrDefault(PACKAGE_KEY, SETTINGSLIB_SEARCH_PACKAGE);
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
}
}
// 生成的部分文件
// SearchIndexableData
public class SearchIndexableData {
private final Class mTargetClass;
private final Indexable.SearchIndexProvider mSearchIndexProvider;
public SearchIndexableData(Class targetClass, Indexable.SearchIndexProvider provider) {
this.mTargetClass = targetClass;
this.mSearchIndexProvider = provider;
}
public Class getTargetClass() {
return this.mTargetClass;
}
public Indexable.SearchIndexProvider getSearchIndexProvider() {
return this.mSearchIndexProvider;
}
}
// SearchIndexableResourcesMobile
public class SearchIndexableResourcesMobile extends SearchIndexableResourcesBase {
public SearchIndexableResourcesMobile() {
this.addIndex(new SearchIndexableData(DisplaySettings.class, DisplaySettings.SEARCH_INDEX_DATA_PROVIDER));
…
}
}
// SearchIndexableResourcesBase
public class SearchIndexableResourcesBase implements SearchIndexableResources {
private final Set mProviders = new HashSet();
public SearchIndexableResourcesBase() {
this.addIndex(new SearchIndexableData(LegalSettings.class, LegalSettings.SEARCH_INDEX_DATA_PROVIDER));
...
}
public void addIndex(SearchIndexableData indexClass) {
this.mProviders.add(indexClass);
}
public Collection<SearchIndexableData> getProviderValues() {
return this.mProviders;
}
}
5. queryNonIndexableKeys 实现
queryNonIndexableKeys 方法用于查询非索引键。
@Override
public Cursor queryNonIndexableKeys(String[] projection) {
// 构建 MatrixCursor
final MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
// 获取非索引键
final List nonIndexableKeys = getNonIndexableKeysFromProvider(getContext());
for (String nik : nonIndexableKeys) {
final Object[] ref = new Object[NON_INDEXABLES_KEYS_COLUMNS.length];
ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
cursor.addRow(ref);
}
return cursor;
}
getNonIndexableKeysFromProvider 方法接收 Context 对象,并返回一个 List,其中包含所有非索引键。
private List getNonIndexableKeysFromProvider(Context context) {
// 获取 SearchIndexableData 数据集
final Collection bundles = FeatureFactory.getFeatureFactory()
.getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
final List<String> nonIndexableKeys = new ArrayList<>();
for (SearchIndexableData bundle : bundles) {
final long startTime = System.currentTimeMillis();
// 获取 Indexable.SearchIndexProvider 对象
Indexable.SearchIndexProvider provider = bundle.getSearchIndexProvider();
List<String> providerNonIndexableKeys;
try {
// 各个 provider 实现的 getNonIndexableKeys 方法
providerNonIndexableKeys = provider.getNonIndexableKeys(context);
} catch (Exception e) {
// Catch a generic crash. In the absence of the catch, the background thread will
// silently fail anyway, so we aren't losing information by catching the exception.
// We crash when the system property exists so that we can test if crashes need to
// be fixed.
// The gain is that if there is a crash in a specific controller, we don't lose all
// non-indexable keys, but we can still find specific crashes in development.
if (System.getProperty(SYSPROP_CRASH_ON_ERROR) != null) {
throw new RuntimeException(e);
}
Log.e(TAG, "Error trying to get non-indexable keys from: "
+ bundle.getTargetClass().getName(), e);
continue;
}
...
nonIndexableKeys.addAll(providerNonIndexableKeys);
}
return nonIndexableKeys;
}
6. Indexable.SearchIndexProvider 的实现
设置中实现该接口的是 BaseSearchIndexProvider,具体逻辑:
public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider {
public BaseSearchIndexProvider() {
}
public BaseSearchIndexProvider(int xmlRes) {
mXmlRes = xmlRes;
}
// 初始化 SearchIndexableResource 并返回,提供了 xmlResId
@Override
public List<SearchIndexableResource> getXmlResourcesToIndex(Context context, boolean enabled) {
if (mXmlRes != 0) {
final SearchIndexableResource sir = new SearchIndexableResource(context);
sir.xmlResId = mXmlRes;
return Arrays.asList(sir);
}
return null;
}
...
// 获取非索引键
@Override
@CallSuper
public List<String> getNonIndexableKeys(Context context) {
final List<String> nonIndexableKeys = new ArrayList<>();
if (!isPageSearchEnabled(context)) {
// Entire page should be suppressed, mark all keys from this page as non-indexable.
nonIndexableKeys.addAll(
getNonIndexableKeysFromXml(context, true /* suppressAllPage */));
nonIndexableKeys.addAll(
getRawDataToIndex(context, true /* enabled */)
.stream()
.map(data -> data.key)
.collect(Collectors.toList()));
return nonIndexableKeys;
}
nonIndexableKeys.addAll(getNonIndexableKeysFromXml(context, false /* suppressAllPage */));
final List<AbstractPreferenceController> controllers = getPreferenceControllers(context);
if (controllers != null && !controllers.isEmpty()) {
for (AbstractPreferenceController controller : controllers) {
if (controller instanceof PreferenceControllerMixin) {
((PreferenceControllerMixin) controller)
.updateNonIndexableKeys(nonIndexableKeys);
} else if (controller instanceof BasePreferenceController) {
((BasePreferenceController) controller).updateNonIndexableKeys(
nonIndexableKeys);
} else {
Log.e(TAG, controller.getClass().getName()
+ " must implement " + PreferenceControllerMixin.class.getName()
+ " treating the key non-indexable");
nonIndexableKeys.add(controller.getPreferenceKey());
}
}
}
return nonIndexableKeys;
}
// 获取界面控制器
public List<AbstractPreferenceController> getPreferenceControllers(Context context) {
List<AbstractPreferenceController> controllersFromCode = new ArrayList<>();
try {
controllersFromCode = createPreferenceControllers(context);
} catch (Exception e) {
Log.w(TAG, "Error initializing controller in fragment: " + this + ", e: " + e);
}
final List<SearchIndexableResource> res = getXmlResourcesToIndex(context, true);
if (res == null || res.isEmpty()) {
return controllersFromCode;
}
List<BasePreferenceController> controllersFromXml = new ArrayList<>();
for (SearchIndexableResource sir : res) {
controllersFromXml.addAll(PreferenceControllerListHelper
.getPreferenceControllersFromXml(context, sir.xmlResId));
}
controllersFromXml = PreferenceControllerListHelper.filterControllers(controllersFromXml,
controllersFromCode);
final List<AbstractPreferenceController> allControllers = new ArrayList<>();
if (controllersFromCode != null) {
allControllers.addAll(controllersFromCode);
}
allControllers.addAll(controllersFromXml);
return allControllers;
}
...
private List<String> getKeysFromXml(Context context, @XmlRes int xmlResId,
boolean suppressAllPage) {
final List<String> keys = new ArrayList<>();
try {
// 解析 xml 文件
final List<Bundle> metadata = PreferenceXmlParserUtils.extractMetadata(context,
xmlResId, FLAG_NEED_KEY | FLAG_INCLUDE_PREF_SCREEN | FLAG_NEED_SEARCHABLE);
for (Bundle bundle : metadata) {
// 判断 suppressAllPage 或 settings:searchable 配置的属性
if (suppressAllPage || !bundle.getBoolean(METADATA_SEARCHABLE, true)) {
keys.add(bundle.getString(METADATA_KEY));
}
}
} catch (IOException | XmlPullParserException e) {
Log.w(TAG, "Error parsing non-indexable from xml " + xmlResId);
}
return keys;
}
}
如上,可以根据 BaseSearchIndexProvider 的实现,控制某个页面是否索引,或者控制某个页面的某个键是否索引。
每个页面可单独实现对应的方法来控制索引。
更多推荐


所有评论(0)