爲什麼要這樣做?
上一篇通過靜態方式添加配置項,應用場景太侷限。
所以繼續研究加載原理,終於發現了動態加載的奧祕。
效果圖
文件清單
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());//pjz
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
添加自定義條目了。