本篇博文基于Android N源代码为参考
从android5.1 开始,google为用户提供了一种很方便的搜索功能,用户可以很方便的在settings中搜索setting里或者系统其他配置了指定继承自SearchIndexablesProvider的应用的设置选项,这样做极大的提高了搜索效率
##SearchIndexablesProvider简介
SearchIndexablesProvider
SearchIndexablesProvider是Android标准API,它提供了三个抽象方法:
Cursor queryXmlResources(String[]) // 返回一个Cursor,这个Cursor中包含了所有可以被索引的XmlResource
Cursor queryRawData(String[])
Cursor queryNonIndexableKeys(String[]) // 返回所有可以不被所有的NonIndexableKey
##search数据库的构建
Setting中关于搜索数据库和表的创建工作都在IndexDatabaseHelper中完成的。
public class IndexDatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "search_index.db";
public interface Tables {
public static final String TABLE_PREFS_INDEX = "prefs_index"; //prefs_index表格中存放的就是搜索的设置选项
public static final String TABLE_META_INDEX = "meta_index";
public static final String TABLE_SAVED_QUERIES = "saved_queries";
}
}
那么我们搜索的这些设置项是从哪里填充到数据库中的,
<font color=’'red>不是开机阶段,而是在每一次打开settings或者当前切换用户(因为系统为每一个用户维护一个单独的search_index.db),或者是当前的语言发生变化,此时会回调
onConfigurationChanged方法,在该方法中会有更新数据库数据的操作。
###数据库查看工具
关于查看数据库的工具,推荐大家一个
ubuntu sqlite3可视化:
sudo apt-get install sqlite3
sqlite3 -version
sudo apt-get install sqlitebrowser
sqlitebrowser
###数据库的初始化
这里说数据库的初始化工作,包含Setting自己和初始化的其他app,也就是拥有继承自SearchIndexablesProvider的应用。
####Index#update
public void update() {
// 查找系统中所有的配置了"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;
// 添加其他APP的设置项
addIndexablesFromRemoteProvider(packageName, authority);
// 添加其他APP中不需要被搜索到的设置项
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
// 上面的addIndexablesFromRemoteProvider会添加设置项到内存中的一个mDataToProcess对象里,updateInternal将该对象更新到数据库中
updateInternal();
}
####构建Uri
private static Uri buildUriForXmlResources(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
}
private static Uri buildUriForRawData(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.INDEXABLES_RAW_PATH);
}
private static Uri buildUriForNonIndexableKeys(String authority) {
return Uri.parse("content://" + authority + "/" +
SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
}
###添加APP中的设置项
####Index#addIndexablesFromRemoteProvider
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
public abstract class SearchIndexablesProvider extends ContentProvider {
....
@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);
}
}
....
}
####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
private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> {
....
@Override
protected Void doInBackground(UpdateData... params) {
final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate;
final List<SearchIndexableData> dataToDelete = params[0].dataToDelete;
final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys;
final boolean forceUpdate = params[0].forceUpdate;
final SQLiteDatabase database = getWritableDatabase();
if (database == null) {
Log.e(LOG_TAG, "Cannot update Index as I cannot get a writable database");
return null;
}
final String localeStr = Locale.getDefault().toString();
try {
database.beginTransaction();
if (dataToDelete.size() > 0) {
processDataToDelete(database, localeStr, dataToDelete);
}
if (dataToUpdate.size() > 0) {
// 插入或者更新当前数据库内容
processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys,
forceUpdate);
}
database.setTransactionSuccessful();
} finally {
database.endTransaction();
}
return null;
}
private boolean processDataToUpdate(SQLiteDatabase database, String localeStr,
List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys,
boolean forceUpdate) {
boolean result = false;
final long current = System.currentTimeMillis();
final int count = dataToUpdate.size();
for (int n = 0; n < count; n++) {
final SearchIndexableData data = dataToUpdate.get(n);
try {
// 继续indexOneSearchIndexableData更新数据库
indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
} catch (Exception e) {
Log.e(LOG_TAG,
"Cannot index: " + data.className + " for locale: " + localeStr, e);
}
}
return result;
}
....
}
####Index#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);
}
}
####Index#indexOneResource
// 对每一条检索数据资源进行数据检索
private void indexOneResource(SQLiteDatabase database, String localeStr,
SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) {
....
// 获取设置不被搜索到的设置项集合
final List<String> nonIndexableKeys = new ArrayList<String>();
List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName);
indexFromResource(sir.context, database, localeStr,
sir.xmlResId, sir.className, sir.iconResId, sir.rank,
sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass,
nonIndexableKeys);
....
}
####Index#indexFromResource
private void indexFromResource(Context context, SQLiteDatabase database, String localeStr,
int xmlResId, String fragmentName, int iconResId, int rank,
String intentAction, String intentTargetPackage, String intentTargetClass,
List<String> nonIndexableKeys) {
....
XmlResourceParser parser = null;
parser = context.getResources().getXml(xmlResId);
// 循环遍历当前布局,添加到啊adb
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries,
fragmentName, screenTitle, iconResId, rank,
keywords, intentAction, intentTargetPackage, intentTargetClass,
true, key, -1 /* default user id */);
}
....
}
####Index#updateOneRowWithFilteredData
private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale,
String title, String summaryOn, String summaryOff, String entries,
String className,
String screenTitle, int iconResId, int rank, String keywords,
String intentAction, String intentTargetPackage, String intentTargetClass,
boolean enabled, String key, int userId) {
final String updatedTitle = normalizeHyphen(title);
final String updatedSummaryOn = normalizeHyphen(summaryOn);
final String updatedSummaryOff = normalizeHyphen(summaryOff);
final String normalizedTitle = normalizeString(updatedTitle);
final String normalizedSummaryOn = normalizeString(updatedSummaryOn);
final String normalizedSummaryOff = normalizeString(updatedSummaryOff);
// 添加或者更新每一行的记录
updateOneRow(database, locale,
updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn,
updatedSummaryOff, normalizedSummaryOff, entries,
className, screenTitle, iconResId,
rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled,
key, userId);
}
private void updateOneRow(SQLiteDatabase database, String locale,
String updatedTitle, String normalizedTitle,
String updatedSummaryOn, String normalizedSummaryOn,
String updatedSummaryOff, String normalizedSummaryOff, String entries,
String className, String screenTitle, int iconResId, int rank, String keywords,
String intentAction, String intentTargetPackage, String intentTargetClass,
boolean enabled, String key, int userId) {
if (TextUtils.isEmpty(updatedTitle)) {
return;
}
// The DocID should contains more than the title string itself (you may have two settings
// with the same title). So we need to use a combination of the title and the screenTitle.
StringBuilder sb = new StringBuilder(updatedTitle);
sb.append(screenTitle);
int docId = sb.toString().hashCode();
ContentValues values = new ContentValues();
values.put(IndexColumns.DOCID, docId);
values.put(IndexColumns.LOCALE, locale);
values.put(IndexColumns.DATA_RANK, rank);
values.put(IndexColumns.DATA_TITLE, updatedTitle);
values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle);
values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn);
values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn);
values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff);
values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff);
values.put(IndexColumns.DATA_ENTRIES, entries);
values.put(IndexColumns.DATA_KEYWORDS, keywords);
values.put(IndexColumns.CLASS_NAME, className);
values.put(IndexColumns.SCREEN_TITLE, screenTitle);
values.put(IndexColumns.INTENT_ACTION, intentAction);
values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage);
values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass);
values.put(IndexColumns.ICON, iconResId);
values.put(IndexColumns.ENABLED, enabled);
values.put(IndexColumns.DATA_KEY_REF, key);
values.put(IndexColumns.USER_ID, userId);
// 更新或者插入当前数据到"prefs_index"表中
database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values);
}
到此为止,设置项的插入操作就完成了
##Setting相关设置项##
在setting中的子页面(包括wifi,sound等)的布局是添加到了一个map集合中,在SearchIndexableResources中检索数据的来源
public final class SearchIndexableResources {
public static int NO_DATA_RES_ID = 0;
private static HashMap<String, SearchIndexableResource> sResMap =
new HashMap<String, SearchIndexableResource>();
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(AdvancedWifiSettings.class.getName(),
new SearchIndexableResource(
Ranking.getRankForClassName(AdvancedWifiSettings.class.getName()),
R.xml.wifi_advanced_settings,
AdvancedWifiSettings.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));
....
}
private SearchIndexableResources() {
}
public static int size() {
return sResMap.size();
}
public static SearchIndexableResource getResourceByName(String className) {
return sResMap.get(className);
}
public static Collection<SearchIndexableResource> values() {
return sResMap.values();
}
}
可以看到在SearchIndexableResources 中维护了一个sResMap,其中添加了所有的SearchIndexableResource,每一个子页面对应一个SearchIndexableResource,并且在SearchIndexableResources 提供了一个values()方法,用来返回当前集合中的所有数据,其实SearchIndexableResources.values()就是在SettingsSearchIndexablesProvider中用到的。
###SettingsSearchIndexablesProvider###
在setting中同样有一个SettingsSearchIndexablesProvider,继承自SearchIndexablesProvider
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()会返回setting中所有的子页面
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;
}
}
SettingsSearchIndexablesProvider重写了queryXmlResources方法,并且通过SearchIndexableResources.values()会返回setting中所有的子页面,最后封装成一个cursor。
##添加不被搜索的设置项##
有时候,在设置中,我们不想让某个xml布局中的某一些设置项被用户搜索到,那么就可以将该设置项对应的key添加到NonIndexableKeys方法的集合中饭,最后返回该集合,这里以NotificationSettings为例,来说明如何添加不被搜索到的设置项。
public class NotificationSettings extends SettingsPreferenceFragment implements Indexable {
....
public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
new BaseSearchIndexProvider() {
// 这里添加当前需要被搜索的布局
public List<SearchIndexableResource> getXmlResourcesToIndex(
Context context, boolean enabled) {
final SearchIndexableResource sir = new SearchIndexableResource(context);
sir.xmlResId = R.xml.notification_settings;
return Arrays.asList(sir);
}
// 从布局中根据preference的key,确定哪个设置项不被搜索到,然后添加到list集合中
public List<String> getNonIndexableKeys(Context context) {
final ArrayList<String> rt = new ArrayList<String>();
if (Utils.isVoiceCapable(context)) {
rt.add(KEY_NOTIFICATION_VOLUME);
} else {
rt.add(KEY_RING_VOLUME);
rt.add(KEY_PHONE_RINGTONE);
rt.add(KEY_WIFI_DISPLAY);
rt.add(KEY_VIBRATE_WHEN_RINGING);
}
return rt;
}
};
}
##为APP添加可搜索的设置项##
同时,也可以为其他APP添加provider,加载指定的设置xml页面,此时在setting中就可以搜索到该页面的设置项,同样的,也可以配置不允许搜索到指定的设置项。
这里以google7.0的packages/services/Telephony/src/com/android/phone/PhoneSearchIndexablesProvider.java作为栗子来看吧:
###创建一个provider继承自SearchIndexablesProvider
public class PhoneSearchIndexablesProvider extends SearchIndexablesProvider {
private static final String TAG = "PhoneSearchIndexablesProvider";
// SearchIndexableResource中存放当前设置项的布局network_setting,以及对应的类名,类名用来点击搜索结果,启动对应的设置界面
private static SearchIndexableResource[] INDEXABLE_RES = new SearchIndexableResource[] {
new SearchIndexableResource(1, R.xml.network_setting,
MobileNetworkSettings.class.getName(),
R.mipmap.ic_launcher_phone),
};
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor queryXmlResources(String[] projection) {
MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
final int count = INDEXABLE_RES.length;
for (int n = 0; n < count; n++) {
Object[] ref = new Object[7];
ref[COLUMN_INDEX_XML_RES_RANK] = INDEXABLE_RES[n].rank;
// 获取R.xml.network_setting的布局id
ref[COLUMN_INDEX_XML_RES_RESID] = INDEXABLE_RES[n].xmlResId;
ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = null;
ref[COLUMN_INDEX_XML_RES_ICON_RESID] = INDEXABLE_RES[n].iconResId;
ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = "android.intent.action.MAIN";
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = "com.android.phone";
ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = INDEXABLE_RES[n].className;
cursor.addRow(ref);
}
return cursor;
}
@Override
public Cursor queryRawData(String[] projection) {
MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
return cursor;
}
// 该方法返回当前布局不想被搜索到的设置项
@Override
public Cursor queryNonIndexableKeys(String[] projection) {
MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
Object[] ref;
ref = new Object[1];
ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] =
"button_roaming_key";
cursor.addRow(ref);
return cursor;
}
}
###添加manifest注册
<provider
android:name="PhoneSearchIndexablesProvider"
android:authorities="com.android.phone"
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>
到现在为止,在setting的搜索框中,就可以搜索到network_setting.xml中指定的设置项了。
##Search总结##
- 创建一个Provider继承自SearchIndexablesProvider
- 添加manifest注册,在AndroidManifest.xml中注册该Provider
- 在自定义的Provider中的queryNonIndexableKeys方法中添加不被搜索的设置项。
欢 迎 关 注 我 的 公 众 号 “编 程 大 全”
专注技术分享,包括Java,python,AI人工智能,Android分享,不定期更新学习视频