当前位置: 移动技术网 > IT编程>开发语言>Java > Android 10.0 Settings源码分析之主界面加载(二)

Android 10.0 Settings源码分析之主界面加载(二)

2020年09月28日  | 移动技术网IT编程  | 我要评论
上篇Android 10.0 Settings源码分析之主界面加载(一)主要记录了主界面xml静态加载(调用displayResourceTiles()方法),本篇主要记录从代码中动态加载设置项(调用refreshDashboardTiles(TAG)方法,其中TAG为 “TopLevelSettings”)。DashboardFragmentRegistry.java说到动态加载,先介绍下这个类DashboardFragmentRegistry.java。这个类主要作用类似于一个注册表的作用,注册记录

上篇Android 10.0 Settings源码分析之主界面加载(一)主要记录了主界面xml静态加载(调用displayResourceTiles()方法),本篇主要记录从代码中动态加载设置项(调用refreshDashboardTiles(TAG)方法,其中TAG为 “TopLevelSettings”)。

DashboardFragmentRegistry.java

说到动态加载,先介绍下这个类DashboardFragmentRegistry.java。这个类主要作用类似于一个注册表的作用,注册记录什么界面(fragment)使用哪一个host去进行相应动态索引加载。

/**
 * A registry to keep track of which page hosts which category.
 */
public class DashboardFragmentRegistry {

    /**
     * Map from parent fragment to category key. The parent fragment hosts child with
     * category_key.
     */
    public static final Map<String, String> PARENT_TO_CATEGORY_KEY_MAP;

    /**
     * Map from category_key to parent. This is a helper to look up which fragment hosts the
     * category_key.
     */
    public static final Map<String, String> CATEGORY_KEY_TO_PARENT_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);
        PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_CONNECT);
        PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_DEVICE);
        PARENT_TO_CATEGORY_KEY_MAP.put(AppAndNotificationDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_APPS);
        PARENT_TO_CATEGORY_KEY_MAP.put(PowerUsageSummary.class.getName(),
                CategoryKey.CATEGORY_BATTERY);
        PARENT_TO_CATEGORY_KEY_MAP.put(DisplaySettings.class.getName(),
                CategoryKey.CATEGORY_DISPLAY);
        PARENT_TO_CATEGORY_KEY_MAP.put(SoundSettings.class.getName(),
                CategoryKey.CATEGORY_SOUND);
        PARENT_TO_CATEGORY_KEY_MAP.put(StorageDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_STORAGE);
        PARENT_TO_CATEGORY_KEY_MAP.put(SecuritySettings.class.getName(),
                CategoryKey.CATEGORY_SECURITY);
        PARENT_TO_CATEGORY_KEY_MAP.put(AccountDetailDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_ACCOUNT_DETAIL);
        PARENT_TO_CATEGORY_KEY_MAP.put(AccountDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_ACCOUNT);
        PARENT_TO_CATEGORY_KEY_MAP.put(
                SystemDashboardFragment.class.getName(), CategoryKey.CATEGORY_SYSTEM);
        PARENT_TO_CATEGORY_KEY_MAP.put(LanguageAndInputSettings.class.getName(),
                CategoryKey.CATEGORY_SYSTEM_LANGUAGE);
        PARENT_TO_CATEGORY_KEY_MAP.put(DevelopmentSettingsDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_SYSTEM_DEVELOPMENT);
        PARENT_TO_CATEGORY_KEY_MAP.put(ConfigureNotificationSettings.class.getName(),
                CategoryKey.CATEGORY_NOTIFICATIONS);
        PARENT_TO_CATEGORY_KEY_MAP.put(LockscreenDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_SECURITY_LOCKSCREEN);
        PARENT_TO_CATEGORY_KEY_MAP.put(ZenModeSettings.class.getName(),
                CategoryKey.CATEGORY_DO_NOT_DISTURB);
        PARENT_TO_CATEGORY_KEY_MAP.put(GestureSettings.class.getName(),
                CategoryKey.CATEGORY_GESTURES);
        PARENT_TO_CATEGORY_KEY_MAP.put(NightDisplaySettings.class.getName(),
                CategoryKey.CATEGORY_NIGHT_DISPLAY);
        PARENT_TO_CATEGORY_KEY_MAP.put(PrivacyDashboardFragment.class.getName(),
                CategoryKey.CATEGORY_PRIVACY);
        PARENT_TO_CATEGORY_KEY_MAP.put(EnterprisePrivacySettings.class.getName(),
                CategoryKey.CATEGORY_ENTERPRISE_PRIVACY);
        PARENT_TO_CATEGORY_KEY_MAP.put(LegalSettings.class.getName(),
                CategoryKey.CATEGORY_ABOUT_LEGAL);
        PARENT_TO_CATEGORY_KEY_MAP.put(MyDeviceInfoFragment.class.getName(),
                CategoryKey.CATEGORY_MY_DEVICE_INFO);
        PARENT_TO_CATEGORY_KEY_MAP.put(BatterySaverSettings.class.getName(),
                CategoryKey.CATEGORY_BATTERY_SAVER_SETTINGS);

        CATEGORY_KEY_TO_PARENT_MAP = new ArrayMap<>(PARENT_TO_CATEGORY_KEY_MAP.size());

        for (Map.Entry<String, String> parentToKey : PARENT_TO_CATEGORY_KEY_MAP.entrySet()) {
            CATEGORY_KEY_TO_PARENT_MAP.put(parentToKey.getValue(), parentToKey.getKey());
        }
    }
}

