您现在的位置是:首页 >技术教程 >Android11.0 Setting一级菜单加载网站首页技术教程

Android11.0 Setting一级菜单加载

framework-coder 2023-05-18 00:00:02
简介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 {

    @Override
    protected 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);
        }
        // 加载TopLevelSettings
        showFragment(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 implements
        PreferenceFragmentCompat.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);
    }

    @Override
    protected int getPreferenceScreenResId() {
        return R.xml.top_level_settings;
    }

    @Override
    protected String getLogTag() {
        return TAG;
    }

先看下top_level_settings布局文件:

 <PreferenceScreen
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:settings="http://schemas.android.com/apk/res-auto"
    android:key="top_level_settings">

    <Preference
        android: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"/>

    <Preference
        android: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"/>

    <Preference
        android: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
  @Override
  public 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中所有的preference

      refreshDashboardTiles(tag); // 动态添加preference

      final 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,会走else
         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.  创建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中,然后显示出来。

2.2.1 如何获取DashboardCategory

主要看下getTilesForCategory(getCategoryKey())方法,分析是如何获取DashboardCategory的
先看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中获取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
 @Override
 public 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) {
         // 进入一级菜单,categoryKey值是com.android.settings.category.ia.homepage
         // 若进入其他二级菜单比如显示,categoryKey值是com.android.settings.category.ia.display
         final String categoryKey = tile.getCategory();
         // 一个categoryKey对应一个DashboardCategory 
         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);
         }
         // 包含有相同categoryKey的tile,都添加到对应的DashboardCategory 
         category.addTile(tile);
     }
     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()方法中

 @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);
     // 如果intent是com.android.settings.action.SETTINGS将包名设置为com.android.settings
     if (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();
     // 获取所有包含特定Action的ResolveInfo
     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 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 {
         // 通过com.android.settings.category获取categoryKey的值
         // AndoridManifest.xml中定义
         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";

总结一下,首次进入设置主界面时,先加载top_level_settings.xml中的一级菜单项;然后通过TileUtils.getCategories()方法解析带有特定Action的Intent,动态获取所有非布局中定义的一级菜单项(Google和一些其他系统应用)。

下面贴下一级菜单DuraSpeed(快霸)的AndroidManifest.xml中部分代码供参考

 <activity
     android:name=".DuraSpeedMainActivity"
     android:configChanges="orientation|keyboardHidden|screenSize|mcc|mnc|navigation"
     android:label="@string/app_name"
     android:launchMode="singleInstance"
     android:permission="com.mediatek.duraspeed.START_DURASPEED_APP">
     <intent-filter>
         <action android:name="android.intent.action.MAIN" />
         <category android:name="android.intent.category.DEFAULT" />
         <category android:name="android.intent.category.INFO" />
     </intent-filter>
     <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_rb" />
 </activity>
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。