Settings 之 SearchIndexablesProvider
首先需要在清单文件中注册action为"android.content.action.SEARCH_INDEXABLES_PROVIDER"的provider,如下:
<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" /> //注册此 action
</intent-filter>
</provider>
搜索数据库路径:/data/user_de/0/com.android.settings/databases/search_index.db
//search_index.db 数据库的prefs_index表格中存放的就是搜索的设置选项
此数据库的初始化不是在开机阶段,而是在每一次打开settings或者当前切换用户(因为系统为每一个用户维护一个单独的search_index.db),或者是当前的语言发生变化会更新数据库.
数据库的初始化:
Index#update
public void update() {
AsyncTask.execute(new Runnable() {
@Override
public void run() {
// 查找系统中所有的配置了"android.content.action.SEARCH_INDEXABLES_PROVIDER"的Provider
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final int size = list.size();
for (int n = 0; n < size; n++) {
final ResolveInfo info = list.get(n);
if (!isWellKnownProvider(info)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
//打印packageName为:
//01-01 17:38:09.257 5207 5511 E qcdds : packageName= com.android.cellbroadcastreceiver
//01-01 17:38:09.678 5207 5511 E qcdds : packageName= com.android.phone
//01-01 17:38:09.777 5207 5511 E qcdds : packageName= com.android.settings
// 添加其他APP的设置项
addIndexablesFromRemoteProvider(packageName, authority); //主要方法
// 添加其他APP中不需要被搜索到的设置项
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
mDataToProcess.fullIndex = true;
// 上面的addIndexablesFromRemoteProvider会添加设置项到内存中的一个mDataToProcess对象里,updateInternal将该对象更新到数据库中
updateInternal();
}
});
}
private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
try {
// rank是按照指定算法计算出的一个值,用来搜索的时候,展示给用户的优先级
final int baseRank = Ranking.getBaseRankForAuthority(authority);
// mBaseAuthority是com.android.settings,authority是其他APP的包名
final Context context = mBaseAuthority.equals(authority) ?
mContext : mContext.createPackageContext(packageName, 0);
// 构建搜索的URI
final Uri uriForResources = buildUriForXmlResources(authority);
// 两种添加到数据库的方式,我们以addIndexablesForXmlResourceUri为例
addIndexablesForXmlResourceUri(context, packageName, uriForResources,
SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank);
final Uri uriForRawData = buildUriForRawData(authority);
addIndexablesForRawDataUri(context, packageName, uriForRawData,
SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank);
return true;
} catch (PackageManager.NameNotFoundException e) {
Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+ Log.getStackTraceString(e));
return false;
}
}
上面代码主要做了下面事情:
根据当前包名创建对应包的context对象。
根据当前包名构建指定URI,例如,settings:content://com.android.settings/settings/indexables_xml_res
然后通过context对象查找对应的Provider的数据
之所以构建出content://com.android.settings/settings/indexables_xml_res 这样的URI是因为所有的需要被搜索到的设置项所在的APP,其Provider都需要继承自SearchIndexablesProvider
//SearchIndexablesProvider 继承 ContentProvider
public abstract class SearchIndexablesProvider extends ContentProvider {
....
//定义了查询路径
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_XML_RES_PATH,
MATCH_RES_CODE);
mMatcher.addURI(mAuthority, SearchIndexablesContract.INDEXABLES_RAW_PATH,
MATCH_RAW_CODE);
mMatcher.addURI(mAuthority, SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH,
MATCH_NON_INDEXABLE_KEYS_CODE);
....
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
switch (mMatcher.match(uri)) {
// 匹配不同的Uri进行查找
case MATCH_RES_CODE:
return queryXmlResources(null);
case MATCH_RAW_CODE:
return queryRawData(null);
case MATCH_NON_INDEXABLE_KEYS_CODE:
return queryNonIndexableKeys(null);
default:
throw new UnsupportedOperationException("Unknown Uri " + uri);
}
}
@Override
public String getType(Uri uri) {
switch (mMatcher.match(uri)) {
case MATCH_RES_CODE:
return SearchIndexablesContract.XmlResource.MIME_TYPE;
case MATCH_RAW_CODE:
return SearchIndexablesContract.RawData.MIME_TYPE;
case MATCH_NON_INDEXABLE_KEYS_CODE:
return SearchIndexablesContract.NonIndexableKey.MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
....
}
//采用 MatrixCursor 构建虚拟的数据表
public class SettingsSearchIndexablesProvider extends SearchIndexablesProvider {
private static final String TAG = "SettingsSearchIndexablesProvider";
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryXmlResources(String[] projection) {
MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
//通过 SearchIndexableResources.values() 获取到所有添加到map集合中的 SearchIndexableResource
/*
SearchIndexableResource的路径为: /frameworks/base/core/java/android/provider/SearchIndexableResource.java
其中定义了 this.rank = rank;
this.xmlResId = xmlResId;
this.className = className;
this.iconResId = iconResId;
等属性
*/
Collection<SearchIndexableResource> values = SearchIndexableResources.values();
for (SearchIndexableResource val : values) {
Object[] ref = new Object[7];
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] = null; // intent action
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = null; // intent target package
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = null; // intent target class
cursor.addRow(ref);
}
return cursor;
}
@Override
public Cursor queryRawData(String[] projection) {
MatrixCursor result = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
return result;
}
// 该方法返回当前布局不想被搜索到的设置项
@Override
public Cursor queryNonIndexableKeys(String[] projection) {
MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
return cursor;
}
}
接着
//Index#addIndexablesForXmlResourceUri
private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
Uri uri, String[] projection, int baseRank) {
// 获取指定包对应的ContentResolver
final ContentResolver resolver = packageContext.getContentResolver();
final Cursor cursor = resolver.query(uri, projection, null, null, null);
if (cursor == null) {
Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
return;
}
try {
final int count = cursor.getCount();
if (count > 0) {
while (cursor.moveToNext()) {
final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
final String targetPackage = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
final String targetClass = cursor.getString(
COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
SearchIndexableResource sir = new SearchIndexableResource(packageContext);
sir.rank = rank;
sir.xmlResId = xmlResId;
sir.className = className;
sir.packageName = packageName;
sir.iconResId = iconResId;
sir.intentAction = action;
sir.intentTargetPackage = targetPackage;
sir.intentTargetClass = targetClass;
// 解析cursor数据,并且添加到内存UpdateData的dataToUpdate属性上, dataToUpdate属性是一个list集合
addIndexableData(sir);
}
}
} finally {
cursor.close();
}
}
public void addIndexableData(SearchIndexableData data) {
synchronized (mDataToProcess) {
mDataToProcess.dataToUpdate.add(data);
}
}
//Index#updateInternal更新到数据库中
private void updateInternal() {
synchronized (mDataToProcess) {
final UpdateIndexTask task = new UpdateIndexTask();
// 拷贝一个mDataToProcess对象的副本,前面将数据添加到mDataToProcess对象中。
UpdateData copy = mDataToProcess.copy();
// 执行UpdateIndexTask,UpdateIndexTask会将copy对象保存到数据库里
task.execute(copy);
mDataToProcess.clear();
}
}
//接下来的调用流程为:
Index$UpdateIndexTask
doInBackground -->
processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, forceUpdate) --> // 插入或者更新当前数据库内容
indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys) --> // 继续indexOneSearchIndexableData更新数据库
private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
//两种方式添加数据库
if (data instanceof SearchIndexableResource) {
indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
} else if (data instanceof SearchIndexableRaw) {
indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
}
}
接下来调用:
indexFromResource() --> //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled) 获取当前布局不想被搜索到的设置项
indexFromProvider() --> //List<SearchIndexableResource> resList = provider.getXmlResourcesToIndex(context, enabled) 需要解析的布局
indexFromResource() --> //使用XmlResourceParser解析xml布局
updateOneRowWithFilteredData() -->
updateOneRow() --> //此方法最终将解析的数据更新至数据库
//在settings中添加搜索项:
在SearchIndexableResources 中维护了一个sResMap,其中添加了所有的SearchIndexableResource,每一个子页面对应一个SearchIndexableResource,并且在SearchIndexableResources 提供了一个values()方
法,用来返回当前集合中的所有数据,其实SearchIndexableResources.values()是在SettingsSearchIndexablesProvider中用到的,SettingsSearchIndexablesProvider重写了queryXmlResources方法,并且通过SearchIndexableResources.values()会返回setting中所有的子页面,最后封装成一个cursor.
在SearchIndexableResources的静态代码块中初始化了:
static {
sResMap.put(WifiSettings.class.getName(),
new SearchIndexableResource(
Ranking.getRankForClassName(WifiSettings.class.getName()),
NO_DATA_RES_ID,
WifiSettings.class.getName(),
R.drawable.ic_settings_wireless));
sResMap.put(SavedAccessPointsWifiSettings.class.getName(),
new SearchIndexableResource(
Ranking.getRankForClassName(SavedAccessPointsWifiSettings.class.getName()),
R.xml.wifi_display_saved_access_points,
SavedAccessPointsWifiSettings.class.getName(),
R.drawable.ic_settings_wireless));
}
两种方式,一种是直接在new SearchIndexableResource() 时传入布局文件,另一种为"NO_DATA_RES_ID"表示此搜索项匹配没有需要解析的xml文件,此xml的解析在Index.java中,
采用第二种时,需要在对应的类中创建一个SEARCH_INDEX_DATA_PROVIDER,类型为SearchIndexProvider,继承BaseSearchIndexProvider并复写其两个方法:
getXmlResourcesToIndex 和 getNonIndexableKeys.
以SecuritySettings为例:
/**
* For Search. Please keep it in sync when updating "createPreferenceHierarchy()"
*/
public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = new SecuritySearchIndexProvider();
private static class SecuritySearchIndexProvider extends BaseSearchIndexProvider {
@Override
public List<SearchIndexableResource> getXmlResourcesToIndex(
Context context, boolean enabled) {
final List<SearchIndexableResource> index = new ArrayList<SearchIndexableResource>();
//返回需要解析的布局
index.add(getSearchResource(context, R.xml.security_settings_misc));
return index;
}
@Override
public List<String> getNonIndexableKeys(Context context) {
// 该方法返回当前布局不想被搜索到的设置项
final List<String> keys = new ArrayList<String>();
LockPatternUtils lockPatternUtils = new LockPatternUtils(context);
// Do not display SIM lock for devices without an Icc card
final UserManager um = UserManager.get(context);
final TelephonyManager tm = TelephonyManager.from(context);
if (!um.isAdminUser() || !tm.hasIccCard()) {
keys.add(KEY_SIM_LOCK);
}
if (um.hasUserRestriction(UserManager.DISALLOW_CONFIG_CREDENTIALS)) {
keys.add(KEY_CREDENTIALS_MANAGER);
}
// TrustAgent settings disappear when the user has no primary security.
if (!lockPatternUtils.isSecure(MY_USER_ID)) {
// keys.add(KEY_TRUST_AGENT); //Move To LockScreen Settings
keys.add(KEY_MANAGE_TRUST_AGENTS);
}
return keys;
}
在搜索过程中,会从数据库中查找匹配,点击筛选结果,根据className启动对应的界面,代码实现在SearchResultsSummary类中:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.search_panel, container, false);
mLayoutSuggestions = (ViewGroup) view.findViewById(R.id.layout_suggestions);
mLayoutResults = (ViewGroup) view.findViewById(R.id.layout_results);
mResultsListView = (ListView) view.findViewById(R.id.list_results);
mResultsListView.setAdapter(mResultsAdapter);
mResultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { // mResultsListView就是查询的结果列表
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// We have a header, so we need to decrement the position by one
position--;
// Some Monkeys could create a case where they were probably clicking on the
// List Header and thus the position passed was "0" and then by decrement was "-1"
if (position < 0) {
return;
}
final Cursor cursor = mResultsAdapter.mCursor;
cursor.moveToPosition(position);
final String className = cursor.getString(Index.COLUMN_INDEX_CLASS_NAME);
final String screenTitle = cursor.getString(Index.COLUMN_INDEX_SCREEN_TITLE);
final String action = cursor.getString(Index.COLUMN_INDEX_INTENT_ACTION);
final String key = cursor.getString(Index.COLUMN_INDEX_KEY);
final SettingsActivity sa = (SettingsActivity) getActivity();
sa.needToRevertToInitialFragment();
if (TextUtils.isEmpty(action)) {
Bundle args = new Bundle();
args.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
Utils.startWithFragment(sa, className, args, null, 0, -1, screenTitle); // 通过className启动Settings中对应的Fragment界面
} else {
final Intent intent = new Intent(action);
final String targetPackage = cursor.getString(
Index.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
final String targetClass = cursor.getString(
Index.COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS);
if (!TextUtils.isEmpty(targetPackage) && !TextUtils.isEmpty(targetClass)) {
final ComponentName component =
new ComponentName(targetPackage, targetClass);
intent.setComponent(component);
}
intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, key);
sa.startActivity(intent);
}
saveQueryToDatabase();
}
});
}