由于目前主要记录主界面加载,而上篇分析道主界面fragment为TopLevelSettings.java,相应的CategoryKey为:

        PARENT_TO_CATEGORY_KEY_MAP.put(TopLevelSettings.class.getName(),
                CategoryKey.CATEGORY_HOMEPAGE);
    // Activities in this category shows up in Settings homepage.
    public static final String CATEGORY_HOMEPAGE = "com.android.settings.category.ia.homepage";

可以看到主界面动态加载关键字应是"com.android.settings.category.ia.homepage"。

refreshDashboardTiles()

上篇Android 10.0 Settings源码分析之主界面加载(一)记录到DashboardFragment.java的refreshDashboardTiles()方法主要是用DashboardCategory动态加载设置项的。
先来整体看看此方法相关逻辑:

    /**
     * Refresh preference items backed by DashboardCategory.
     */
    @VisibleForTesting
    void refreshDashboardTiles(final String TAG) {
        final PreferenceScreen screen = getPreferenceScreen();
        final DashboardCategory category =
                mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
        if (category == null) {
            Log.d(TAG, "NO dashboard tiles for " + TAG);
            return;
        }
        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 List<String> remove = new ArrayList<>(mDashboardTilePrefKeys);

        // There are dashboard tiles, so we need to install SummaryLoader.
        if (mSummaryLoader != null) {
            mSummaryLoader.release();
        }
        final Context context = getContext();
        mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
        mSummaryLoader.setSummaryConsumer(this);
        // 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;
            }
            if (!displayTile(tile)) {
                continue;
            }
            if (mDashboardTilePrefKeys.contains(key)) {
                // Have the key already, will rebind.
                final Preference preference = screen.findPreference(key);
                mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                        getMetricsCategory(), preference, tile, key,
                        mPlaceholderPreferenceController.getOrder());
            } else {
                // Don't have this key, add it.
                final Preference pref = new Preference(getPrefContext());
                mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                        getMetricsCategory(), pref, tile, key,
                        mPlaceholderPreferenceController.getOrder());
                screen.addPreference(pref);
                mDashboardTilePrefKeys.add(key);
            }
            remove.remove(key);
        }
        // Finally remove tiles that are gone.
        for (String key : remove) {
            mDashboardTilePrefKeys.remove(key);
            final Preference preference = screen.findPreference(key);
            if (preference != null) {
                screen.removePreference(preference);
            }
        }
        mSummaryLoader.setListening(true);
    }

其中TAG为"TopLevelSettings"

getTilesForCategory()

packages/apps/Settings/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java

        final DashboardCategory category =
                mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());

1、先来看 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());
    }

此方法是获取相关fragment的CategoryKey用于动态加载,根据上面分析主界面是TopLevelSettings.java,故而key为"com.android.settings.category.ia.homepage"。

2、继续看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);
    }

