Android P Settings分析

作者: 徒步青云 | 来源:发表于2019-05-08 09:44 被阅读670次

    一、背景

    公司安排我对Android P Settings的源码进行修改,屏蔽掉不需要的设置选项,添加我们产品所特有的设置选项。

    二、准备工作

    工欲善其事必先利其器,准备工作是不能缺少的,而一个好的IDE能帮你专注本职工作。这里我推荐使用Android Studio,当然,eclipse这种上个世纪的IDE也可以。

    1、获得源码

    首先,想方设法从你Android P源码里copy出Settings和settingslib这两部分源码,路径分别是platform/packages/apps/Settings和frameworks/base/packages/SettingsLib。

    2、创建项目

    然后用Android Studio新建个一个项目名为Settings,包名为com.android.settings的项目,再删除app/src/main中AndroidManifest.xml文件、res目录、以及java目录下的子目录(java目录保留),然后将你Settings的相应目录及文件copy进来。

    3、新建Module

    File->New->New Module->Android Library,创建一个名为settingslib,包名为:com.android.settingslib的module。后面操作与Settings操作类似,删除自动生成的目录,再将settingslib的相应文件及目录copy进去。

    4、添加依赖,并设置用java1.8编译。

    如图所示:


    image.png
    image.png

    完成

    三、总览流程

    Android 9的Settings与Android 5相比,在架构上复杂了很多,其中变化最大的是一级设置列表,
    category解析流程


    图片.png

    四、分析

    第一步:分析AndroidManifest文件,可以看到Settings.java是应用的入口类。


    image.png

    打开Settings.java,里面是大量的子Activity,他们与Settings类似,都继承于SettingsActivity

    image.png

    打开SettingsActivity,它继承于SettingsDrawerActivity,而SettingsDrawerActivity内容不多,主要功能是
    1、注册对应用安装、卸载、更新的事件广播,同时更新一级设置列表。


    image.png

    2、重写setContentView,让子类调用的setContentView时,将子类布局填充到自己content_frame中。


    image.png

    3、当接受到广播时,调用CategoriesUpdateTask,异步更新sTileBlacklist列表,并回调监听事件。


    image.png

    我们再回到SettingsActivity,从onCreate开始分析

        @Override
        protected void onCreate(Bundle savedState) {
            super.onCreate(savedState);
            Log.d(LOG_TAG, "Starting onCreate");
            long startTime = System.currentTimeMillis();
    
            //获得FeatureFactoryImpl
            final FeatureFactory factory = FeatureFactory.getFactory(this);
    
            //获得DashboardFeatureProviderImpl
            mDashboardFeatureProvider = factory.getDashboardFeatureProvider(this);
    
            // Should happen before any call to getIntent()
            //将子类在Manifest中com.android.settings.FRAGMENT_CLASS的值赋值给mFragmentClass
            getMetaData();
    
            //注意:这里getIntent()是自己重写过的方法,并不是Activity自带的getIntent()
            final Intent intent = getIntent();
            if (intent.hasExtra(EXTRA_UI_OPTIONS)) {
                getWindow().setUiOptions(intent.getIntExtra(EXTRA_UI_OPTIONS, 0));
            }
    
            // Getting Intent properties can only be done after the super.onCreate(...)
            //:settings:show_fragment
            final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
    
            final ComponentName cn = intent.getComponent();
            final String className = cn.getClassName();//子类名
    
            //如果是Settings界面,即主界面
            mIsShowingDashboard = className.equals(Settings.class.getName());
    
            // This is a "Sub Settings" when:
            // - this is a real SubSettings
            // - or :settings:show_fragment_as_subsetting is passed to the Intent
            final boolean isSubSettings = this instanceof SubSettings ||
                    intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING, false);
    
            // If this is a sub settings, then apply the SubSettings Theme for the ActionBar content
            // insets
            if (isSubSettings) {
                setTheme(R.style.Theme_SubSettings);
            }
    
            setContentView(mIsShowingDashboard ?
                    R.layout.settings_main_dashboard : R.layout.settings_main_prefs);
    
            //内容区域View
            mContent = findViewById(R.id.main_content);
    
            getFragmentManager().addOnBackStackChangedListener(this);
    
            if (savedState != null) {
                // We are restarting from a previous saved state; used that to initialize, instead
                // of starting fresh.
                setTitleFromIntent(intent);
    
                ArrayList<DashboardCategory> categories =
                        savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
                if (categories != null) {
                    mCategories.clear();
                    mCategories.addAll(categories);
                    setTitleFromBackStack();
                }
            } else {
                //运行fragment
                launchSettingFragment(initialFragmentName, isSubSettings, intent);
            }
    
    //UI相关操作,略
    

    最后几句从名字可以看出,通过调用launchSettingFragment来显示具体的界面,而第一个参数initialFragmentName,是通过getIntent().getStringExtra(EXTRA_SHOW_FRAGMENT)获得,这里不经要问:EXTRA_SHOW_FRAGMENT的值从何而来?不可能每次都在startActivity中添加吧,这里,有一个巨大的坑:getIntent(),它被重写了,以至于我刚开始看这里还有点懵逼。下面就是getIntent()的源码:

        @Override
        public Intent getIntent() {
            Intent superIntent = super.getIntent();
            String startingFragment = getStartingFragmentClass(superIntent);//获得子类名
            // This is called from super.onCreate, isMultiPane() is not yet reliable
            // Do not use onIsHidingHeaders either, which relies itself on this method
            if (startingFragment != null) {
                Intent modIntent = new Intent(superIntent);
                modIntent.putExtra(EXTRA_SHOW_FRAGMENT, startingFragment);
                Bundle args = superIntent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                if (args != null) {
                    args = new Bundle(args);
                } else {
                    args = new Bundle();
                }
                args.putParcelable("intent", superIntent);
                modIntent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args);
                return modIntent;
            }
            return superIntent;
        }
    
        /**
         * Checks if the component name in the intent is different from the Settings class and
         * returns the class name to load as a fragment.
         */
        private String getStartingFragmentClass(Intent intent) {
            if (mFragmentClass != null) return mFragmentClass;
    
            //其实就是获取子类名称
            String intentClass = intent.getComponent().getClassName();
            if (intentClass.equals(getClass().getName())) return null;//如果是当前父类名
    
            if ("com.android.settings.RunningServices".equals(intentClass)
                    || "com.android.settings.applications.StorageUse".equals(intentClass)) {
                // Old names of manage apps.
                intentClass = ManageApplications.class.getName();
            }
    
            return intentClass;
        }
    

    上面代码其实说白了,就是对super.getIntent()得到的intent进行封装,并将要跳转的Activity值赋值给EXTRA_SHOW_FRAGMENT。所以,onCreate中initialFragmentName值,其实就是每次跳转的目标Activity类名(是SettingsActivity的子类)。

    接下来我们开始分析launchSettingFragment函数了。

        @VisibleForTesting
        void launchSettingFragment(String initialFragmentName, boolean isSubSettings, Intent intent) {
            if (!mIsShowingDashboard && initialFragmentName != null) {//不是根界面,且指定了要显示的Fragment
                setTitleFromIntent(intent);
    
                Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
                switchToFragment(initialFragmentName, initialArguments, true, false,
                        mInitialTitleResId, mInitialTitle, false);
            } else {//是跟界面,或者没指定要显示的Fragment
                // Show search icon as up affordance if we are displaying the main Dashboard
                mInitialTitleResId = R.string.dashboard_title;
    
                switchToFragment(DashboardSummary.class.getName(), null /* args */, false, false,
                        mInitialTitleResId, mInitialTitle, false);
            }
        }
    

    这里对两种情况进行分开处理,如果是根界面,就显示DashboardSummary,否则显示指定的Fragment。他们都通过switchToFragment进行实现。

        /**
         * Switch to a specific Fragment with taking care of validation, Title and BackStack
         */
        private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
                boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
            Log.d(LOG_TAG, "Switching to fragment " + fragmentName);
            if (validate && !isValidFragment(fragmentName)) {//验证所需显示的fragmentName是否在SettingsGateway里注册
                throw new IllegalArgumentException("Invalid fragment for this activity: "
                        + fragmentName);
            }
            Fragment f = Fragment.instantiate(this, fragmentName, args);
            FragmentTransaction transaction = getFragmentManager().beginTransaction();
            transaction.replace(R.id.main_content, f);
            if (withTransition) {
                TransitionManager.beginDelayedTransition(mContent);
            }
            if (addToBackStack) {
                transaction.addToBackStack(SettingsActivity.BACK_STACK_PREFS);
            }
            if (titleResId > 0) {
                transaction.setBreadCrumbTitle(titleResId);
            } else if (title != null) {
                transaction.setBreadCrumbTitle(title);
            }
            transaction.commitAllowingStateLoss();
            getFragmentManager().executePendingTransactions();
            Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
            return f;
        }
    

    switchToFragment就简简单单将fragment填充到main_content中。

    到目前为止,我们已经将大概的布局填充过程介绍完毕,现在我们开始以DashboardSummary为例(即一级设置列表),分析其数据是如何获得的。首先看onCreateView方法:

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
            //、、、、、、
            //前面只是对布局中的RecyclerView设置参数等等,略掉
            mAdapter = new DashboardAdapter(getContext(), bundle,
                    mConditionManager.getConditions(), mSuggestionControllerMixin, getLifecycle());
            mDashboard.setAdapter(mAdapter);
            mSummaryLoader.setSummaryConsumer(mAdapter);
            ActionBarShadowController.attachToRecyclerView(
                    getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
            rebuildUI();//更新列表
            //、、、、、、
            return root;
        }
    
        @VisibleForTesting
        void rebuildUI() {
            ThreadUtils.postOnBackgroundThread(() -> updateCategory());
        }
    
        @WorkerThread
        void updateCategory() {
            final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
                    CategoryKey.CATEGORY_HOMEPAGE);
          //......................后面设置category...............
        }
    

    通过mDashboardFeatureProvider.getTilesForCategory()方法获得Category,其参数CATEGORY_HOMEPAGE,实际是com.android.settings.category.ia.homepage,这里可以看出,一级设置类别,是通过查找AndroidManifest文件里com.android.settings.category.ia.homepage属性获得。
    而getTilesForCategory内部通过CategoryManager的getTilesByCategory方法实现。

    public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) {
            return getTilesByCategory(context, categoryKey, TileUtils.SETTING_PKG);
        }
    
        public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey,
                String settingPkg) {
            tryInitCategories(context, settingPkg);
            //由此可以看出,上一行代码是获取Categories,并保存到mCategoryByKeyMap中
            return mCategoryByKeyMap.get(categoryKey);
        }
    
        private synchronized void tryInitCategories(Context context, String settingPkg) {
            // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange
            // happens.
            tryInitCategories(context, false /* forceClearCache */, settingPkg);
        }
    
        private synchronized void tryInitCategories(Context context, boolean forceClearCache,
                String settingPkg) {
            if (mCategories == null) {
                if (forceClearCache) {
                    mTileByComponentCache.clear();
                }
                mCategoryByKeyMap.clear();
    //这里正式获得Categories
                mCategories = TileUtils.getCategories(context, mTileByComponentCache,
                        false /* categoryDefinedInManifest */, mExtraAction, settingPkg);
                for (DashboardCategory category : mCategories) {
                    mCategoryByKeyMap.put(category.key, category);
                }
                backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap);
                sortCategories(context, mCategoryByKeyMap);
                filterDuplicateTiles(mCategoryByKeyMap);
            }
        }
    

    这里可以发现整个方法,是通过TileUtils.getCategories获得Categorie。

        public static List<DashboardCategory> getCategories(Context context,
                Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest,
                String extraAction, String settingPkg) {
            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.
                    //查找 com.android.settings.action.SETTINGS
                    getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true,
                            settingPkg);
                    getTilesForAction(context, user, OPERATOR_SETTINGS, cache,
                            OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
                    getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
                            MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg);
                }
                if (setup) {
                    getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false,
                            settingPkg);
                    if (!categoryDefinedInManifest) {
                        getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false,
                                settingPkg);
                        if (extraAction != null) {
                            getTilesForAction(context, user, extraAction, cache, null, tiles, false,
                                    settingPkg);
                        }
                    }
                }
            }
    
            HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
            for (Tile tile : tiles) {
                DashboardCategory category = categoryMap.get(tile.category);
                if (category == null) {
                    category = createCategory(context, tile.category, categoryDefinedInManifest);
                    if (category == null) {
                        Log.w(LOG_TAG, "Couldn't find category " + tile.category);
                        continue;
                    }
                    categoryMap.put(category.key, category);
                }
                category.addTile(tile);
            }
            ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
            for (DashboardCategory category : categories) {
                category.sortTiles();
            }
            Collections.sort(categories, CATEGORY_COMPARATOR);
            if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took "
                    + (System.currentTimeMillis() - startTime) + " ms");
            return categories;
        }
    

    五、一级设置列表小结

    到此为止,我们可以清楚的知道一级菜单是如何解析出来的,首先,TileUtils对AndroidManifest文件进行解析,获取所有包含com.android.settings.action.SETTINGS的Activity,然后在DashboardSummary类中,通过DashboardFeatureProvider的getTilesForCategory方法,获得所有包含com.android.settings.category.ia.homepage的类,再将其填充到列表。

    这里我们有其它一些细节没有进行介绍:
    1、所有继承于SettingsActivity并需要在一级设置列表显示的Activity,都需要在SettingsGateway的SETTINGS_FOR_RESTRICTED列表中声明
    2、所有需要在SettingsActivity或其子Activity中设置的Fragment,都必须要在SettingsGateway的ENTRY_FRAGMENTS列表中声明,否则会抛出 Invalid fragment for this activity:xxxx异常

    六、二级设置列表分析

    这里内容比较简单,但内容也是最多的,其主要内容包括以下几种情况:
    1、二级设置是列表
    2、二级设置是界面
    3、三级设置是界面
    情况1、也是看到最多的情况,比如**网络和互联网界面

    相关文章

      网友评论

        本文标题:Android P Settings分析

        本文链接:https://www.haomeiwen.com/subject/xkfgwqtx.html