一、背景
公司安排我对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、也是看到最多的情况,比如**网络和互联网界面
网友评论