可以看到方法返回值是通过关键字key(“com.android.settings.category.ia.homepage”)去map集合中索引返回DashboardCategory的对象,故tryInitCategories()方法肯定是存在加载然后对map赋值的操作。直接看tryInitCategories()方法:

    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);
        }
    }
  1. 首先清空mCategoryByKeyMap集合;
  2. 调用getCategories()方法,去查询构建DashboardCategory的list列表;
  3. 遍历list填充mCategoryByKeyMap集合;
  4. 检查是否使用旧的category keys,如果是,使用最新的category keys去替换;
  5. 排序;
  6. 去掉category中重复的tiles。

可以看到主要还是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();
        boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
                != 0;
        ArrayList<Tile> tiles = new ArrayList<>();
        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.
                getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
                getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                        OPERATOR_DEFAULT_CATEGORY, tiles, false);
                getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                        MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
            }
            if (setup) {
                getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
                getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
            }
        }

        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);
        }
        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;
    }
  1. 判断是否完成开机向导设置,setup 为true时表明已完成;
  2. 新建tiles 集合
  3. 遍历设备中所有用户,调用getTilesForAction()方法根据相关action获取相关tiles,填充tiles集合;设置中主要通过此action去搜索系统中符合的活动去作为主界面TopLevelSettings的tile,相关action定义如下:
    /**
     * Settings will search for system activities of this action and add them as a top level
     * settings tile using the following parameters.
     *
     * <p>A category must be specified in the meta-data for the activity named
     * {@link #EXTRA_CATEGORY_KEY}
     *
     * <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
     * otherwise the label for the activity will be used.
     *
     * <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
     * otherwise the icon for the activity will be used.
     *
     * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
     */
    public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";

    /**
     * @See {@link #EXTRA_SETTINGS_ACTION}.
     */
    public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";

    /**
     * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
     */
    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 OPERATOR_DEFAULT_CATEGORY =
            "com.android.settings.category.wireless";

    private static final String MANUFACTURER_SETTINGS =
            "com.android.settings.MANUFACTURER_APPLICATION_SETTING";

    private static final String MANUFACTURER_DEFAULT_CATEGORY =
            "com.android.settings.category.device";
  1. 新建categoryMap集合, HashMap<String, DashboardCategory> categoryMap,其中map的key为categoryKey;
  2. 遍历tiles集合,以每个tile的tile.getCategory()的值为构造参数,创建DashboardCategory对象,并将tile添加到此对象中,将此填充到map集合中;
  3. 将categoryMap的值赋值给ArrayList cagtories以便执行排序算法,遍历新集合利用Collections函数和比较器TILE_COMPARATOR将category.tiles按照priority从大到小排序。

可以看到主要调用getTilesForAction()方法获取数据源:

    @VisibleForTesting
    static void getTilesForAction(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);
        }
        final PackageManager pm = context.getPackageManager();
        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;
            }
            ActivityInfo activityInfo = resolved.activityInfo;
            Bundle metaData = activityInfo.metaData;
            String categoryKey = defaultCategory;

            // Load category
            if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
                    && categoryKey == null) {
                Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent "
                        + intent + " missing metadata "
                        + (metaData == null ? "" : EXTRA_CATEGORY_KEY));
                continue;
            } else {
                categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
            }

            Pair<String, String> key = new Pair<>(activityInfo.packageName, activityInfo.name);
            Tile tile = addedCache.get(key);
            if (tile == null) {
                tile = new Tile(activityInfo, 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);
            }
        }
    }
  1. 通过action构建intent,根据requireSettings来决定是否指定Settings进程包名:
        final Intent intent = new Intent(action);
        if (requireSettings) {
            intent.setPackage(SETTING_PKG);
        }
  1. 使用PM查询符合相关intent action支持的ResolveInfo集合,每个ResolveInfo对象主要是从AndroidManifest.xml中解析出的:
        final PackageManager pm = context.getPackageManager();
        List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
                PackageManager.GET_META_DATA, user.getIdentifier());
  1. 遍历ResolveInfo集合,获取集合中每一个ResolveInfo对象,判断是否是系统进程,是否AndroidManifest.xml配置的meta标签name包含com.android.settings.category并解析其value值,构建tile对象,并将此添加到tiles集合内输出。

可以看到每个Tile对象,都是包含有从AndroidManifest.xml解析出的Resolveinfo对象和解析meta标签name包含com.android.settings.category value的值。

