> 文章列表 > Android11.0 Setting一级菜单加载

Android11.0 Setting一级菜单加载

Android11.0 Setting一级菜单加载

我们先看看设置中的一级菜单是怎么加载出来的

1.Settings的入口

首先,从Settings的AndroidManifest.xml中开始:

 <!-- Alias for launcher activity only, as this belongs to each profile. --><activity-alias android:name="Settings"android:label="@string/settings_label_launcher"android:taskAffinity="com.android.settings.root"android:launchMode="singleTask"android:targetActivity=".homepage.SettingsHomepageActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter><meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/></activity-alias>

点击Launcher中的Settings图标,会打开别名为Settings的Activity,其实际目标Activity是SettingsHomepageActivity,接着查看SettingsHomepageActivity:

  • 源码位置:vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/homepage/SettingsHomepageActivity.java
 public class SettingsHomepageActivity extends FragmentActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.settings_homepage_container);final View root = findViewById(R.id.settings_homepage_container);root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);setHomepageContainerPaddingTop();// 加载顶部的搜索框final Toolbar toolbar = findViewById(R.id.search_action_bar);FeatureFactory.getFactory(this).getSearchFeatureProvider().initSearchToolbar(this /* activity */, toolbar, SettingsEnums.SETTINGS_HOMEPAGE);final ImageView avatarView = findViewById(R.id.account_avatar);getLifecycle().addObserver(new AvatarViewMixin(this, avatarView));getLifecycle().addObserver(new HideNonSystemOverlayMixin(this));if (!getSystemService(ActivityManager.class).isLowRamDevice()) {// Only allow contextual feature on high ram devices.showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content);}// 加载TopLevelSettingsshowFragment(new TopLevelSettings(), R.id.main_content);((FrameLayout) findViewById(R.id.main_content)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);}

布局文件对应 settings_homepage_container.xml,加载了顶部搜索框和新创建TopLevelSettings 填充 main_content

  • 源码路径:vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/homepage/TopLevelSettings.java
 public class TopLevelSettings extends DashboardFragment implementsPreferenceFragmentCompat.OnPreferenceStartFragmentCallback {private static final String TAG = "TopLevelSettings";public TopLevelSettings() {final Bundle args = new Bundle();// Disable the search icon because this page uses a full search view in actionbar.args.putBoolean(NEED_SEARCH_ICON_IN_ACTION_BAR, false);setArguments(args);}@Overrideprotected int getPreferenceScreenResId() {return R.xml.top_level_settings;}@Overrideprotected String getLogTag() {return TAG;}

先看下top_level_settings布局文件:

 <PreferenceScreenxmlns:android="http://schemas.android.com/apk/res/android"xmlns:settings="http://schemas.android.com/apk/res-auto"android:key="top_level_settings"><Preferenceandroid:key="top_level_network" // 网络和互联网android:title="@string/network_dashboard_title"android:summary="@string/summary_placeholder"android:icon="@drawable/ic_homepage_network"android:order="-120"android:fragment="com.android.settings.network.NetworkDashboardFragment"settings:controller="com.android.settings.network.TopLevelNetworkEntryPreferenceController"/><Preferenceandroid:key="top_level_connected_devices" // 已连接设备android:title="@string/connected_devices_dashboard_title"android:summary="@string/summary_placeholder"android:icon="@drawable/ic_homepage_connected_device"android:order="-110"android:fragment="com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment"settings:controller="com.android.settings.connecteddevice.TopLevelConnectedDevicesPreferenceController"/><Preferenceandroid:key="top_level_apps_and_notifs" // 应用和通知android:title="@string/app_and_notification_dashboard_title"android:summary="@string/app_and_notification_dashboard_summary"android:icon="@drawable/ic_homepage_apps"android:order="-100"android:fragment="com.android.settings.applications.AppAndNotificationDashboardFragment"/>

可以看到都是一个个的Preference,对应设置界面的条目,但是数量不对等,有些项不存在比如Google设置,所以还存在动态添加。

2.一级设置菜单的加载

TopLevelSettings继承自DashboardFragment,DashboardFragment继承自SettingsPreferenceFragment,看下DashboardFragment:

  • 源码位置:vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/dashboard/DashboardFragment.java
  @Overridepublic void onCreatePreferences(Bundle savedInstanceState, String rootKey) {checkUiBlocker(mControllers);refreshAllPreferences(getLogTag());mControllers.stream().map(controller -> (Preference) findPreference(controller.getPreferenceKey())).filter(Objects::nonNull).forEach(preference -> {// Give all controllers a chance to handle click.preference.getExtras().putInt(CATEGORY, getMetricsCategory());});}.../* 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();  // 加载xml中所有的preferencerefreshDashboardTiles(tag); // 动态添加preferencefinal Activity activity = getActivity();if (activity != null) {Log.d(tag, "All preferences added, reporting fully drawn");activity.reportFullyDrawn();}updatePreferenceVisibility(mPreferenceControllers);}

refreshAllPreferences()方法中有两个关键性的方法,一个是displayResourceTiles()从xml布局文件中加载preference,一个是refreshDashboardTiles()动态创建添加preference。

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

addPreferencesFromResource()方法类似于activity中的setContentView()方法,加载布局文件中的preference,一级菜单的fragment是TopLevelSettings ,这里getPreferenceScreenResId()获取的就是top_level_settings.xml

2.2动态添加一级菜单preference:
 /* Refresh preference items backed by DashboardCategory.*/private void refreshDashboardTiles(final String tag) {final PreferenceScreen screen = getPreferenceScreen();// 获取与当前调用者key值相同的DashboardCategory final DashboardCategory category =mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());if (category == null) {Log.d(tag, "NO dashboard tiles for " + tag);return;}// 获取DashboardCategory下的所有项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) {final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);if (TextUtils.isEmpty(key)) {Log.d(tag, "tile does not contain a key, skipping " + tile);continue;}// config_suppress_injected_tile_keys数组中的key不会显示if (!displayTile(tile)) { continue;}// 首次进入Settings,会走elseif (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.  创建Preference并添加final Preference pref = createPreference(tile);final List<DynamicDataObserver> observers =mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),forceRoundedIcons, getMetricsCategory(), pref, tile, key,mPlaceholderPreferenceController.getOrder());screen.addPreference(pref);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()方法主要有两个点:
①通过getTilesForCategory()获取与当前调用者key值相同的DashboardCategory,接着获取DashboardCategory中存储的所有tile。
②遍历所有tile,构建preference,通过bindPreferenceToTileAndGetObservers方法,将tile中信息与Preference绑定,并将preference添加到PreferenceScreen中,然后显示出来。

3.如何获取DashboardCategory

主要看下getTilesForCategory(getCategoryKey())方法,分析是如何获取DashboardCategory的
先看getCategoryKey()方法:

 /* Returns the CategoryKey for loading {@link DashboardCategory} for this fragment.*/@VisibleForTestingpublic String getCategoryKey() {return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName());}

这个方法是直接从DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP中获取key为getClass().getName()的value值,而.PARENT_TO_CATEGORY_KEY_MAP是在DashboardFragmentRegistry中定义的静态键值对

  • 源码位置: vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/dashboard/DashboardFragmentRegistry.java
    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);PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),CategoryKey.CATEGORY_CONNECT);
  • 源码位置:frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
 public final class CategoryKey {// 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";public static final String CATEGORY_CONNECT = "com.android.settings.category.ia.connect";public static final String CATEGORY_DEVICE = "com.android.settings.category.ia.device";public static final String CATEGORY_APPS = "com.android.settings.category.ia.apps";

一级菜单对应的fragment是TopLevelSettings ,所以这里key的值是CategoryKey.CATEGORY_HOMEPAGE,也就是com.android.settings.category.ia.homepage(一级菜单的CategoryKey)

再看getTilesForCategory()方法:
DashboardFeatureProviderImpl是DashboardFeatureProvider接口的实现类

  • 源码位置:vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
 @Overridepublic DashboardCategory getTilesForCategory(String key) {return mCategoryManager.getTilesByCategory(mContext, key);}
  • 源码位置:vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/dashboard/CategoryManager.java
 public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {tryInitCategories(context);return mCategoryByKeyMap.get(categoryKey);}...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) {if (forceClearCache) {mTileByComponentCache.clear();}mCategoryByKeyMap.clear();mCategories = TileUtils.getCategories(context, mTileByComponentCache);for (DashboardCategory category : mCategories) {mCategoryByKeyMap.put(category.key, category);}backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);sortCategories(context, mCategoryByKeyMap);filterDuplicateTiles(mCategoryByKeyMap);}}   

