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);
        }
    });
}