设置添加搜索索引浅析
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;
}

}

设置搜索浅析

  1. 修改记录
    版本 修改日期 作者 修改内容
    v1.0 2024.1115 杨腾飞 初版
  2. 代码路径
    源码:packages/apps/SettingsIntelligence/
    编译及安装:单编模块名SettingsIntelligence,可直接安装编译出来的 apk。

3.启动页







此处由设置代码易知,搜索启动如上 activity。接下来查看该界面的加载流程。

  1. 数据加载
    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 的实现

  1. 修改记录
    版本 修改日期 作者 修改内容
    v1.0 2025.03.27 杨腾飞 初版
  2. 实现的 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;

}
其他查询数据的方法可自行查看源码。

  1. 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 的实现,控制某个页面是否索引,或者控制某个页面的某个键是否索引。

每个页面可单独实现对应的方法来控制索引。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