首语
Android Settings中搜索功能帮助我们可以快速访问设置项,进行自定义设置,以得到更佳的使用体验。Android Settings搜索的实现实际不在Settings模块里,而是存在一个单独的模块—SettingsIntelligence,它里面实现了Settings的核心搜索功能,因此,学习SettingsIntelligence搜索实现可以让我们更多了解Settings模块。
搜索实现流程
本文以Android 13 SettingsIntelligence模块源码进行分析。
首先搜索栏的跳转实现在SearchFeatureProvider的initSearchToolbar中,initSearchToolbar在Android Settings解析文章分析过,在SettingsHomepageActivity的initSearchBarView方法中调用。最终跳转到包名为com.android.settings.intelligence,action为android.settings.APP_SEARCH_SETTINGS的页面中。
代码语言:javascript复制public interface SearchFeatureProvider {
default void initSearchToolbar(FragmentActivity activity, Toolbar toolbar, int pageId) {
...
final Intent intent = buildSearchIntent(context, pageId)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
...
toolbar.setOnClickListener(tb -> {
FeatureFactory.getFactory(context).getSlicesFeatureProvider()
.indexSliceDataAsync(context);
FeatureFactory.getFactory(context).getMetricsFeatureProvider()
.logSettingsTileClick(KEY_HOMEPAGE_SEARCH_BAR, pageId);
final Bundle bundle = ActivityOptions.makeSceneTransitionAnimation(activity).toBundle();
activity.startActivity(intent, bundle);
});
}
}
它对应的模块为SettingsIntelligence,模块路径:packages/apps/SettingsIntelligence。从AndroidManifest.xml可以看到,Settings跳转搜索的页面为SearchActivity,SearchActivity添加SearchFragment,在SearchFragment中实现了搜索的核心逻辑。
查看onCreate方法,进行了一些变量的初始化,onCreateView方法中进行view初始化,设置布局为search_panel,我们只需要关注搜索框控件SearchView,设置查询字符串为mQuery,即输入搜索的内容。
设置查询监听,重写onQueryTextSubmit和onQueryTextChange方法。当搜索框文本改变时,通过restartLoaders方法调用LoadManager开启加载数据流程。当Loader创建成功时,回调onCreateLoader方法,调用getSearchResultLoader方法来SearchResultLoader实例。
代码语言:javascript复制public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...
mSearchView = toolbar.findViewById(R.id.search_view);
mSearchView.setQuery(mQuery, false /* submitQuery */);
mSearchView.setOnQueryTextListener(this);
mSearchView.requestFocus();
return view;
}
@Override
public boolean onQueryTextChange(String query) {
...
if (isEmptyQuery) {
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;
}
@Override
public boolean onQueryTextSubmit(String query) {
// Save submitted query.
mSavedQueryController.saveQuery(mQuery);
hideKeyboard();
return true;
}
private void restartLoaders() {
mShowingSavedQuery = false;
final LoaderManager loaderManager = getLoaderManager();
loaderManager.restartLoader(SearchCommon.SearchLoaderId.SEARCH_RESULT,
null /* args */, this /* callback */);
}
@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;
}
}
}
在SearchFeatureProvider实现类SearchFeatureProviderImpl中创建了SearchResultLoader实例,SearchResultLoader在子线程进行数据查找。
代码语言:javascript复制public class SearchResultLoader extends AsyncLoader<List<? extends SearchResult>> {
private final String mQuery;
public SearchResultLoader(Context context, String query) {
super(context);
mQuery = query;
}
@Override
public List<? extends SearchResult> loadInBackground() {
SearchResultAggregator aggregator = SearchResultAggregator.getInstance();
return aggregator.fetchResults(getContext(), mQuery);
}
}
fetchResults方法进行数据查找,并创建了一个tasks集合,然后变量tasks,保存到taskResults中。
代码语言:javascript复制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();
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;
}
}
getSearchQueryTasks中构建了各种类型的task,如DatabaseResultTask/InstalledAppResultTask等等。这些task都继承于SearchQueryTask.QueryWorker。
代码语言:javascript复制public class SearchFeatureProviderImpl implements SearchFeatureProvider {
@Override
public List<SearchQueryTask> getSearchQueryTasks(Context context, String query) {
final List<SearchQueryTask> tasks = new ArrayList<>();
final String cleanQuery = cleanQuery(query);
tasks.add(DatabaseResultTask.newTask(context, getSiteMapManager(), cleanQuery));
tasks.add(InstalledAppResultTask.newTask(context, getSiteMapManager(), cleanQuery));
tasks.add(AccessibilityServiceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
tasks.add(InputDeviceResultTask.newTask(context, getSiteMapManager(), cleanQuery));
return tasks;
}
}
而SearchQueryTask又继承于FutureTask,call方法去处理任务,完成后返回结果。
代码语言:javascript复制public class SearchQueryTask extends FutureTask<List<? extends SearchResult>> {
public static abstract class QueryWorker implements Callable<List<? extends SearchResult>> {
@Override
public List<? extends SearchResult> call() throws Exception {
final long startTime = System.currentTimeMillis();
try {
return query();
} finally {
final long endTime = System.currentTimeMillis();
FeatureFactory.get(mContext).metricsFeatureProvider(mContext)
.logEvent(getQueryWorkerId(), endTime - startTime);
}
}
}
}
我们以DatabaseResultTask为例,查看它实现的query方法。query方法通过一系列的查询方法将数据添加到resultSet中,可以看到query方法中获取SQLite数据库实例,IndexDatabaseHelper中初始化数据库,可以看到数据库名为search_index.db,表名和表字段。最后通过query方法查询数据。
代码语言:javascript复制public class DatabaseResultTask extends SearchQueryTask.QueryWorker {
public static SearchQueryTask newTask(Context context, SiteMapManager siteMapManager,
String query) {
return new SearchQueryTask(new DatabaseResultTask(context, siteMapManager, query));
}
@Override
protected List<? extends SearchResult> query() {
if (mQuery == null || mQuery.isEmpty()) {
return new ArrayList<>();
}
// Start a Future to get search result scores.
FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
mContext, mQuery);
if (rankerTask != null) {
ExecutorService executorService = mFeatureProvider.getExecutorService();
executorService.execute(rankerTask);
}
final Set<SearchResult> resultSet = new HashSet<>();
resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
// Try to retrieve the scores in time. Otherwise use static ranking.
if (rankerTask != null) {
try {
final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
TimeUnit.MILLISECONDS);
return getDynamicRankedResults(resultSet, searchRankScores);
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Error waiting for result scores: " e);
}
}
List<SearchResult> resultList = new ArrayList<>(resultSet);
Collections.sort(resultList);
return resultList;
}
private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildSingleWordWhereClause(matchColumns);
final String query = mQuery "%";
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank);
}
private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
final SQLiteDatabase database =
IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
//查询搜索数据
try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
whereClause,
selection, null, null, null)) {
return mConverter.convertCursor(resultCursor, baseRank, mSiteMapManager);
}
}
}
那么问题来了,Settings搜索数据存储在SQLite数据库中,我们分析了它的查询流程,那么它是如何存储的呢?
其实在SearchFragment的onCreate就有实现,通过updateIndexAsync刷新数据。
代码语言:javascript复制public class SearchFragment extends Fragment implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
...
mSearchFeatureProvider.updateIndexAsync(getContext(), this /* indexingCallback */);
}
通过indexDatabase方法更新数据。
代码语言:javascript复制public class SearchFeatureProviderImpl implements SearchFeatureProvider {
@Override
public void updateIndexAsync(Context context, IndexingCallback callback) {
if (DEBUG) {
Log.d(TAG, "updating index async");
}
getIndexingManager(context).indexDatabase(callback);
}
}
IndexingTask继承于AsyncTask。异步执行performIndexing方法,通过queryIntentContentProviders方法获取ContentProvider,然后根据provider查找数据,更新到数据库中。看下intent指定的action PROVIDER_INTERFACE为"android.content.action.SEARCH_INDEXABLES_PROVIDER",在Settings查找是否有定义此action的ContentProvider。
代码语言:javascript复制public class DatabaseIndexingManager {
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
public class IndexingTask extends AsyncTask<Void, Void, Void> {
@VisibleForTesting
IndexingCallback mCallback;
private long mIndexStartTime;
public IndexingTask(IndexingCallback callback) {
mCallback = callback;
}
@Override
protected void onPreExecute() {
mIndexStartTime = System.currentTimeMillis();
mIsIndexingComplete.set(false);
}
@Override
protected Void doInBackground(Void... voids) {
performIndexing();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
int indexingTime = (int) (System.currentTimeMillis() - mIndexStartTime);
FeatureFactory.get(mContext).metricsFeatureProvider(mContext).logEvent(
SettingsIntelligenceLogProto.SettingsIntelligenceEvent.INDEX_SEARCH,
indexingTime);
mIsIndexingComplete.set(true);
if (mCallback != null) {
mCallback.onIndexingFinished();
}
}
}
public void performIndexing() {
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
final List<ResolveInfo> providers =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, providers);
if (isFullIndex) {
rebuildDatabase();
}
PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex);
final long updateDatabaseStartTime = System.currentTimeMillis();
updateDatabase(indexData, isFullIndex);
IndexDatabaseHelper.setIndexed(mContext, providers);
if (DEBUG) {
final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
Log.d(TAG, "performIndexing updateDatabase took time: " updateDatabaseTime);
}
}
}
可以发现,在Settings的AndroidManifest.xml中指定一个Provider。
代码语言:javascript复制<provider
android:name=".search.SettingsSearchIndexablesProvider"
android:authorities="com.android.settings"
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>
SettingsSearchIndexablesProvider继承于SearchIndexablesProvider,SearchIndexablesProvider继承于ContentProvider, query方法进行了分类查询,插入,删除,更新均不支持,通过final修饰和抛出UnsupportedOperationException屏蔽了。
代码语言:javascript复制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;
}
}
@Override
public final Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("Insert not supported");
}
}
以queryXmlResources为例,通过getSearchIndexableResourcesFromProvider方法获取数据集合,并保存到cursor中。bundles里一个class类型的集合。
代码语言:javascript复制public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
@Override
public Cursor queryXmlResources(String[] projection) {
final MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
final List<SearchIndexableResource> 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<SearchIndexableResource> getSearchIndexableResourcesFromProvider(Context context) {
final Collection<SearchIndexableData> bundles = FeatureFactory.getFactory(context)
.getSearchFeatureProvider().getSearchIndexableResources().getProviderValues();
List<SearchIndexableResource> 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;
}
}
SearchIndexableResourcesMobile继承于SearchIndexableResourcesBase,
代码语言:javascript复制public class SearchFeatureProviderImpl implements SearchFeatureProvider {
@Override
public SearchIndexableResources getSearchIndexableResources() {
if (mSearchIndexableResources == null) {
mSearchIndexableResources = new SearchIndexableResourcesMobile();
}
return mSearchIndexableResources;
}
}
SearchIndexableResourcesMobile类生成在IndexableProcessor中,IndexableProcessor设置的注解为SearchIndexable,SearchIndexable注解可以指定target(ALL/MOBILE/TV/WEAR/AUTO/ARC)对应不同平台。通过JavaPoet库来addCode实例化SearchIndexableData,getProviderValues方法返回的是带有SearchIndexable注解的所有类集合。
代码语言:javascript复制@SupportedSourceVersion(SourceVersion.RELEASE_9)
@SupportedAnnotationTypes({"com.android.settingslib.search.SearchIndexable"})
public class IndexableProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
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);
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;
}
//实例化SearchIndexableData
builder.addCode(
"$N(new SearchIndexableData($L.class, $L"
".SEARCH_INDEX_DATA_PROVIDER));n",
addIndex, className, className);
...
}
}
inal 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(PACKAGE, "SearchIndexableResources"))
.addField(providers)
.addMethod(baseConstructorBuilder.build())
.addMethod(addIndex)
.addMethod(getProviderValues)
.build();
final JavaFile searchIndexableResourcesBase = JavaFile.builder(PACKAGE, baseClass).build();
final JavaFile searchIndexableResourcesMobile = JavaFile.builder(PACKAGE,
TypeSpec.classBuilder(CLASS_MOBILE)
.addModifiers(Modifier.PUBLIC)
.superclass(ClassName.get(PACKAGE, baseClass.name))
.addMethod(mobileConstructorBuilder.build())
.build())
.build();
}
实例化SearchIndexableData,mTargetClass为className.class,mSearchIndexProvider为className.SEARCH_INDEX_DATA_PROVIDER,其中的className就是对应添加SearchIndexable注解的类名
代码语言:javascript复制public class SearchIndexableData {
public SearchIndexableData(Class targetClass, Indexable.SearchIndexProvider provider) {
mTargetClass = targetClass;
mSearchIndexProvider = provider;
}
}
总结一下,Settings搜索功能就是在需要被提供的页面添加@SearchIndexable注解,在这页面创建一个常量SEARCH_INDEX_DATA_PROVIDER,这个常量类型必须为Indexable.SearchIndexProvider。以TopLevelSettings为例。添加了@SearchIndexable注解,指定Target为MOBILE,也创建了SEARCH_INDEX_DATA_PROVIDER,Settings封装了一个基础的SearchIndexProvider,不返回任何要索引的数据,类名为BaseSearchIndexProvider。
代码语言:javascript复制@SearchIndexable(forTarget = MOBILE)
public class TopLevelSettings extends DashboardFragment implements SplitLayoutListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider(R.xml.top_level_settings) {
@Override
protected boolean isPageSearchEnabled(Context context) {
// Never searchable, all entries in this page are already indexed elsewhere.
return false;
}
};
}
SearchIndexProvider和BaseSearchIndexProvider扩展的方法可以让我们准确处理菜单搜索需求。
代码语言:javascript复制public class BaseSearchIndexProvider implements Indexable.SearchIndexProvider {
public BaseSearchIndexProvider() {
}
public BaseSearchIndexProvider(int xmlRes) {
mXmlRes = xmlRes;
}
//返回SearchIndexableResource
@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;
}
//返回SearchIndexableRaw集合
@Override
public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
return null;
}
//返回动态数据集合
@Override
public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) {
return null;
}
//无法搜索的集合
@Override
@CallSuper
public List<String> getNonIndexableKeys(Context context) {
...
}
//页面是否启用搜索
protected boolean isPageSearchEnabled(Context context) {
return true;
}
//获取xml设置禁用搜索的集合
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public List<String> getNonIndexableKeysFromXml(Context context, @XmlRes int xmlResId,
boolean suppressAllPage) {
return getKeysFromXml(context, xmlResId, suppressAllPage);
}
}
以上就是Settings的搜索逻辑。要测试新菜单的可搜索性,需要先清除Settings数据,让数据库重新添加数据。
总结
Settings菜单如果想要支持搜索,首先对应页面需要添加@SearchIndexable注解,其次在本页面创建一个常量SEARCH_INDEX_DATA_PROVIDER,然后根据需要重写需要的实现方法。这样这个菜单就支持搜索了。
SettingsIntelligence会扫描这些添加@SearchIndexable注解的页面,将这些页面的菜单添加到数据库中,查询时根据关键词进行匹配查询。