可以看到通过TileUtils.getCategories()方法获取DashboardCategory集合,然后遍历DashboardCategory集合以键值对的方式添加到mCategoryByKeyMap中 ,外部在根据key值从mCategoryByKeyMap中获取对应的DashboardCategory

再看TileUtils.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();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);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);}} 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);android.util.Log.d(LOG_TAG,"categoryKey="+categoryKey+" PackageName="+tile.getPackageName()+" Title="+tile.getTitle(context)+" ComponentName="+tile.getComponentName()+" "+tile.getIntent()+" key="+tile.getKey(context));}final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());for (DashboardCategory category : categories) {category.sortTiles();}if (DEBUG_TIMING) {Log.d(LOG_TAG, "getCategories took "+ (System.currentTimeMillis() - startTime) + " ms");}return categories;}

通过loadTilesForAction()方法给tiles赋值,然后遍历tiles数组,categoryMap根据tile的categoryKey判断是否包含DashboardCategory,不包含则往里添加,然后将tile添加进DashboardCategory中,遍历完之后得到categories数组,最后进行排序。

loadTilesForAction()最终是进入到loadTile()方法中

 @VisibleForTestingstatic 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);// 如果intent是com.android.settings.action.SETTINGS将包名设置为com.android.settingsif (requireSettings) {intent.setPackage(SETTING_PKG);}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);}}private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,ComponentInfo componentInfo) {String categoryKey = defaultCategory;// Load categoryif ((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 {// 通过com.android.settings.category获取categoryKey的值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);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);}if (!outTiles.contains(tile)) {outTiles.add(tile);}}

通过 PackageManager 查询系统中所有带指定 Action 的 Intent 对应信息 ResolveInfo 集合,然后遍历该集合获取符合条件应用信息包名、类名、categoryKey等构造 tile对象,最终添加进 tiles数组中。

最主要的就是指定的Action和CategoryKey的值
Action

public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
private static final String OPERATOR_SETTINGS = "com.android.settings.OPERATOR_APPLICATION_SETTING";
private static final String MANUFACTURER_SETTINGS = "com.android.settings.MANUFACTURER_APPLICATION_SETTING";

我们可以在Settings的AndroidManifest.xml文件中看到类似的配置

 <intent-filter android:priority="5"><action android:name="com.android.settings.action.SETTINGS" /></intent-filter><meta-dataandroid:name="com.android.settings.category"android:value="com.android.settings.category.ia.homepage" />

tile就是Settings主页面中一个一级菜单项。