Android开发会遇到一些自写界面需要允许被搜索,或者三方应用挂靠在Settings,用户也希望能被搜索。
在知道怎么添加之前,得先了解下整个框架,才能更好地加入我们自己的代码。
这里稍微整理了下整个search database数据如何索引加载流程。
Settings搜索界面是由SearchFragment展现,当用户在Settings主页中点击搜索图标,会启动到SearchActivity。
<activity android:name=".search.SearchActivity"
android:label="@string/search_settings"
android:icon="@drawable/ic_search_24dp"
android:parentActivityName="Settings"
android:theme="@style/Theme.Settings.NoActionBar">
<intent-filter>
<action android:name="com.android.settings.action.SETTINGS_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
首次启动Settings时,并不会主动加载数据库,而是在第一次发生搜索时,异步进行。
//com/android/settings/search/SearchFragment.java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
long startTime = System.currentTimeMillis();
setHasOptionsMenu(true);
Log.d(TAG, "onCreate: ");
......
final Activity activity = getActivity();
// Run the Index update only if we have some space
if (!Utils.isLowStorage(activity)) {
mSearchFeatureProvider.updateIndexAsync(activity, this /* indexingCallback */); // 创建数据库,并建立索引
} else {
Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
}
if (SettingsSearchIndexablesProvider.DEBUG) {
Log.d(TAG, "onCreate spent " + (System.currentTimeMillis() - startTime) + " ms");
}
}
最后走到一个核心类DatabaseIndexingManager,它负责Settings关联所有的索引。
核心方法如下:
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
/**
* Accumulate all data and non-indexable keys from each of the content-providers.
* Only the first indexing for the default language gets static search results - subsequent
* calls will only gather non-indexable keys.
*/
public void performIndexing() {
final long startTime = System.currentTimeMillis();
// 遍历查询设备中所有声明了android.content.action.SEARCH_INDEXABLES_PROVIDER action的ContentProvider。
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
final List<ResolveInfo> providers =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
final String localeStr = Locale.getDefault().toString();
final String fingerprint = Build.FINGERPRINT;
final String providerVersionedNames =
IndexDatabaseHelper.buildProviderVersionedNames(providers);
final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, localeStr,
fingerprint, providerVersionedNames);
if (isFullIndex) {
rebuildDatabase();
}
//遍历所有自定义的自写activity/fragment对应的SearchIndexableProvider提供的可搜索和不可搜索的KEY,并保存至数据结构UpdateData中。
for (final ResolveInfo info : providers) {
if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
Log.d(LOG_TAG, "knealq performIndexing: authority:" + authority + ",packageName:" + packageName + ",isFullIndex:" + isFullIndex + ", resolverInfo:" + info);
if (isFullIndex) {
//查询可搜索的所有Provider( ? extens SearchIndexableProvider)对应所有可搜索KEY,并保存到数据结构:UpdateData.dataToUpdate。
addIndexablesFromRemoteProvider(packageName, authority);
}
final long nonIndexableStartTime = System.currentTimeMillis();
//查询可搜索的所有Provider( ? extens SearchIndexableProvider)对应所有不可搜索(黑名单)KEY,并保存到数据结构:UpdateData.nonIndexableKeys。
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
if (SettingsSearchIndexablesProvider.DEBUG) {
final long nonIndextableTime = System.currentTimeMillis() - nonIndexableStartTime;
Log.d(LOG_TAG, "performIndexing update non-indexable for package " + packageName
+ " took time: " + nonIndextableTime);
}
}
final long updateDatabaseStartTime = System.currentTimeMillis();
// 遍历providers后,将相关索引转化成SearchIndexableData,并保存到database(/data/user_de/0/com.android.settings/databases/search_index.db)中。
updateDatabase(isFullIndex, localeStr);
if (SettingsSearchIndexablesProvider.DEBUG) {
final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime;
Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime);
}
//TODO(63922686): Setting indexed should be a single method, not 3 separate setters.
IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
IndexDatabaseHelper.setProvidersIndexed(mContext, providerVersionedNames);
if (SettingsSearchIndexablesProvider.DEBUG) {
final long indexingTime = System.currentTimeMillis() - startTime;
Log.d(LOG_TAG, "performIndexing took time: " + indexingTime
+ "ms. Full index? " + isFullIndex);
}
}
方法中首先,看到的是通过PackageManager扫描查询注册了指定Action的ContentProvider, 并将其转换放置赋给一个String。
一共搜索到如下app有注册。
我们重点关注com.android.settings,
com.android.cellbroadcastreceiver:29,
com.android.emergency:29,
com.android.settings:29,
com.android.traceur:2,
com.google.android.apps.messaging:54087046,
com.google.android.apps.wellbeing:131132,
com.google.android.gms:200414038,
com.google.android.googlequicksearchbox:301068684,
com.google.android.inputmethod.latin:26881014,
com.google.android.permissioncontroller:291900801,
com.google.android.permissioncontroller:291900801,
再来就是检查是否fullIndex, 是否fullIndex需要满足三个条件,
即当前build fingerprint,locale环境,以及当前扫描到的ContentProvider均未被索引过。
这个在应用内部有个sharedpreference保存相关记录,举个栗子。
AOSP:/data/user_de/0/com.android.settings/shared_prefs # cat index.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="indexed_providers">com.android.cellbroadcastreceiver:29,com.android.emergency:29,com.android.settings:29,com.android.traceur:2,com.google.android.apps.messaging:54087046,com.google.android.apps.wellbeing:131132,com.google.android.gms:200414038,com.google.android.googlequicksearchbox:301068684,com.google.android.inputmethod.latin:26881014,com.google.android.permissioncontroller:291900801,com.google.android.permissioncontroller:291900801,</string>
<boolean name="en_US" value="true" />
<boolean name="Google/XXXXXXXX/AOSP:10/QP1A.190711.020/XXXXX:userdebug/release-keys" value="true" />
</map>
AOSP:/data/user_de/0/com.android.settings/shared_prefs #
说白了就是判断这个index.xml里边的内容是否已经存在,或者是否有差异。
当fullindex为true,需要创建database和表,具体由IndexDatabaseHelper类来完成。
/**
* Reconstruct the database in the following cases:
* - Language has changed
* - Build has changed
*/
private void rebuildDatabase() {
// Drop the database when the locale or build has changed. This eliminates rows which are
// dynamically inserted in the old language, or deprecated settings.
final SQLiteDatabase db = getWritableDatabase();
IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
}
public void reconstruct(SQLiteDatabase db) {
dropTables(db);
bootstrapDB(db);
}
private void bootstrapDB(SQLiteDatabase db) {
db.execSQL(CREATE_INDEX_TABLE);
db.execSQL(CREATE_META_TABLE);
db.execSQL(CREATE_SAVED_QUERIES_TABLE);
db.execSQL(CREATE_SITE_MAP_TABLE);
db.execSQL(INSERT_BUILD_VERSION);
Log.i(TAG, "Bootstrapped database");
}
private void dropTables(SQLiteDatabase db) {
clearCachedIndexed(mContext);
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX);
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX);
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES);
db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SITE_MAP);
}
数据库创建后,首先遍历所有可搜和不可搜的KEY,并保存。
这些数据哪里来,我们暂且称之为“总入口”SearchIndexableResources.sResMap。
Settings对应Provider是SettingsSearchIndexableProvider,其声明了queryXmlResources等接口,而真正访问的数据是来自SearchIndexableResources类里边静态初始化的一个sResMap,里边保存了SearchIndexableData信息。
queryXmlResources直接拿seMap里边的信息即可。
queryNonIndexableKeys则通过sResMap指定的className(前边提到的extends Indexable的fragment、activity),通过DatabaseIndexUtils类找到fragment/Activity内部静态声明定义的BaseSearchIndexProvider。找到后,调用其给出的getNonIndexableKeys(context)方法,它返回的是List<String>。
以FingerprintSettingsFragment为例,它声明了FingerprintSearchIndexProvider, 其继承自BaseSearchIndexProvider,实现了Indexable.SearchIndexProvider接口。并静态实例化了一个被命名为SEARCH_INDEX_DATA_PROVIDER的FingerprintSearchIndexProvider。
注意:
划重点,这里将用法。
【1】
自写fragment、activity中必须按照如下框架写。
// 第一步
public class SearchDefinedExt implements Indexable {// 往sResMap注册的类,可以是Activity、Fragment,还可以是其它类似工具类,不限定。
public static final SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER
= new BaseSearchIndexProvider() { //注意命名,必须是这个, Override几个你需要的方法,DataIndexingUtils工具类就是按照这个名称来找SearchIndexProvider的。
@Override
public List<SearchIndexableRaw> getRawDataToIndex(Context context,
boolean enabled) {
List<SearchIndexableRaw> indexables = new ArrayList<SearchIndexableRaw>();
//省略若干
return indexables;
}
@Override
public List<String> getNonIndexableKeys(Context context) {
List<String> keys = super.getNonIndexableKeys(context);
final ArrayList<String> result = new ArrayList<String>();
return result;
}
};
}
//第2步
//往sResMap总入口添加声明,方便别人能找,可类比于一本书的目录。
public final class SearchIndexableResources {
static {
Log.d("SearchIndexableResources", "static initializer: ");
//添加自己所需要的
addIndex(SearchDefinedExt.class, NO_DATA_RES_ID, R.drawable.ic_settings_wireless);
}
}
这里需要着重说明的是,如果第2步不加,将无法索引到SearchDefinedExt里边定义的SearchIndexProvider。
这个“目录”索引添加很重要。
【2】
三方挂靠Settings的应用怎么办?
Settings并不能直接获取到另外一个app资源xmlResId。
1) 实现自己的ContentProvider,且要符合Search规则,比如注册:android.content.action.SEARCH_INDEXABLES_PROVIDER action。
2) 使用Settings扩展出来的SearchDefinedExt,在getRawDataToIndex()里边添加自己应用的intentAction, intentTargetPackage, intentTargetClass封装成SearchIndexableRaw的形式传递出去。
回归到SearchFragment,我们看到调用updateIndexAsync时有传入this本类(本类实现了IndexingCallback),即当DatabaseIndexingManager索引完成database创建和数据插入(performIndexing)后。
将通过callback回传消息,告诉SearchFragment,然后让其更新UI,Ui通过SearchResultsAdapter负责加载SearchResult。
SearchResultsAdapter onCreateViewHolder时,将根据情况生成IntentSearchViewHolder或者SavedQueryViewHolder。
其中ViewHolder在onBind()时将注册OnClickListener点击事件,一旦点击,跳转到我们索引的界面。