android10.0(Q) Settings 動態添加設置項實現原理實戰

爲什麼要這樣做?

上一篇通過靜態方式添加配置項,應用場景太侷限。

所以繼續研究加載原理,終於發現了動態加載的奧祕。

效果圖

tsKNlj.gif trfk1U.png

文件清單

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

trfMh6.png

問題日誌如下,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

添加自定義條目了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章