总结:

  1. 遍历设备所有用户,调getTilesForAction()方法利用PM去检索设备中所有符合传入action的activity ResolveInfo;
  2. 判断每一个ResolveInfo是否是系统进程,是否AndroidManifest.xml中配置的meta标签name包含"com.android.settings.category"的 value,将符合条件的以此value的值和ResolveInfo对象构建tile对象;并以此构建填充tiles集合
  3. 构建HashMap<String, DashboardCategory> categoryMap集合,以AndroidManifest.xml中配置的meta标签name包含"com.android.settings.category"的 value值为参数来构建DashboardCategory对象,遍历tiles集合将符合条件的tile填充DashboardCategory对象(DashboardCategory对象即包含可以显示在界面上的设置项),并以value为key,DashboardCategory对象为value填充categoryMap集合;
  4. 将categoryMap的值赋值给ArrayList cagtories以便执行排序算法,遍历新集合利用Collections函数和比较器TILE_COMPARATOR将category.tiles按照priority从大到小排序。
  5. 经过对Categories集合的更新、排序、去重等操作,得到最终所需的mCategoryByKeyMap集合;
  6. 再根据所传入的TAG(TopLevelSettings),去mCategoryByKeyMap集合检索,最终得出适合在Settings主界面TopLevelSettings中可以显示的DashboardCategory对象。

继续回到refreshDashboardTiles()方法:

        final DashboardCategory category =
                mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
        if (category == null) {
            Log.d(TAG, "NO dashboard tiles for " + TAG);
            return;
        }
        final List<Tile> tiles = category.getTiles();
        if (tiles == null) {
            Log.d(TAG, "tile list is empty, skipping category " + category.key);
            return;
        }

通过getTilesForCategory()方法得到适合在Settings主界面TopLevelSettings中可以显示的DashboardCategory对象。判断对象是否为空,对象内是否包含tiles集合;

        // Create a list to track which tiles are to be removed.
        final List<String> remove = new ArrayList<>(mDashboardTilePrefKeys);

        // There are dashboard tiles, so we need to install SummaryLoader.
        if (mSummaryLoader != null) {
            mSummaryLoader.release();
        }
        final Context context = getContext();
        mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey());
        mSummaryLoader.setSummaryConsumer(this);
        // Install dashboard tiles.
        final boolean forceRoundedIcons = shouldForceRoundedIcon();
  1. 新建remove集合,跟踪哪些tile需要被移除的;
  2. 初始化SummaryLoader,顾名思义,主要用来加载更新tile的summary;即对于界面显示来说即preference的summary的更新;

开始遍历适合在Settings主界面TopLevelSettings中可以显示的DashboardCategory对象内的tile集合,每个tile包含从AndroidManifest.xml解析出的resolveinfo对象,此即为初步符合条件可以显示在主界面的动态设置项:

        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;
            }
            if (!displayTile(tile)) {
                continue;
            }
            if (mDashboardTilePrefKeys.contains(key)) {
                // Have the key already, will rebind.
                final Preference preference = screen.findPreference(key);
                mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                        getMetricsCategory(), preference, tile, key,
                        mPlaceholderPreferenceController.getOrder());
            } else {
                // Don't have this key, add it.
                final Preference pref = new Preference(getPrefContext());
                mDashboardFeatureProvider.bindPreferenceToTile(getActivity(), forceRoundedIcons,
                        getMetricsCategory(), pref, tile, key,
                        mPlaceholderPreferenceController.getOrder());
                screen.addPreference(pref);
                mDashboardTilePrefKeys.add(key);
            }
            remove.remove(key);
        }
  1. getDashboardKeyForTile();DashboardFeatureProviderImpl.java的getDashboardKeyForTile()方法:
    @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();
    }

判断AndroidManifest.xml中是否配置了meta标签name为"com.android.settings.keyhint"的属性;
如果配置,则获取其value值作为后续显示在界面的preference的key值:

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

