在上一篇 Android S版本MtkSettings的加载流程(一) 我们介绍了MtkSettings从启动到加载的一个流程。这一篇主要记录设置项prefrence的两种加载方式:静态加载和动态加载。
两种加载方式
主页TopLevelSettings.java和二级菜单页都继承于DashboardFragment.java,设置项加载方式的入口都是从DashboardFragment.java开始的。
网络和互联网:NetworkDashboardFragment.java实现了父类DashboardFragment.java的getPreferenceScreenResId()方法获取xml布局。
它是一个抽象方法,需要每个子类Fragment设置自己的布局文件。
getPreferenceScreenResId()方法的调用链关系如下:
重点来了,DashboardFragment类通过调用displayResourceTiles()方法从xml资源文件中静态加载显示preference,从refreshDashboardTiles()方法中动态加载显示preference。
/**
* Refresh all preference items, including both static prefs from xml, and dynamic items from
* DashboardCategory.
*/
private void refreshAllPreferences(final String tag) {
final PreferenceScreen screen = getPreferenceScreen();
// First remove old preferences.
if (screen != null) {
// Intentionally do not cache PreferenceScreen because it will be recreated later.
screen.removeAll();
}
// Add resource based tiles.
displayResourceTiles();
refreshDashboardTiles(tag);
final Activity activity = getActivity();
if (activity != null) {
Log.d(tag, "All preferences added, reporting fully drawn");
activity.reportFullyDrawn();
}
updatePreferenceVisibility(mPreferenceControllers);
}
1. 静态方式加载preference
/**
* Displays resource based tiles.
*/
private void displayResourceTiles() {
final int resId = getPreferenceScreenResId();
if (resId <= 0) {
return;
}
addPreferencesFromResource(resId);
final PreferenceScreen screen = getPreferenceScreen();
screen.setOnExpandButtonClickListener(this);
displayResourceTilesToScreen(screen);
}
2. 动态方式加载preference
/**
* Refresh preference items backed by DashboardCategory.
*/
private void refreshDashboardTiles(final String tag) {
final PreferenceScreen screen = getPreferenceScreen();
//[android]获得子菜单所属类别Category
final DashboardCategory category =
mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
if (category == null) {
Log.d(tag, "NO dashboard tiles for " + tag);
return;
}
//[android]获得该类别Category下的所有菜单项
final List<Tile> tiles = category.getTiles();
if (tiles == null) {
Log.d(tag, "tile list is empty, skipping category " + category.key);
return;
}
// Create a list to track which tiles are to be removed.
final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
// Install dashboard tiles.
final boolean forceRoundedIcons = shouldForceRoundedIcon();
for (Tile tile : tiles) {
//[android]获得菜单项tile所属的key:如果未配置meta标签name为"com.android.settings.keyhint"的属性,则将class name拼接处理
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
if (TextUtils.isEmpty(key)) {
Log.d(tag, "tile does not contain a key, skipping " + tile);
continue;
}
//[android]过滤不需要显示的tile
if (!displayTile(tile)) {
continue;
}
if (mDashboardTilePrefKeys.containsKey(key)) {
// Have the key already, will rebind.
final Preference preference = screen.findPreference(key);
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
forceRoundedIcons, getMetricsCategory(), preference, tile, key,
mPlaceholderPreferenceController.getOrder());
} else {
// Don't have this key, add it.
//[android]根据tile类型创建首选项Preference
final Preference pref = createPreference(tile);
//[android]将Preference与Tile提供的数据绑定,并获取动态数据观察者
final List<DynamicDataObserver> observers =
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
forceRoundedIcons, getMetricsCategory(), pref, tile, key,
mPlaceholderPreferenceController.getOrder());
screen.addPreference(pref);
//[android]注册监听URI数据:title,summary,switch状态变化
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
remove.remove(key);
}
// Finally remove tiles that are gone.
for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
final String key = entry.getKey();
mDashboardTilePrefKeys.remove(key);
final Preference preference = screen.findPreference(key);
if (preference != null) {
screen.removePreference(preference);
}
unregisterDynamicDataObservers(entry.getValue());
}
}
动态加载流程
即refreshDashboardTiles()方法的流程,首先关注方法mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
-
getCategoryKey()
/** * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment. */ @VisibleForTesting public String getCategoryKey() { return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName()); }
DashboardFragmentRegistry类在静态块中初始化了PARENT_TO_CATEGORY_KEY_MAP键值对象
static { PARENT_TO_CATEGORY_KEY_MAP = new ArrayMap<>(); PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(), CategoryKey.CATEGORY_HOMEPAGE); PARENT_TO_CATEGORY_KEY_MAP.put( NetworkDashboardFragment.class.getName(), CategoryKey.CATEGORY_NETWORK); //省略... }
CategoryKey类定义多种category类别
// Activities in this category shows up in Settings homepage. public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage"; // Top level category. public static final String CATEGORY_NETWORK = "com.android.settings.category.ia.wireless"; //省略各种category定义
此方法getCategoryKey()是获取fragment的CategoryKey用于动态加载,比如主界面TopLevelSettings.java的key为"com.android.settings.category.ia.homepage"。网络和互联网界面NetworkDashboardFragment.java的key为"com.android.settings.category.ia.wireless"。
-
getTilesForCategory()
getTilesForCategory方法的具体实现在DashboardFeatureProviderImpl.java中
@Override public DashboardCategory getTilesForCategory(String key) { return mCategoryManager.getTilesByCategory(mContext, key); }
CategoryManager.java的getTilesByCategory()方法:
public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) { tryInitCategories(context); return mCategoryByKeyMap.get(categoryKey); }
-
tryInitCategories()
getTilesByCategory()方法返回值是通过categoryKey去mCategoryByKeyMap集合中寻找并返回DashboardCategory对象,那么tryInitCategories()方法应该会存在加载配置项然后对mCategoryByKeyMap赋值的操作。
private synchronized void tryInitCategories(Context context) { // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange // happens. tryInitCategories(context, false /* forceClearCache */); } private synchronized void tryInitCategories(Context context, boolean forceClearCache) { if (mCategories == null) { final boolean firstLoading = mCategoryByKeyMap.isEmpty(); if (forceClearCache) { mTileByComponentCache.clear(); } //清空mCategoryByKeyMap集合 mCategoryByKeyMap.clear(); //查询构建DashboardCategory的list集合 mCategories = TileUtils.getCategories(context, mTileByComponentCache); //遍历list填充mCategoryByKeyMap集合; for (DashboardCategory category : mCategories) { mCategoryByKeyMap.put(category.key, category); } //使用最新的category keys去替换旧的category keys backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); //根据PackageName排序Categories sortCategories(context, mCategoryByKeyMap); //去掉category中重复的tiles filterDuplicateTiles(mCategoryByKeyMap); if (firstLoading) { logTiles(context); } } }
主要还是查询构建DashboardCategory的list集合的逻辑,即TileUtils.getCategories()这个方法去获取相关数据。
-
getCategories()
源码:frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java
/** * Build a list of DashboardCategory. */ public static List<DashboardCategory> getCategories(Context context, Map<Pair<String, String>, Tile> cache) { final long startTime = System.currentTimeMillis(); //[android]是否完成开机向导设置 final boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0; final ArrayList<Tile> tiles = new ArrayList<>(); final UserManager userManager = (UserManager) context.getSystemService( Context.USER_SERVICE); //[android]遍历所有用户,调用loadTilesForAction()方法根据相关action获取相关tiles,填充tiles集合 for (UserHandle user : userManager.getUserProfiles()) { // TODO: Needs much optimization, too many PM queries going on here. if (user.getIdentifier() == ActivityManager.getCurrentUser()) { // Only add Settings for this user. loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true); loadTilesForAction(context, user, OPERATOR_SETTINGS, cache, OPERATOR_DEFAULT_CATEGORY, tiles, false); loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache, MANUFACTURER_DEFAULT_CATEGORY, tiles, false); } if (setup) { loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false); loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false); } } //[android]新建categoryMap集合,key为categoryKey,value为DashboardCategory对象 final HashMap<String, DashboardCategory> categoryMap = new HashMap<>(); for (Tile tile : tiles) { final String categoryKey = tile.getCategory(); DashboardCategory category = categoryMap.get(categoryKey); if (category == null) { category = new DashboardCategory(categoryKey); if (category == null) { Log.w(LOG_TAG, "Couldn't find category " + categoryKey); continue; } categoryMap.put(categoryKey, category); } category.addTile(tile); } final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values()); for (DashboardCategory category : categories) { //[android]对Tiles按照priority从大到小排序:Collections.sort(mTiles, Tile.TILE_COMPARATOR); category.sortTiles(); } if (DEBUG_TIMING) { Log.d(LOG_TAG, "getCategories took " + (System.currentTimeMillis() - startTime) + " ms"); } return categories; }
getCategories()方法新建tiles集合,遍历设备中所有用户,调用getTilesForAction()方法根据相关action分别从Activity和Provider两种ComponentInfo中获取Tile,最后填充tiles集合。
-
getTilesForAction()和loadTile()
设置中主要通过这些action去搜索系统中符合的组件去作为菜单项的Tile。
@VisibleForTesting static void loadTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean requireSettings) { final Intent intent = new Intent(action); if (requireSettings) { intent.setPackage(SETTING_PKG); } //[android]分别从Activity和Provider两种ComponentInfo中获取Tile loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent); loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent); } private static void loadActivityTiles(Context context, UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent) { final PackageManager pm = context.getPackageManager(); final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, user.getIdentifier()); for (ResolveInfo resolved : results) { if (!resolved.system) { // Do not allow any app to add to settings, only system ones. continue; } final ActivityInfo activityInfo = resolved.activityInfo; final Bundle metaData = activityInfo.metaData; loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo); } }
最终都会调用到loadTile()方法的逻辑:通过PackageManager服务从清单文件的Activity和Provider两种ComponentInfo中提取Tile信息。
private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData, ComponentInfo componentInfo) { // Skip loading tile if the component is tagged primary_profile_only but not running on // the current user. if (user.getIdentifier() != ActivityManager.getCurrentUser() && Tile.isPrimaryProfileOnly(componentInfo.metaData)) { Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent " + intent + " is primary profile only, skip loading tile for uid " + user.getIdentifier()); return; } //[android]解析AndroidManifest.xml meta标签中key="com.android.settings.category"的值 String categoryKey = defaultCategory; // Load category if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY)) && categoryKey == null) { Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent " + intent + " missing metadata " + (metaData == null ? "" : EXTRA_CATEGORY_KEY)); return; } else { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); } final boolean isProvider = componentInfo instanceof ProviderInfo; final Pair<String, String> key = isProvider ? new Pair<>(((ProviderInfo) componentInfo).authority, metaData.getString(META_DATA_PREFERENCE_KEYHINT)) : new Pair<>(componentInfo.packageName, componentInfo.name); //[android]构建具体的tile对象,比如ActivityTile,包含ComponentInfo和categoryKey Tile tile = addedCache.get(key); if (tile == null) { tile = isProvider ? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData) : new ActivityTile((ActivityInfo) componentInfo, categoryKey); addedCache.put(key, tile); } else { tile.setMetaData(metaData); } if (!tile.userHandle.contains(user)) { tile.userHandle.add(user); } //[android]将搜集到的tile对象添加到outTiles集合内并输出 if (!outTiles.contains(tile)) { outTiles.add(tile); } }
构建的Tile对象从AndroidManifest.xml meta-data配置中动态获取了哪些信息?
- 优先级order:META_DATA_KEY_ORDER = “com.android.settings.order”
/**
* Priority of this tile, used for display ordering.
*/
public int getOrder() {
if (hasOrder()) {
return mMetaData.getInt(META_DATA_KEY_ORDER);
} else {
return 0;
}
}
- 是否有switch控制开关:META_DATA_PREFERENCE_SWITCH_URI = “com.android.settings.switch_uri”
/**
* Check whether tile has a switch.
*/
public boolean hasSwitch() {
return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SWITCH_URI);
}
- 标题Title:META_DATA_PREFERENCE_TITLE = “com.android.settings.title”
title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
// Set the preference title by the component if no meta-data is found
if (title == null) {
title = getComponentLabel(context);
}
- 摘要Summary:META_DATA_PREFERENCE_SUMMARY = “com.android.settings.summary”
summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY);
- 图标Icon:META_DATA_PREFERENCE_ICON = “com.android.settings.icon”
int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON);
// Set the icon. Skip the transparent color for backward compatibility since Android S.
if (iconResId != 0 && iconResId != android.R.color.transparent) {
final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);
if (isIconTintable(context)) {
final TypedArray a = context.obtainStyledAttributes(new int[]{
android.R.attr.colorControlNormal});
final int tintColor = a.getColor(0, 0);
a.recycle();
icon.setTint(tintColor);
}
return icon;
} else {
return null;
}
- 唯一标识Key:META_DATA_PREFERENCE_KEYHINT = “com.android.settings.keyhint”
/**
- Optional key to use for this tile.
*/
public String getKey(Context context) {
if (!hasKey()) {
return null;
}
ensureMetadataNotStale(context);
if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
} else {
return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT);
}
}
当清单文件的meta-data没有配置key时,Tile对象的key是什么值?
DashboardFeatureProviderImpl.getDashboardKeyForTile()首先判断AndroidManifest.xml中是否配置了meta-data标签name为"com.android.settings.keyhint"的属性;
如果未配置该属性,则通过将class name拼接处理:DASHBOARD_TILE_PREF_KEY_PREFIX + getClassName(),即"dashboard_tile_pref_" + classname
@Override
public String getDashboardKeyForTile(Tile tile) {
if (tile == null) {
return null;
}
if (tile.hasKey()) {
return tile.getKey(mContext);
}
final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX);
final ComponentName component = tile.getIntent().getComponent();
sb.append(component.getClassName());
return sb.toString();
}
当清单文件的meta-data数据发生变更时,preference如何及时更新?
通过refreshDashboardTiles()中的bindPreferenceToTileAndGetObservers()方法。
@Override
public List<DynamicDataObserver> bindPreferenceToTileAndGetObservers(FragmentActivity activity,
boolean forceRoundedIcon, int sourceMetricsCategory, Preference pref, Tile tile,
String key, int baseOrder) {
if (pref == null) {
return null;
}
if (!TextUtils.isEmpty(key)) {
pref.setKey(key);
} else {
pref.setKey(getDashboardKeyForTile(tile));
}
final List<DynamicDataObserver> outObservers = new ArrayList<>();
DynamicDataObserver observer = bindTitleAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
observer = bindSummaryAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
observer = bindSwitchAndGetObserver(pref, tile);
if (observer != null) {
outObservers.add(observer);
}
//以下代码省略
}
bindTitleAndGetObserver、bindSummaryAndGetObserver、bindSwitchAndGetObserver方法分别对标题、摘要、switch开关状态进行了绑定监听,它们最终都调用了createDynamicDataObserver()方法进行异步刷新。
private DynamicDataObserver createDynamicDataObserver(String method, Uri uri, Preference pref) {
return new DynamicDataObserver() {
@Override
public Uri getUri() {
return uri;
}
@Override
public void onDataChanged() {
switch (method) {
case METHOD_GET_DYNAMIC_TITLE:
refreshTitle(uri, pref);
break;
case METHOD_GET_DYNAMIC_SUMMARY:
refreshSummary(uri, pref);
break;
case METHOD_IS_CHECKED:
refreshSwitch(uri, pref);
break;
}
}
};
}
通过以上一系列流程,设置项Preference就从AndroidManifest.xml文件meta-data标签属性中加载出来了。以Settings对“系统-开发者选项“的配置项为例:
<activity
android:name="Settings$DevelopmentSettingsDashboardActivity"
<!--若meta-data中无定义com.android.settings.title,则取lable为preference标题-->
android:label="@string/development_settings_title"
android:icon="@drawable/ic_settings_development"
android:exported="true"
android:enabled="false">
<intent-filter android:priority="1">
<action android:name="android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
<action android:name="com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS" />
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<!--通过TileUtils.loadActivityTiles()方法可以被Settings搜索到的action-->
<action android:name="com.android.settings.action.SETTINGS" />
</intent-filter>
<!--preference根据order值显示优先级-->
<meta-data android:name="com.android.settings.order" android:value="-40"/>
<!--类别,表示显示在SystemDashboardFragment中,DashboardFragmentRegistry.java有定义-->
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.system" />
<!--preference摘要-->
<meta-data android:name="com.android.settings.summary"
android:resource="@string/summary_empty"/>
<!--preference图标-->
<meta-data android:name="com.android.settings.icon"
android:resource="@drawable/ic_settings_development" />
<!--preference点击跳转的fragment:DevelopmentSettingsDashboardFragment-->
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.development.DevelopmentSettingsDashboardFragment" />
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
实践案例
1、将第三方应用(系统升级)页面动态添加为Settings首页的一级菜单
在系统升级应用清单文件添加如下配置即可:
<activity
<intent-filter android:priority="5">
<action android:name="com.android.settings.action.EXTRA_SETTINGS" />
</intent-filter>
<meta-data
android:name="com.android.settings.category"
android:value="com.android.settings.category.ia.homepage" />
<meta-data
android:name="com.android.settings.icon"
android:resource="@drawable/ic_settings_applications" />
<meta-data
android:name="com.android.settings.summary"
android:resource="@string/auto_check" />
</activity>
2、将Settings“系统-开发者选项-系统跟踪”菜单项去掉
系统跟踪界面来源于系统应用Traceur,对应的Activity为:com.android.traceur/.MainActivity
该Activity的meta-data中没有定义"com.android.settings.keyhint"的属性,根据getDashboardKeyForTile()的规则得出该Tile对应的key为:“dashboard_tile_pref_com.android.traceur.MainActivity”。
由此我们扩展下DashboardFragment.displayTile()方法,同时把该key加入到mSuppressInjectedTileKeys定义的list集合中。具体修改如下:
protected boolean displayTile(Tile tile) {
if (mSuppressInjectedTileKeys != null && tile.hasKey()) {
// For suppressing injected tiles for OEMs.
return !mSuppressInjectedTileKeys.contains(tile.getKey(getContext()));
}
//add by liuke
if (mSuppressInjectedTileKeys != null) {
String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
return !mSuppressInjectedTileKeys.contains(key);
}
//end by liuke
return true;
}