1.preference体系学习总结
类图
第一部分主要学习了preference体系的相关知识(类图中蓝色和绿色部分)
1.1 数据结构描述
1.1.1 preference:
设置的基石,简单来讲可以认为是设置列表中的每一个项目。
其中有个重要的方法getView,这个方法返回的View将会被添加到PreferenceFragment或PreferenceActivity里
作用:提供一个view给即将被展示的activity并且关联一个sharepreferences来保存或取出preference数据,其他常用的preference子类都继承于该类从而进行view样式的改变。
1.1.2(v7包的Preference)
去除了getView方法,增加了继承于RecycleView.ViewHolder的PreferenceViewHolder类
1.1.3 PreferenceGroup:
继承于PreferenceGroup,内部维护了一个元素为Preference的List,和Preference的关系类似于View和ViewGroup一样,采用了组合模式进行组织。
1.1.4(v7包的PreferenceGroup)
与旧实现类似,改动不大
1.1.5 PreferenceScreen:
继承于PreferenceGroup,是一个界面的root节点。当一个PreferenceScreen嵌套在另一个PreferenceScreen内部时会以Dialog的形式开启一个新的界面进行显示。
内部持有一个listView,一个listAdapter(PreferenceGroupAdapter),持有一个layout文件"com.android.internal.R.layout.preference_list_fragment"的id,还有一个Dialog,用于展示嵌套的preferenceScreen
1.1.6 (v7包的PreferenceScreen)
与旧实现对比解耦了关联的View体系的东西,更加简洁。
1.1.7 PreferenceManager :
管理类,用XmlPullParser遍历解析xml文件来创建preference。
1.重要的属性
activity,fragment,sharepreference,preferenceDataStore,preferenceScreen;
关联了一个根布局“PreferenceScreen”,和SharedPreference进行交互。
2.重要的方法
inflateFromResource:通过PreferenceInflater(继承于GenericInflater)递归地扫描xml文件取出所有节点信息来构建出PreferenceScreen对象。
1.1.8 (v14包的PreferenceManager)
增加了一些接口OnDisplayPreferenceDialogListener、OnNavigateToScreenListener
1.1.9 PreferenceFragment:
用于显示preference对象列表
持有一个listView,最终会关联到PreferenceScreen的listView,实现了onPreferenceTreeClick接口,这个接口会在ListView的项被点击时回调。
1.1.10(v14包的PreferenceFragment)
与旧实现相比改动较大。主要是采用了RecyclerView并且实现了一些Dialog的接口。
关联了recycleView和preferenceManager
1.1.11 PreferenceActivity
继承于ListActivity,重要的内部类
Header:
Header包含的属性:
1.title; 2.summary;3.icon;4.fragment;5.intent;6.bundle;
HeaderAdapter
HeaderAdapter包含的属性:1.icon;2.title;3.summary
从这个adapter和layout布局(preference_header_item.xml)可以看出一个header只有这三项。
1.2 PreferenceFragment加载xml源码分析(旧)
重要流程说明:
第8步:PreferenceInflater是一个xml解析器,通过解析xml取出节点,然后反射生成PreferenceScreen对象
1.3 PreferenceFragment点击事件触发流程(旧)
AbsListView里的onKeyUp
AdapterView里的performItemClick
OnItemClickListener的onItemClick
Preference的performClick
PreferenceManager.OnPreferenceTreeClickListener的onPreferenceTreeClick
1.4 PreferenceActivity加载xml源码分析
重要流程说明:
第4步:生成Header的方式也是通过xml解析器解析xml的header节点然后反射生成Header对象。
无论是新的loadHeadersFromResource还是旧的addPreferencesFromResource,底层解析xml都用的XmlPullParser类。
1.5. PreferenceActivity点击事件触发流程
AbsListView里的onKeyUp
AdapterView里的performItemClick
OnItemClickListener的onItemClick
ListActivity里的onListItemClick
PreferenceAcitvity里的onHeaderClick
然后根据fragment是否为空来执行switchToHeader或者startActivity
2.aosp中Setting模块学习总结
类图
主要分析只在系统Settings中使用的部分(除去上面分析过的蓝色和绿色部分)
2.1 Settings主页面的显示分析:
aosp中P版本设置界面如上图所示,除去没显示的,主界面中一共展示了12个列表项,先分析列表项,后面再分析搜索和suggestion项。
主Activity为Settings,继承于SettingsActivity,内部包含一个DashboardSummary的Fragment,从类图上看出DashboardSummary并没有用到preference的那一套东西。布局也相对简单,只有一个FocusRecyclerView,页面的布局由DashboardAdapter负责,DashboardAdapter用到的所有数据由DashboardData进行描述。这里只分析DashboardData中的DashboardCategory,也就是12个列表项所属的DashboardCategory,对于设置主界面来说只有一个DashboardCategory,如下:
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.homepage"/>
从android manifest中也可以看到12个这样的数据,代表主界面的12个列表项。
从类图中可以看出一个列表项由Tile这样一个数据结构进行描述:
public class Tile implements Parcelable {
public CharSequence title;
public CharSequence summary;
public Icon icon;
DashboardCategory表示一个类别,持有一个元素为Tile的List表;
可以发现Dashboard和Preference有很多相似的地方
/**
* Base fragment for dashboard style UI containing a list of static and dynamic setting items.
*/
从注释可以看出DashboardFragment是为了动态加载而设计的,相对于Preference是静态地从xml文件中解析读取的。
CategoryManager主要是对DashboardCategory进行管理,内部会借助于TileUtils来生成DashboardCategory对象,在生成DashboardCategory对象时会借助PackageManager来查询手机中的安装包信息来进行分析,来动态决定是否展示一些项。
DashboardFeatureProviderImpl实现了DashboardFeatureProvider接口,持有CategoryManager对象,通过CategoryManager对象进行Category的获取
SuggestionFeatureProviderImpl实现了SuggestionFeatureProvider接口,应该是用于获取推荐信息,后面再分析。
FeatureFactoryImpl实现了FeatureFactory接口,通过反射生成,用于生成DashboardFeatureProviderImpl和SuggestionFeatureProviderImpl等一些FeatureProvider对象。
整个设置主界面加载流程大致如图所示
图中绿色为主线程,其他线程为后台线程。
对于流程做个简单的分析。
1. SettingsActivity的onCreate流程
在这个过程中会进行FeatureFactory、DashboardFeatureProvider、CategoryManager的初始化,并且如果判断是主界面,则启动DashboardSummary这个Fragment。
-
1.1 DashboardSummary在onAttach中会创建SuggestionFeatureProvider,使之来进行推荐功能的实现。
-
1.2 DashboardSummary在onCreate中会成创建DashboardFeatureProvider,以及新建一个SummaryLoader,用"om.android.settings.category.ia.homepage"来作为CategoryKey,SummaryLoader构造时会启动一个HandlerThread来在后台运行,以便随时接受主线程发过来的消息从而触发SummaryProvider的setListening方法进行Summary的更新,比如更新存储空间或者电量的百分比,或者根据不同的手机特性显示不同的Summary,比如在安全设置Fragment中对于支持指纹的手机显示"屏幕锁定、指纹”,不支持指纹的手机仅显示"屏幕锁定”。
-
1.3 DashboardSummary在onCreateView中会进行view相关的处理,rootview为R.layout.dashboard,这个view中只有一个简单的RecycleView,创建DashboardAdapter,执行rebuildUI方法,该方法启动一个后台线程调用updateCategory来进行Category的更新,更新完毕通过notifyDashboardDataChanged通知主线程进行view的更新,如果需要加载推荐项,推荐项没加载,则向主线程发送一个延迟更新view的消息,通过Handler的postDelayed方法实现。
2. SettingsActivity的onResume流程
-
2.1 这个方法首先会在父类SettingsDrawerActivity的onResume中执行CategoriesUpdateTask,来在后台更新所有的Categories,调用完毕后回主线程调用CategoryListener接口的onCategoriesChanged方法,DashboardFrament和DashboardSummary实现了该接口来进行Category的刷新。
-
2.2 SettingsAcitvity在重写onResume后再次向AsyncTask中post一个doUpdateTilesList的方法,这个方法会排在CategoriesUpdateTask之后执行,也是用来更新Categories的。
-
2.3 在DashboardSummary的onResume中,会调用SummaryLoader的setListening方法,这个方法会向HandlerThread中post消息,分析这个消息类型MSG_GET_CATEGORY_TILES_AND_SET_LISTENING,对应时序图的47步,在后台线程获取到category后,遍历所有的tile,调用makeProviderW方法,因为所有该category下的Fragment都实现了SummaryProvider以及SummaryLoader.SummaryProviderFactory,SummaryProvider实现了SummaryLoader.SummaryProvider接口,因此通过tile找到相关的factory后,再通过反射生成SummaryProvider对象,如时序图的52步。紧接着在setListeningW中遍历调用所有的SummaryProvider的setListening方法,这个方法进一步触发SummaryLoader的setSummary方法来进行各个列表项summary的动态变化,比如电量的更新等。
public void setSummary(SummaryProvider provider, final CharSequence summary) { final ComponentName component = mSummaryProviderMap.get(provider); ThreadUtils.postOnMainThread(() -> { final Tile tile = getTileFromCategory( mDashboardFeatureProvider.getTilesForCategory(mCategoryKey), component); if (tile == null) { if (DEBUG) { Log.d(TAG, "Can't find tile for " + component); } return; } if (DEBUG) { Log.d(TAG, "setSummary " + tile.title + " - " + summary); } updateSummaryIfNeeded(tile, summary); }); } @VisibleForTesting void updateSummaryIfNeeded(Tile tile, CharSequence summary) { if (TextUtils.equals(tile.summary, summary)) { if (DEBUG) { Log.d(TAG, "Summary doesn't change, skipping summary update for " + tile.title); } return; } mSummaryTextMap.put(mDashboardFeatureProvider.getDashboardKeyForTile(tile), summary); tile.summary = summary; if (mSummaryConsumer != null) { mSummaryConsumer.notifySummaryChanged(tile); } else { if (DEBUG) { Log.d(TAG, "SummaryConsumer is null, skipping summary update for " + tile.title); } } }
而在SummaryLoader的setSummary方法中再通过SummaryConsumer接口的notifySummaryChanged方法进行界面的更新,对于主界面来说是DashboardAdapter实现了SummaryConsumer接口,会直接触发notifyItemChanged来进行view的更新(对于其他DashboardFragment的子类来说则是父类DashboardFragment实现了SummaryConsumer接口,会触发Preference的setSummary方法,这个方法会调用notifyChanged方法,进而触发OnPreferenceChangeInternalListener的onPreferenceChange方法,而PreferenceGroupAdapter实现了该接口,因而也会触发notifyItemChanged来进行view的更新)。
2.2 SecuritySettings页面的显示流程分析:
Settings主界面的显示完全是动态的,而除去主界面其他的Fragment的显示则是动态和静态相结合,以要分析的SecuritySettings来说明。SecuritySettings的继承于DashboardFragment,而DashboardFragment主要就是用来加载动态和静态的item。DashboardFragment最终继承于PreferenceFragment,因此可以解析xml中配置的preference来进行显示,而且持有DashboardFeatureProviderImpl对象,因此也可以加载DashboardCategory中的Tile进行显示。
SecuritySettings界面显示是这个样子的:
layout文件R.xml.security_dashboard_settings如下所示:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="security_dashboard_page"
android:title="@string/security_settings_title"
settings:initialExpandedChildrenCount="9">
<!-- security_settings_status.xml -->
<PreferenceCategory
android:order="-10"
android:key="security_status"
android:title="@string/security_status_title" />
<PreferenceCategory
android:order="1"
android:key="dashboard_tile_placeholder" />
<!-- security section -->
<PreferenceCategory
android:order="10"
android:key="security_category"
android:title="@string/lock_settings_title"> <!-- 设备安全性-->
<com.android.settings.widget.GearPreference
android:key="unlock_set_or_change"
android:title="@string/unlock_set_unlock_launch_picker_title"
android:summary="@string/summary_placeholder"
settings:keywords="@string/keywords_lockscreen" /> <!-- 屏幕锁定-->
<Preference
android:key="lockscreen_preferences"
android:title="@string/lockscreen_settings_title" <!-- 锁屏时的偏好设置-->
android:summary="@string/summary_placeholder"
android:fragment="com.android.settings.security.LockscreenDashboardFragment" />
<Preference
android:key="fingerprint_settings"
android:title="@string/security_settings_fingerprint_preference_title" <!-- 指纹-->
android:summary="@string/summary_placeholder"
settings:keywords="@string/keywords_fingerprint_settings"/>
</PreferenceCategory>
....
</PreferenceScreen>
代码里会解析这个xml文件来构造PreferenceScreen对象。
从图里看到并没有显示锁屏时的偏好设置这一个Preference的条目,从源码分析是LockScreenPreferenceController这个类动态的改变了这一个Preference的显示状态。
@Override
public int getAvailabilityStatus() {
if (!mLockPatternUtils.isSecure(MY_USER_ID)) {
return mLockPatternUtils.isLockScreenDisabled(MY_USER_ID)
? DISABLED_FOR_USER : AVAILABLE;
} else {
return mLockPatternUtils.getKeyguardStoredPasswordQuality(MY_USER_ID)
== PASSWORD_QUALITY_UNSPECIFIED
? DISABLED_FOR_USER : AVAILABLE;
}
}
@Override
public void updateState(Preference preference) {
preference.setSummary(
LockScreenNotificationPreferenceController.getSummaryResource(mContext));
}
@Override
public void onResume() {
mPreference.setVisible(isAvailable());
}
而这三个item其实是通过动态从DashboardTiles中取出来的。下面简单分析SecuritySettings的显示过程,主要是DashboardFragment的流程分析。
2.2.1.DashboardFragmenet的onAttach流程
@Override
public void onAttach(Context context) {
super.onAttach(context);
mDashboardFeatureProvider = FeatureFactory.getFactory(context).
getDashboardFeatureProvider(context);
final List<AbstractPreferenceController> controllers = new ArrayList<>();
// Load preference controllers from code
final List<AbstractPreferenceController> controllersFromCode =
createPreferenceControllers(context);
// Load preference controllers from xml definition
final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper
.getPreferenceControllersFromXml(context, getPreferenceScreenResId());
// Filter xml-based controllers in case a similar controller is created from code already.
final List<BasePreferenceController> uniqueControllerFromXml =
PreferenceControllerListHelper.filterControllers(
controllersFromXml, controllersFromCode);
// Add unique controllers to list.
if (controllersFromCode != null) {
controllers.addAll(controllersFromCode);
}
controllers.addAll(uniqueControllerFromXml);
...
mPlaceholderPreferenceController =
new DashboardTilePlaceholderPreferenceController(context);
controllers.add(mPlaceholderPreferenceController);
for (AbstractPreferenceController controller : controllers) {
addPreferenceController(controller);
}
}
方法里主要是一些controllers的创建,这些controller可以从代码创建,也可以从xml解析获取,
在SecuritySettings里通过代码创建了一些controller,如:
private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
Lifecycle lifecycle, SecuritySettings host) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
controllers.add(new LocationPreferenceController(context, lifecycle));
controllers.add(new ManageDeviceAdminPreferenceController(context));
controllers.add(new EnterprisePrivacyPreferenceController(context));
controllers.add(new ManageTrustAgentsPreferenceController(context));
...
return controllers;
}
在xml中是通过声明settings:controller属性来创建的,如
<SwitchPreference
android:key="visiblepattern_profile"
android:summary="@string/summary_placeholder"
android:title="@string/lockpattern_settings_enable_visible_pattern_title_profile"
settings:controller="com.android.settings.security.VisiblePatternProfilePreferenceController"/>
2.2.1.DashboardFragmenet的onCreate流程
DashboardFragmenet最终继承于v14包里的PreferenceFragment的onCreate方法,里面会调用onCreatePreferences方法,DashboardFragmenet重写了onCreatePreferences方法,进而调用refreshAllPreferences方法。
/**
* Refresh all preference items, including both static prefs from xml, and dynamic items from
* DashboardCategory.
*/
private void refreshAllPreferences(final String TAG) {
// First remove old preferences.
if (getPreferenceScreen() != null) {
// Intentionally do not cache PreferenceScreen because it will be recreated later.
getPreferenceScreen().removeAll();
}
// Add resource based tiles.
displayResourceTiles();
refreshDashboardTiles(TAG);
}
从注释可以看出这个方法是这个Fragment显示流程的核心。
/**
* Displays resource based tiles.
*/
private void displayResourceTiles() {
final int resId = getPreferenceScreenResId();
if (resId <= 0) {
return;
}
addPreferencesFromResource(resId);
final PreferenceScreen screen = getPreferenceScreen();
mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach(
controller -> controller.displayPreference(screen));
}
displayResourceTiles这个方法首先获取了PreferenceScreen,然后通过循环调用了每个controller的displayPreference方法
以上面说的LockScreenPreferenceController为例,LockScreenPreferenceController继承于BasePreferenceController,因此会调用如下方法:
/**
* Displays preference in this controller.
*/
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
if (getAvailabilityStatus() == DISABLED_DEPENDENT_SETTING) {
// Disable preference if it depends on another setting.
final Preference preference = screen.findPreference(getPreferenceKey());
if (preference != null) {
preference.setEnabled(false);
}
}
}
从而来决定一个preference是否该显示。
执行完displayResourceTiles后接着会执行refreshDashboardTiles方法,从注释也可以看出这个方法是用来展示动态DashboardCategory的item的
/**
* Refresh preference items backed by DashboardCategory.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void refreshDashboardTiles(final String TAG) {
final PreferenceScreen screen = getPreferenceScreen();
final DashboardCategory category =
mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
...
final List<Tile> tiles = category.getTiles();
...
mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
mSummaryLoader.setSummaryConsumer(this);
...
// Install dashboard tiles.
for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
...
if (mDashboardTilePrefKeys.contains(key)) {
// Have the key already, will rebind.
....
} else {
// Don't have this key, add it.
final Preference pref = new Preference(getPrefContext());
mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), getMetricsCategory(),
pref, tile, key, mPlaceholderPreferenceController.getOrder());
screen.addPreference(pref);
mDashboardTilePrefKeys.add(key);
}
remove.remove(key);
}
...
mSummaryLoader.setListening(true);
}
5-11行取出相关key的tile对象。对于SecuritySettings来说key是"com.android.settings.category.ia.security”,三个tile的title为“Google Play 保护机制、查找我的设备、安全更新”,如前面一节所介绍的,这三项是从gms服务中取出的,并不是Settings app本身内置的,因此可以看作是动态加载。
13行创建了一个SummaryLoader对象,从之前的分析可知,会创建一个HandlerThread置于后台运行。
14行把自己设置为SummaryConsumer接口对象,当SummaryLoader后台更新完,会调用setListeningW,这个方法又会取出所有满足要求的SummaryProvider去执行setListening方法,SummaryProvider又会反过来调用SummaryLoader的setSummary方法,SummaryLoader这时会post一个updateSummaryIfNeeded方法到主线程执行,而这个方法会取出mSummaryConsumer也就是DashboardFragment去执行notifySummaryChanged方法,这个方法会获取tile关联的preference,执行其setSummary方法,这个方法又会调用notifyChanged方法,这个方法调用OnPreferenceChangeInternalListener接口的onPreferenceChange方法,而PreferenceGroupAdapter实现了这个接口,因此最终通过PreferenceGroupAdapter实现了view中Summary的更新。
25-29行创建Preference对象,并且将Preference和Tile进行绑定,然后添加到PreferenceScreen中。
34行调用SummaryLoader的setListening方法,从前面一节分析可知,这个操作会往后台HandlerThread发送一个消息,从而在后台监听Summary是否有更新
private SummaryProvider getSummaryProvider(Tile tile) {
if (!mActivity.getPackageName().equals(tile.intent.getComponent().getPackageName())) {
// Not within Settings, can't load Summary directly.
// TODO: Load summary indirectly.
return null;
}
...
return null;
}
在SummaryLoader的getSummaryProvider方法第二行中当前包名为com.android.settings,而tile的包名为com.google.android.gms,因此返回空,从注释也可以看出,如果当前的tile不在Settings应用中,是不能获取到SummaryProvider的。因此后面的通知Adapter进行Summary的刷新操作也就不会执行了,
3.aosp 8.0 SettingsSearch分析
类图
3.1搜索的数据源
SearchIndexableData:用于搜索的可索引数据
SearchIndexableResource:xml资源
SearchIndexableRaw:原始数据
BaseColumns:基类,rank排名,className类名,iconResId等;
XmlResource:xml的资源id,关联SearchIndexableResource
RawData:原始数据,title标题,summary概要等;关联SearchIndexableData
NonIndexableKey:描述一个不能被索引的数据
SearchIndexablesProvider:用于搜索的可索引provider的基类,用于给搜索提供preference的xml文件数据或者原始数据。
以上的类除了SearchIndexableRaw外其他都位于framework包中;
以下的类或接口位于settings中;
Indexable.SearchIndexProvider:接口,其实现类的实例可以提供可索引的数据
SettingsSearchIndexablesProvider:设置app中的content provider,实现了SearchIndexablesPrevider的相关搜索方法,在phone的app中也有一个类似的实现:PhoneSearchIndexablesProvider,从而可以在设置的搜索中找到Phone中的xml数据进行跳转
BaseSearchIndexProvider:
IndexDatabaseHelper:提供数据库的操作,数据库文件位于data/user_de/0/com.android.settings/databases/search_index.db
3.2数据库构建过程
设置中的界面大部分都是通过xml文件中声明的Preference类的各种子类构建而成,页面打开时通过解析xml文件中的各个节点从而构建成listview中的各个item从而进行显示,以日期和时间页面DateTimeSettings为例:
构成这个界面的文件date_time_prefs.xml如下
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
android:title="@string/date_and_time"
settings:keywords="@string/keywords_date_and_time">
<com.android.settingslib.RestrictedSwitchPreference android:key="auto_time"
android:title="@string/date_time_auto"
android:summaryOn="@string/date_time_auto_summaryOn"
android:summaryOff="@string/date_time_auto_summaryOff"
settings:useAdditionalSummary="true"
settings:restrictedSwitchSummary="@string/enabled_by_admin"
/>
<SwitchPreference android:key="auto_zone"
android:title="@string/zone_auto"
android:summaryOn="@string/zone_auto_summaryOn"
android:summaryOff="@string/zone_auto_summaryOff"
/>
<Preference android:key="date"
android:title="@string/date_time_set_date"
android:summary="03/10/2008"
/>
<Preference android:key="time"
android:title="@string/date_time_set_time"
android:summary="12:00am"
/>
<Preference
android:fragment="com.android.settings.datetime.ZonePicker"
android:key="timezone"
android:title="@string/date_time_set_timezone"
android:summary="GMT-8:00"
/>
<SwitchPreference android:key="24 hour"
android:title="@string/date_time_24hour"
/>
</PreferenceScreen>
上述的xml文件的每一项跟界面展示是一一对应的,实际情况也可能不一样,可以通过配置一些属性或者代码动态增删一些项。
title为显示的标题,summary为摘要,keywords为关键词(不直接显示,用于搜索),留意上面的settings:keywords=”@string/keywords_date_and_time"中keywords_date_and_time的值为
string name="keywords_date_and_time" msgid="758325881602648204">"时钟, 军用"</string>
上面写的是界面跟xml的关系,搜索过程是一个数据库的检索过程,因此搜索需要用到数据库,数据库数据的来源就是上面的xml文件,从模拟器取出search_index.db数据库观察,如下图
可见xml的数据跟数据库中的记录也是一一对应的,因此搜索过程就是数据库的检索过程,输入搜索的字符串最终会转换成SQL数据库查询语句从而返回查询结果。
之所以搜索军用能出现日期和时间的结果,是因为“军用”是keyword的一部分
设置中也能搜索其他app的数据,只要其实现了SearchIndexablesProvider,以phone的app为例,
public class PhoneSearchIndexablesProvider extends SearchIndexablesProvider {
private static final String TAG = "PhoneSearchIndexablesProvider";
private static SearchIndexableResource[] INDEXABLE_RES = new SearchIndexableResource[] {
new SearchIndexableResource(1, R.xml.network_setting_fragment,
MobileNetworkSettings.class.getName(),
R.mipmap.ic_launcher_phone),
};
...
@Override
public Cursor queryXmlResources(String[] projection) {
....
}
...
}
这样实现后(还需要在AndroidManifest里面做些配置),设置app就能跨进程取到network_setting_fragment.xml中的数据并加入search_index.db数据库,如下
下面简单分析下数据库的创建过程:
刚进入设置创建数据库流程如下:
当点击搜索按钮后会启动SearchFragment
@Override
public void onAttach(Context context) {
super.onAttach(context);
mSearchFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
mMetricsFeatureProvider = FeatureFactory.getFactory(context).getMetricsFeatureProvider();
}
首先会创建mSearchFeatureProvider,
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final LoaderManager loaderManager = getLoaderManager();
mSearchAdapter = new SearchResultsAdapter(this);
mSavedQueryController = new SavedQueryController(
getContext(), loaderManager, mSearchAdapter);
mSearchFeatureProvider.initFeedbackButton();
...
// Run the Index update only if we have some space
if (!Utils.isLowStorage(activity)) {
mSearchFeatureProvider.updateIndex(activity, this /* indexingCallback */);
} else {
Log.w(TAG, "Cannot update the Indexer as we are running low on storage space!");
}
}
在onCreate中会执行索引过程,
@Override
public void updateIndex(Context context, IndexingCallback callback) {
long indexStartTime = System.currentTimeMillis();
getIndexingManager(context).indexDatabase(callback);
...
}
接着会执行IndexManager的indexDatabase方法
public void indexDatabase(IndexingCallback callback) {
IndexingTask task = new IndexingTask(callback);
task.execute();
}
开启了一个台任务,IndexingTask是一个AsyncTask,后台执行performIndexing方法
@Override
protected Void doInBackground(Void... voids) {
performIndexing();
return null;
}
/**
* 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.
*/
@VisibleForTesting
void performIndexing() {
final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
// 这里是返回手机中所有声明了action为"android.content.action.SEARCH_INDEXABLES_PROVIDER"的provider的信息,暂时只有三个应用做了这个声明,Settings,Phone和cellbroadcastreceiver,因此Settings的搜索中可以搜索到phone的app中的信息从而进行跳转。
final List<ResolveInfo> list =
mContext.getPackageManager().queryIntentContentProviders(intent, 0);
String localeStr = Locale.getDefault().toString();
String fingerprint = Build.FINGERPRINT;
final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
if (isFullIndex) {
rebuildDatabase();
}
for (final ResolveInfo info : list) {
if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
continue;
}
final String authority = info.providerInfo.authority;
final String packageName = info.providerInfo.packageName;
if (isFullIndex) {
加载外部app的索引
addIndexablesFromRemoteProvider(packageName, authority);
}
addNonIndexablesKeysFromRemoteProvider(packageName, authority);
}
// 更新到数据库
updateDatabase(isFullIndex, localeStr);
...
}
3.3数据搜索以及显示过程
在搜索框输入字符后,会回调到SearchFragment的onQueryTextChange方法
@Override
public boolean onQueryTextChange(String query) {
...
if (isEmptyQuery) {
...
} else {
restartLoaders();
}
...
}
整个搜索过程涉及到了Loader机制,
@Override
public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
final Activity activity = getActivity();
switch (id) {
case LOADER_ID_DATABASE:
return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
case LOADER_ID_INSTALLED_APPS:
return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
default:
return null;
}
}
@Override
public List<? extends SearchResult> loadInBackground() {
...
primaryFirstWordResults = firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]);
primaryMidWordResults = secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]);
secondaryResults = anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]);
tertiaryResults = anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]);
final List<SearchResult> results = new ArrayList<>(
primaryFirstWordResults.size()
+ primaryMidWordResults.size()
+ secondaryResults.size()
+ tertiaryResults.size());
results.addAll(primaryFirstWordResults);
results.addAll(primaryMidWordResults);
results.addAll(secondaryResults);
results.addAll(tertiaryResults);
return removeDuplicates(results);
}
private List<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildSingleWordWhereClause(matchColumns);
final String query = mQueryText + "%";
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank);
}
(data_title like ? OR data_title_normalized like ? ) AND enabled = 1
日期%
% 日期%
@Override
public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
List<? extends SearchResult> data) {
mSearchAdapter.addSearchResults(data, loader.getClass().getName());
if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
return;
}
final int resultCount = mSearchAdapter.displaySearchResults();
if (resultCount == 0) {
mNoResultsView.setVisibility(View.VISIBLE);
} else {
mNoResultsView.setVisibility(View.GONE);
mResultsRecyclerView.scrollToPosition(0);
}
mSearchFeatureProvider.showFeedbackButton(this, getView());
}
/**
* Merge the results from each of the loaders into one list for the adapter.
* Prioritizes results from the local database over installed apps.
*
* @return Number of matched results
*/
public int displaySearchResults() {
final List<? extends SearchResult> databaseResults = mResultsMap
.get(DatabaseResultLoader.class.getName());
final List<? extends SearchResult> installedAppResults = mResultsMap
.get(InstalledAppResultLoader.class.getName());
final int dbSize = (databaseResults != null) ? databaseResults.size() : 0;
final int appSize = (installedAppResults != null) ? installedAppResults.size() : 0;
final List<SearchResult> newResults = new ArrayList<>(dbSize + appSize);
int dbIndex = 0;
int appIndex = 0;
int rank = TOP_RANK;
while (rank <= BOTTOM_RANK) {
while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
newResults.add(databaseResults.get(dbIndex++));
}
while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
newResults.add(installedAppResults.get(appIndex++));
}
rank++;
}
while (dbIndex < dbSize) {
newResults.add(databaseResults.get(dbIndex++));
}
while (appIndex < appSize) {
newResults.add(installedAppResults.get(appIndex++));
}
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new SearchResultDiffCallback(mSearchResults, newResults), false /* detectMoves */);
mSearchResults = newResults;
diffResult.dispatchUpdatesTo(this);
return mSearchResults.size();
}
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(int position, int count) {
adapter.notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
adapter.notifyItemRangeRemoved(position, count);
}
@Override
public void onMoved(int fromPosition, int toPosition) {
adapter.notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
adapter.notifyItemRangeChanged(position, count, payload);
}
});
}