其中ensureMetadataNotStale()方法主要是确保能获取最新的mMetaData:

    /**
     * Ensures metadata is not stale for this tile.
     */
    private void ensureMetadataNotStale(Context context) {
        final PackageManager pm = context.getApplicationContext().getPackageManager();

        try {
            final long lastUpdateTime = pm.getPackageInfo(mActivityPackage,
                    PackageManager.GET_META_DATA).lastUpdateTime;
            if (lastUpdateTime == mLastUpdateTime) {
                // All good. Do nothing
                return;
            }
            // App has been updated since we load metadata last time. Reload metadata.
            mActivityInfo = null;
            getActivityInfo(context);
            mLastUpdateTime = lastUpdateTime;
        } catch (PackageManager.NameNotFoundException e) {
            Log.d(TAG, "Can't find package, probably uninstalled.");
        }
    }

    private ActivityInfo getActivityInfo(Context context) {
        if (mActivityInfo == null) {
            final PackageManager pm = context.getApplicationContext().getPackageManager();
            final Intent intent = new Intent().setClassName(mActivityPackage, mActivityName);
            final List<ResolveInfo> infoList =
                    pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
            if (infoList != null && !infoList.isEmpty()) {
                mActivityInfo = infoList.get(0).activityInfo;
                mMetaData = mActivityInfo.metaData;
            } else {
                Log.e(TAG, "Cannot find package info for "
                        + intent.getComponent().flattenToString());
            }
        }
        return mActivityInfo;
    }

查询此App最后一次修改的时间与上一次修改时间是否一致,如果不是则重新通过PM查询更新mMetaData属性,确保mMetaData属性是从App内获取到的最新的。

如果未配置meta标签name为"com.android.settings.keyhint"的属性,则通过将activity name拼接处理:

        final StringBuilder sb = new StringBuilder(DASHBOARD_TILE_PREF_KEY_PREFIX);
        final ComponentName component = tile.getIntent().getComponent();
        sb.append(component.getClassName());
        return sb.toString();

以主设置界面的Google设置项为例:
在这里插入图片描述
其拼接处理后即为:“dashboard_tile_pref_com.google.android.gms.app.settings.GoogleSettingsIALink”

  1. 判断获取到的key是否为空,判断此设置项是否需要被显示;
  2. 调用bindPreferenceToTile()方法,对preference进行数据绑定;
  3. 调用setListening()方法,设置监听,以便于后续各个preference后续可以自行根据需要更新summary。

bindPreferenceToTile()

packages/apps/Settings/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java

    @Override
    public void bindPreferenceToTile(FragmentActivity activity, boolean forceRoundedIcon,
            int sourceMetricsCategory, Preference pref, Tile tile, String key, int baseOrder) {
        if (pref == null) {
            return;
        }
        pref.setTitle(tile.getTitle(activity.getApplicationContext()));
        if (!TextUtils.isEmpty(key)) {
            pref.setKey(key);
        } else {
            pref.setKey(getDashboardKeyForTile(tile));
        }
        bindSummary(pref, tile);
        bindIcon(pref, tile, forceRoundedIcon);
        final Bundle metadata = tile.getMetaData();
        String clsName = null;
        String action = null;

        if (metadata != null) {
            clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
            action = metadata.getString(META_DATA_KEY_INTENT_ACTION);
        }
        if (!TextUtils.isEmpty(clsName)) {
            pref.setFragment(clsName);
        } else {
            final Intent intent = new Intent(tile.getIntent());
            intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
                    sourceMetricsCategory);
            if (action != null) {
                intent.setAction(action);
            }
            pref.setOnPreferenceClickListener(preference -> {
                launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory);
                return true;
            });
        }
        final String skipOffsetPackageName = activity.getPackageName();


        if (tile.hasOrder()) {
            final int order = tile.getOrder();
            boolean shouldSkipBaseOrderOffset = TextUtils.equals(
                    skipOffsetPackageName, tile.getIntent().getComponent().getPackageName());
            if (shouldSkipBaseOrderOffset || baseOrder == Preference.DEFAULT_ORDER) {
                pref.setOrder(order);
            } else {
                pref.setOrder(order + baseOrder);
            }
        }
    }
  1. 设置preference title,根据getTitle()方法获取title:
pref.setTitle(tile.getTitle(activity.getApplicationContext()));
    /**
     * Title of the tile that is shown to the user.
     */
    public CharSequence getTitle(Context context) {
        CharSequence title = null;
        ensureMetadataNotStale(context);
        final PackageManager packageManager = context.getPackageManager();
        if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
            if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
                try {
                    final Resources res =
                            packageManager.getResourcesForApplication(mActivityPackage);
                    title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE));
                } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
                    Log.w(TAG, "Couldn't find info", e);
                }
            } else {
                title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
            }
        }
        // Set the preference title to the activity's label if no
        // meta-data is found
        if (title == null) {
            final ActivityInfo activityInfo = getActivityInfo(context);
            if (activityInfo == null) {
                return null;
            }
            title = activityInfo.loadLabel(packageManager);
        }
        return title;
    }

