为什么要这样做?
上一篇通过静态方式添加配置项,应用场景太局限。
所以继续研究加载原理,终于发现了动态加载的奥秘。
效果图
文件清单
frameworks\base\packages\SettingsLib\Tile\src\com\android\settingslib\drawer\TileUtils.java vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java
实现过程
去除 TileUtils 中是否系统App判断逻辑,注释 getTilesForAction() 中 resolved.system 判断
frameworks\base\packages\SettingsLib\Tile\src\com\android\settingslib\drawer\TileUtils.java
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. Log.w(LOG_TAG, "not allow app " + resolved.activityInfo.name); //cczheng annotaion for 3rd app can add setting preference //continue; } Log.w(LOG_TAG, "resolved.targetUserId="+resolved.targetUserId); ActivityInfo activityInfo = resolved.activityInfo; Bundle metaData = activityInfo.metaData; String categoryKey = defaultCategory;
系统修改就已经搞定了,我去,这也太简单了吧,不是,你骗我的吧。大兄弟真的搞定了,只要注释 continue; 就行了
接下来就可以为所欲为的添加配置项了。客户只需要在自己 app 的 AndroidManifest.xml 中配置属性给要跳转的Activity即可
<activity android:name=".activity.SettingPreferenceActivity"> <intent-filter > <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.order" android:value="-150" /> <meta-data android:name="com.android.settings.icon" android:resource="@mipmap/ic_icon" /> <meta-data android:name="com.android.settings.summary" android:resource="@string/title_activity_settings" /> </activity>
解释下各个属性意义
com.android.settings.action.EXTRA_SETTINGS 设置遍历所有应用解析标记
com.android.settings.category.ia.homepage 在设置主界面显示
com.android.settings.order 设置主界面排序,网络和互联网默认-120,只要大于即可排到第一
com.android.settings.icon 显示图标
com.android.settings.summary 显示子标题文字
遇到的问题解决
当动态添加设置项对应app卸载后,再次进入设置页面,会看到如下bug
问题日志如下,app卸载后由于设置应用没有重新初始化,缓存了刚刚的状态,加载设置项对应icon找不到资源,就出现上述bug,
当你把设置强行停止再进入发现bug消失了,但总不能要求客户也这么操作吧。
2020-06-05 16:39:56.964 6059-6059/com.android.settings D/AdaptiveHomepageIcon: Setting background color -15043608 2020-06-05 16:39:56.965 6059-6059/com.android.settings I/TopLevelSettings: key dashboard_tile_pref_com.cczheng.androiddemo.activity.SettingPreferenceActivity 2020-06-05 16:39:56.965 6059-6059/com.android.settings D/TopLevelSettings: tile null 2020-06-05 16:39:56.966 6059-6059/com.android.settings D/Tile: Can't find package, probably uninstalled. 2020-06-05 16:39:56.966 6059-6059/com.android.settings W/ziparchive: Unable to open '/data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk': No such file or directory 2020-06-05 16:39:56.967 6059-6059/com.android.settings E/ndroid.setting: Failed to open APK '/data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk' I/O error 2020-06-05 16:39:56.967 6059-6059/com.android.settings E/ResourcesManager: failed to add asset path /data/app/com.cczheng.androiddemo-tcKDlXiPvEgQLoVFL4Pd3g==/base.apk 2020-06-05 16:39:56.967 6059-6059/com.android.settings W/PackageManager: Failure retrieving resources for com.cczheng.androiddemo 2020-06-05 16:39:56.968 6059-6059/com.android.settings D/Tile: Can't find package, probably uninstalled. 2020-06-05 16:39:56.970 6059-6059/com.android.settings D/Tile: Couldn't find info android.content.pm.PackageManager$NameNotFoundException: com.cczheng.androiddemo at android.app.ApplicationPackageManager.getApplicationInfoAsUser(ApplicationPackageManager.java:414) at android.app.ApplicationPackageManager.getApplicationInfo(ApplicationPackageManager.java:395) at android.app.ApplicationPackageManager.getResourcesForApplication(ApplicationPackageManager.java:1545) at com.android.settingslib.drawer.Tile.getSummary(Tile.java:220) at com.android.settings.dashboard.DashboardFeatureProviderImpl.bindSummary(DashboardFeatureProviderImpl.java:172) at com.android.settings.dashboard.DashboardFeatureProviderImpl.bindPreferenceToTile(DashboardFeatureProviderImpl.java:117) at com.android.settings.dashboard.DashboardFragment.refreshDashboardTiles(DashboardFragment.java:511) at com.android.settings.dashboard.DashboardFragment.refreshAllPreferences(DashboardFragment.java:394) at com.android.settings.dashboard.DashboardFragment.onCreatePreferences(DashboardFragment.java:170) at androidx.preference.PreferenceFragmentCompat.onCreate(PreferenceFragmentCompat.java:160) at com.android.settingslib.core.lifecycle.ObservablePreferenceFragment.onCreate(ObservablePreferenceFragment.java:61)
解决办法
依旧是通过检测APP是否已经卸载来决定是否加载对应配置项,依旧是在 DashboardFragment 中
vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java
void refreshDashboardTiles(final String TAG) { ...... // 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; } Log.i(TAG, "key " + key); Log.d(TAG, "tile " + tile.getKey(getContext())); //cczheng add for fix app uninstall show bug if (!checkTilePackage(tile.getPackageName())) { Log.d(TAG, "Can't find package, probably uninstalled don't load"); continue; }//E check Can't find package, probably uninstalled. if (!displayTile(tile)) { continue; } ...... } private boolean checkTilePackage(String packageName){ try { android.content.pm.PackageManager pm = getContext().getPackageManager(); pm.getApplicationInfo(packageName, android.content.pm.PackageManager.GET_UNINSTALLED_PACKAGES); android.util.Log.e("DashboardAdapter", packageName + " app exists show voip dashboard"); return true; }catch (Exception e){ android.util.Log.e("DashboardAdapter", packageName + " app don't exists"); return false; } }
好了,至此需求已经搞定了。如果你想知道为啥这样改,请继续往下看。
原理分析
从启动开始说起
进入setting的AndroidManifest.xml里看一看,找启动Activity
<!-- Alias for launcher activity only, as this belongs to each profile. --> <activity-alias android:name="Settings" android:label="@string/settings_label_launcher" 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>
发现启动Activity是Settings,但是前面的标签是activity-alias,所以这是另一个Activity的别名,然后它真实的启动Activity应该是targetActivity所标注的SettingsHomepageActivity。
走进SettingsHomepageActivity.java
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); final AvatarViewMixin avatarViewMixin = new AvatarViewMixin(this, avatarView); getLifecycle().addObserver(avatarViewMixin); if (!getSystemService(ActivityManager.class).isLowRamDevice()) { // Only allow contextual feature on high ram devices. showFragment(new ContextualCardsFragment(), R.id.contextual_cards_content); } showFragment(new TopLevelSettings(), R.id.main_content); ((FrameLayout) findViewById(R.id.main_content)) .getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING); } private void showFragment(Fragment fragment, int id) { final FragmentManager fragmentManager = getSupportFragmentManager(); final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); final Fragment showFragment = fragmentManager.findFragmentById(id); if (showFragment == null) { fragmentTransaction.add(id, fragment); } else { fragmentTransaction.show(showFragment); } fragmentTransaction.commit(); } @VisibleForTesting void setHomepageContainerPaddingTop() { final View view = this.findViewById(R.id.homepage_container); final int searchBarHeight = getResources().getDimensionPixelSize(R.dimen.search_bar_height); final int searchBarMargin = getResources().getDimensionPixelSize(R.dimen.search_bar_margin); // The top padding is the height of action bar(48dp) + top/bottom margins(16dp) final int paddingTop = searchBarHeight + searchBarMargin * 2; view.setPadding(0 /* left */, paddingTop, 0 /* right */, 0 /* bottom */); } }
代码不多,布局文件对应 settings_homepage_container.xml, 布局加载完成后增加顶部padding为了给SearchActionBar预留空间,
如果不需要SeacherActionBar直接将这部分代码注释即可。接下来看到新创建 TopLevelSettings 填充 main_content,主角登场啦。
TopLevelSettings 就是我们看到的Settings主界面。
进入TopLevelSettings
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; }
top_level_settings.xml
vendor\mediatek\proprietary\packages\apps\MtkSettings\res\xml\top_level_settings.xml
<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"/>
可以看到主界面对应布局 top_level_settings.xml中都是一个个Preference,也就对应了主页面每一个条目,可以看到
xml 中 Preference数目和主界面显示数目是不对等了,为啥呢?因为存在动态添加的,查阅 TopLevelSettings 代码发现没啥
特殊而且代码量很少,看到 TopLevelSettings 继承 DashboardFragment,点进去看看
DashboardFragment.java
vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragment.java
@Override public void onAttach(Context context) { super.onAttach(context); mSuppressInjectedTileKeys = Arrays.asList(context.getResources().getStringArray( R.array.config_suppress_injected_tile_keys)); mDashboardFeatureProvider = FeatureFactory.getFactory(context). getDashboardFeatureProvider(context); final List<AbstractPreferenceController> controllers = new ArrayList<>(); // Load preference controllers from code final List<AbstractPreferenceController> controllersFromCode = createPreferenceControllers(context); // Load preference controllers from xml definition final List<BasePreferenceController> controllersFromXml = PreferenceControllerListHelper .getPreferenceControllersFromXml(context, getPreferenceScreenResId()); // Filter xml-based controllers in case a similar controller is created from code already. final List<BasePreferenceController> uniqueControllerFromXml = PreferenceControllerListHelper.filterControllers( controllersFromXml, controllersFromCode); // Add unique controllers to list. if (controllersFromCode != null) { controllers.addAll(controllersFromCode); } controllers.addAll(uniqueControllerFromXml); }
注释已经写得很清楚了,分别从java代码和xml中加载PreferenceController,然后过滤去重最终加载页面显示。
java代码加载
createPreferenceControllers() return null,而且子类TopLevelSettings并未覆盖实现,所以 controllersFromCode 为 null
xml加载
getPreferenceControllersFromXml(context, getPreferenceScreenResId()), getPreferenceScreenResId对应刚刚的 top_level_settings
具体的遍历解析xml文件代码就不看了,可以自行跟进去查看
controllers 集合获取完成,那么这个 Controller 究竟有什么用呢?
看Settings中的Preference你会发信几乎每个都对应一个 settings:controller 属性,xml中若没有那么也会在java代码中对应增加
Controller 可以用来处理 Preference的显示和点击。扯远了回到主题,继续寻找和动态增加相关线索
@Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { refreshAllPreferences(getLogTag()); } 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); }
嗯,这下有点意思了,refreshAllPreferences() 一上来移除所有的Preference,通过 displayResourceTiles()
加载指定xml中的所有Preference
private void displayResourceTiles() { final int resId = getPreferenceScreenResId(); if (resId <= 0) { return; } addPreferencesFromResource(resId); final PreferenceScreen screen = getPreferenceScreen(); screen.setOnExpandButtonClickListener(this); mPreferenceControllers.values().stream().flatMap(Collection::stream).forEach( controller -> controller.displayPreference(screen)); }
好像也不是我们要找的,再往下看 refreshDashboardTiles(TAG);
void refreshDashboardTiles(final String TAG) { final PreferenceScreen screen = getPreferenceScreen(); final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(getCategoryKey()); Log.e(TAG, "refreshDashboardTiles key="+ 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; } Log.e(TAG, "tile list size="+tiles.size()); // 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; } Log.i(TAG, "key " + key); Log.d(TAG, "tile " + tile.getKey(getContext())); 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) { Log.d(TAG, "remove tiles that are gone " + key); mDashboardTilePrefKeys.remove(key); final Preference preference = screen.findPreference(key); if (preference != null) { screen.removePreference(preference); } } mSummaryLoader.setListening(true); }
哈哈哈,终于找到奥秘所在了,因为这个方法中有调用 addPreference(),页面中要想增加Preference条目必须调用此方法,
接下来逐行来看这个方法都干什么了?
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(getCategoryKey());
来看传递参数 getCategoryKey()
public String getCategoryKey() { return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName()); }
getClass().getName() 获取当前调用类名,我们从 TopLevelSettings 中进来的,那自然是它
DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP 是静态MAP集合,看下初始赋值
vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\dashboard\DashboardFragmentRegistry.java
public static final Map<String, String> 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); PARENT_TO_CATEGORY_KEY_MAP.put(ConnectedDeviceDashboardFragment.class.getName(), CategoryKey.CATEGORY_CONNECT); PARENT_TO_CATEGORY_KEY_MAP.put(AdvancedConnectedDeviceDashboardFragment.class.getName(), CategoryKey.CATEGORY_DEVICE); ...
看到 TopLevelSettings 对应 String 为 CategoryKey.CATEGORY_HOMEPAGE,也就是 getCategoryKey() 返回值
为 com.android.settings.category.ia.homepage
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";
进入 getTilesForCategory() 中获取 DashboardCategory
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); }
可以看到从 mCategoryByKeyMap 中获取 key为com.android.settings.category.ia.homepage 对应 DashboardCategory
mCategoryByKeyMap 赋值在 tryInitCategories() 中
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) { android.util.Log.i("settingslib", "category.key="+category.key); mCategoryByKeyMap.put(category.key, category); } backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); sortCategories(context, mCategoryByKeyMap); filterDuplicateTiles(mCategoryByKeyMap); } }
获取 category 集合,编译集合依次往map中添加,继续跟 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(); 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. loge("getIdentifier="+user.getIdentifier()); loge("getCurrentUser="+ActivityManager.getCurrentUser());// 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; }
可以看到开始创建空集合 tiles,通过调用getTilesForAction() 进行赋值。赋值后遍历 tiles,获取
tile 中 DashboardCategory,判断 categoryMap 中是否包含,不包含则往里添加。最终创建 ArrayList categories,
并赋值 categoryMap.values(),进行排序后 return categories
核心还是在 tiles 赋值,再来看 getTilesForAction()
static void getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, boolean requireSettings) { loge("action="+action); 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. Log.w(LOG_TAG, "not allow app " + resolved.activityInfo.name); continue; } Log.w(LOG_TAG, "resolved.targetUserId="+resolved.targetUserId); 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)); loge("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); } loge("tile key="+tile.getPackageName()); } }
通过 PackageManager 查询系统中所有带指定 Action 的 Intent 对应信息 ResolveInfo 集合,然后遍历该集合
获取符合条件应用信息包名、类名、icon等构造 tile,最终添加进 outTiles 中。
可以看到循环一开始就有硬性判断,if (!resolved.system)
// Do not allow any app to add to settings, only system ones.
必须是系统应用才能向Setting主界面中添加配置项,这显然不是我们希望的,我们既然是开放给客户的,自然不需要这个判断
注释 continue 即可。
上面说到必须是指定action,才能被 PackageManager 搜索到,来看下都有哪些Action
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"; 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 EXTRA_CATEGORY_KEY = "com.android.settings.category"; public static final String META_DATA_KEY_ORDER = "com.android.settings.order"; 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); getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false); getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
可以看到我们上面指定的就是 com.android.settings.action.EXTRA_SETTINGS,google 的 GMSCore app 采用的是
com.android.settings.action.IA_SETTINGS
配置了指定Action后,还需要配置 meta-data 节点,别忘记了 Settings 中匹配 category 通过 key=com.android.settings.category.ia.homepage
// 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)); loge("Found " + resolved.activityInfo.name + " for intent " + intent + " missing metadata " + (metaData == null ? "" : EXTRA_CATEGORY_KEY)); continue; } else { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); }
所以要增加 meta-data 才能显示在主页中
<meta-data android:name="com.android.settings.category" android:value="com.android.settings.category.ia.homepage" />
通过Tile构造函数发现还有其它可选 meta-data 配置,
com.android.settings.order 对应Preference排序
com.android.settings.icon 对应Preference图标
com.android.settings.summary 对应Preference子标题
所以最终xml中配置为
<activity android:name=".activity.SettingPreferenceActivity"> <intent-filter > <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.order" android:value="-150" /> <meta-data android:name="com.android.settings.icon" android:resource="@mipmap/ic_icon" /> <meta-data android:name="com.android.settings.summary" android:resource="@string/title_activity_settings" /> </activity>
嗯,数据加载搞清了,现在我们回到 DashboardFragment 中的 refreshDashboardTiles()
如果未遍历到key=com.android.settings.category.ia.homepage 对应 DashboardCategory 则直接 return,
无需刷新,比如当进入二级页面时,key将不再是com.android.settings.category.ia.homepage,若 category 中
tile集合为空也直接return,因为没有需要添加的条目。接下来就是遍历 tiles 通过 PreferenceScreen.addPreference
添加自定义条目了。