首先确保tile中保存的meta的对象是最新的,再去读取AndroidManifest.xml中是否配置了Activity的meta标签name为"com.android.settings.title"的属性值,如果配置了则以此属性值为preference title;反之则再去尝试读取其AndroidManifest.xml配置的activity的 "android:label"属性值。

  1. 设置preference的key,主要还是通过调用getDashboardKeyForTile()方法去获取,上面已详细分析过:
        if (!TextUtils.isEmpty(key)) {
            pref.setKey(key);
        } else {
            pref.setKey(getDashboardKeyForTile(tile));
        }
  1. 设置preference的summary:
bindSummary(pref, tile);
    private void bindSummary(Preference preference, Tile tile) {
        final CharSequence summary = tile.getSummary(mContext);
        if (summary != null) {
            preference.setSummary(summary);
        } else if (tile.getMetaData() != null
                && tile.getMetaData().containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
            // Set a placeholder summary before  starting to fetch real summary, this is necessary
            // to avoid preference height change.
            preference.setSummary(R.string.summary_placeholder);

            ThreadUtils.postOnBackgroundThread(() -> {
                final Map<String, IContentProvider> providerMap = new ArrayMap<>();
                final String uri = tile.getMetaData().getString(META_DATA_PREFERENCE_SUMMARY_URI);
                final String summaryFromUri = TileUtils.getTextFromUri(
                        mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
                ThreadUtils.postOnMainThread(() -> preference.setSummary(summaryFromUri));
            });
        } else {
            preference.setSummary(R.string.summary_placeholder);
        }
    }

首先判断tile对象是否设置了mSummaryOverride,是,则以此作为preference的summary;
其次再此确保此时tile保存的meta属性是最新的,通过读取"com.android.settings.summary_uri"、"com.android.settings.summary"属性,根据需要取其value作为preference的summary。

  1. 设置preference的icon,通过读取meta的属性"com.android.settings.icon_uri"、"com.android.settings.icon"的value的值;
  2. 设置preference的点击跳转界面:
        final Bundle metadata = tile.getMetaData();
        String clsName = null;
        String action = null;

        if (metadata != null) {
            clsName = metadata.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
            action = metadata.getString(META_DATA_KEY_INTENT_ACTION);
        }
        if (!TextUtils.isEmpty(clsName)) {
            pref.setFragment(clsName);
        } else {
            final Intent intent = new Intent(tile.getIntent());
            intent.putExtra(MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY,
                    sourceMetricsCategory);
            if (action != null) {
                intent.setAction(action);
            }
            pref.setOnPreferenceClickListener(preference -> {
                launchIntentOrSelectProfile(activity, tile, intent, sourceMetricsCategory);
                return true;
            });
        }

如果设置了"com.android.settings.FRAGMENT_CLASS"属性,则直接设置此value为跳转的fragment;反之,则构建intent,设置点击监听,跳转activity;

  1. 如果设置了"com.android.settings.order"属性,则根据其value值来设置preference显示前后。order为负时,绝对值越高,界面显示越靠前;order为正时,值越高,显示越靠后。

总结:

  1. 主要是通过解析tile对象内保存的meta属性去设置preference的title、key、summary、icon、跳转界面、order显示优先级;
  2. Android 10.0中设置主界面的设置项除了加载三方应用的,其余设置基本都是top_level_settings.xml定义的。故对于动态AndroidManifest中配置加载,以其它界面的设置配置项为例,示例如下:
        <activity
            android:name="Settings$DevelopmentSettingsDashboardActivity"
            android:label="@string/development_settings_title"              <!-- preference 标题 -->
            android:icon="@drawable/ic_settings_development"                <!-- preference 图标 -->
            android:parentActivityName="Settings"
            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" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <!-- 可以被Settings搜索到的action,主要逻辑在TileUtils#getTilesForAction()方法 -->
                <action android:name="com.android.settings.action.SETTINGS" />
            </intent-filter>
            <!-- preference order值,列表显示的优先级 -->
            <meta-data android:name="com.android.settings.order" android:value="-40"/>
            <!-- 类别,定义显示在哪个fragment,这里定义的值代表显示在SystemDashboardFragment中,具体看DashboardFragmentRegistry.java中map集合定义 -->
            <meta-data android:name="com.android.settings.category"
                       android:value="com.android.settings.category.ia.system" />
            <!-- preference summary -->
            <meta-data android:name="com.android.settings.summary"
                       android:resource="@string/summary_empty"/>
            <!-- preference 点击跳转fragment界面 -->
            <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>

setListening()

packages/apps/Settings/src/com/android/settings/dashboard/SummaryLoader.java
回到refreshDashboardTiles()方法,看剩余语句,设置监听,以便于后续各个preference后续可以自行根据需要更新summary。:

mSummaryLoader.setListening(true);
    /**
     * Only call from the main thread.
     */
    public void setListening(boolean listening) {
        if (mListening == listening) {
            return;
        }
        mListening = listening;
        // Unregister listeners immediately.
        for (int i = 0; i < mReceivers.size(); i++) {
            mActivity.unregisterReceiver(mReceivers.valueAt(i));
        }
        mReceivers.clear();

        mWorker.removeMessages(Worker.MSG_SET_LISTENING);
        if (!listening) {
            // Stop listen
            mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 0 /* listening */).sendToTarget();
        } else {
            // Start listen
            if (mSummaryProviderMap.isEmpty()) {
                // Category not initialized yet, init before starting to listen
                if (!mWorker.hasMessages(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING)) {
                    mWorker.sendEmptyMessage(Worker.MSG_GET_CATEGORY_TILES_AND_SET_LISTENING);
                }
            } else {
                // Category already initialized, start listening immediately
                mWorker.obtainMessage(Worker.MSG_SET_LISTENING, 1 /* listening */).sendToTarget();
            }
        }
    }
  1. 清除遗留的handle消息"MSG_SET_LISTENING";
  2. listening为false时,表面需要暂停监听;
  3. 为true时,判断是否已经初始化mSummaryProviderMap,是则直接发送"MSG_SET_LISTENING"消息,开始监听。否则发送"MSG_GET_CATEGORY_TILES_AND_SET_LISTENING"消息,初始化并开启监听;

直接看Work:

    private class Worker extends Handler {
        private static final int MSG_GET_CATEGORY_TILES_AND_SET_LISTENING = 1;
        private static final int MSG_GET_PROVIDER = 2;
        private static final int MSG_SET_LISTENING = 3;

        public Worker(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_GET_CATEGORY_TILES_AND_SET_LISTENING:
                    final DashboardCategory category =
                            mDashboardFeatureProvider.getTilesForCategory(mCategoryKey);
                    if (category == null || category.getTilesCount() == 0) {
                        return;
                    }
                    final List<Tile> tiles = category.getTiles();
                    for (Tile tile : tiles) {
                        makeProviderW(tile);
                    }
                    setListeningW(true);
                    break;
                case MSG_GET_PROVIDER:
                    Tile tile = (Tile) msg.obj;
                    makeProviderW(tile);
                    break;
                case MSG_SET_LISTENING:
                    boolean listening = msg.obj != null && msg.obj.equals(1);
                    setListeningW(listening);
                    break;
            }
        }
    }

“MSG_GET_CATEGORY_TILES_AND_SET_LISTENING”:

  1. 调用getTilesForCategory()、getTiles()方法获取可以显示在当前界面的设置项,这两个方法上面都有分析到;
makeProviderW():
  1. 遍历tiles集合,执行makeProviderW()方法,获取每个tile对象的SummaryProvider对象,填充mSummaryProviderMap集合:
    private synchronized void makeProviderW(Tile tile) {
        SummaryProvider provider = getSummaryProvider(tile);
        if (provider != null) {
            if (DEBUG) Log.d(TAG, "Creating " + tile);
            mSummaryProviderMap.put(provider, tile.getIntent().getComponent());
        }
    }

调用getSummaryProvider()方法获取SummaryProvider:

    private SummaryProvider getSummaryProvider(Tile tile) {
        if (!mActivity.getPackageName().equals(tile.getPackageName())) {
            // Not within Settings, can't load Summary directly.
            // TODO: Load summary indirectly.
            return null;
        }
        final Bundle metaData = tile.getMetaData();
        final Intent intent = tile.getIntent();
        if (metaData == null) {
            Log.d(TAG, "No metadata specified for " + intent.getComponent());
            return null;
        }
        final String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
        if (clsName == null) {
            Log.d(TAG, "No fragment specified for " + intent.getComponent());
            return null;
        }
        try {
            Class<?> cls = Class.forName(clsName);
            Field field = cls.getField(SUMMARY_PROVIDER_FACTORY);
            SummaryProviderFactory factory = (SummaryProviderFactory) field.get(null);
            return factory.createSummaryProvider(mActivity, this);
        } catch (ClassNotFoundException e) {
            if (DEBUG) Log.d(TAG, "Couldn't find " + clsName, e);
        } catch (NoSuchFieldException e) {
            if (DEBUG) Log.d(TAG, "Couldn't find " + SUMMARY_PROVIDER_FACTORY, e);
        } catch (ClassCastException e) {
            if (DEBUG) Log.d(TAG, "Couldn't cast " + SUMMARY_PROVIDER_FACTORY, e);
        } catch (IllegalAccessException e) {
            if (DEBUG) Log.d(TAG, "Couldn't get " + SUMMARY_PROVIDER_FACTORY, e);
        }
        return null;
    }

解析tile对象内保存的meta属性name为"com.android.settings.FRAGMENT_CLASS"的value值,获取class name,通过反射获取该类的"SUMMARY_PROVIDER_FACTORY"变量,调用SummaryProviderFactory接口方法createSummaryProvider(),而其方法的具体实现是在此类中,以DateTimeSettings.java类为例:
packages/apps/Settings/src/com/android/settings/DateTimeSettings.java:

    private static class SummaryProvider implements SummaryLoader.SummaryProvider {

        private final Context mContext;
        private final SummaryLoader mSummaryLoader;

        public SummaryProvider(Context context, SummaryLoader summaryLoader) {
            mContext = context;
            mSummaryLoader = summaryLoader;
        }

        @Override
        public void setListening(boolean listening) {
            if (listening) {
                final Calendar now = Calendar.getInstance();
                mSummaryLoader.setSummary(this, ZoneGetter.getTimeZoneOffsetAndName(mContext,
                        now.getTimeZone(), now.getTime()));
            }
        }
    }

    public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
            = new SummaryLoader.SummaryProviderFactory() {
        @Override
        public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
                SummaryLoader summaryLoader) {
            return new SummaryProvider(activity, summaryLoader);
        }
    };

故getSummaryProvider()方法实质就是返回tile对象相对应的类内定义的SummaryProvider的对象,而此mSummaryProviderMap内保存的就是所有AndroidManifest.xml中定义的可在界面显示的设置项的类内定义的SummaryProvider的对象。

setListeningW():
  1. 调用setListeningW方法,开始监听:
    private synchronized void setListeningW(boolean listening) {
        if (mWorkerListening == listening) {
            return;
        }
        mWorkerListening = listening;
        if (DEBUG) {
            Log.d(TAG, "Listening " + listening);
        }
        for (SummaryProvider p : mSummaryProviderMap.keySet()) {
            try {
                p.setListening(listening);
            } catch (Exception e) {
                Log.d(TAG, "Problem in setListening", e);
            }
        }
    }

可以看到所谓的开始"监听",也只不过是遍历所有的SummaryProvider对象,调用其setListening()方法;

MSG_GET_PROVIDER:

MSG_SET_LISTENING:

这两个消息实质也是分别调用makeProviderW()、setListeningW()方法;

总结:

遍历当前通过AndroidManifest.xml配置的可以显示的设置项的fragment,判断其内是否定义变量"SUMMARY_PROVIDER_FACTORY",调用接口方法setListening()方法,实现设置项preference的summary的更新。

总结:

  1. refreshDashboardTiles()主要是动态的通过PM从AndroidManifest.xml中读取相关配置来加载可以显示的设置item;
  2. getTilesForCategory();通过PM去检索AndroidManifest.xml中符合相关action的可以显示在当前fragment上的设置项;
  3. bindPreferenceToTile();解析AndroidManifest.xml中配置的meta属性来对设置项preference进行数据绑定;
  4. setListening();预留接口,方便各个设置项本身可以通过此来更新preference的summary;

本文地址:https://blog.csdn.net/Otaku_627/article/details/108746641

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网