0%

Android 7.0 Settings 菜单加载过程

Android 7.0 Settings 首页菜单项与之前版本的加载方法不同,这里带大家简单分析一下…

Settings 首页菜单项加载过程

1. 入口类

AndroidManifest.xml中可以看到Settings.java为入口类

1
2
3
4
5
6
7
8
9
10
11
12
/// file: packages/apps/Settings/src/com/android/settings/Settings.java

public class Settings extends SettingsActivity {

/*
* Settings subclasses for launching independently.
*/
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }
...
public static class WifiGprsSelectorActivity extends SettingsActivity { /* empty */ }
}

可以看到Settings中定义了很多类,而没有具体的实现,Settings类只是一个管理类,定义设置中的Activity,具体实现类是在其父类SettingsActivity 中。

2. 选择布局、切换fragment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// file: packages/apps/Settings/src/com/android/settings/SettingsActivity.java

@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);

// 获取intent
final Intent intent = getIntent();

final ComponentName cn = intent.getComponent();
final String className = cn.getClassName();

// 第一次通过Launcher点击进入,这里calssName为Settings
mIsShowingDashboard = className.equals(Settings.class.getName())
|| className.equals(Settings.WirelessSettings.class.getName())
|| className.equals(Settings.DeviceSettings.class.getName())
|| className.equals(Settings.PersonalSettings.class.getName())
|| className.equals(Settings.WirelessSettings.class.getName());

// 加载settings_main_dashboard 布局
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard : R.layout.settings_main_prefs);

mContent = (ViewGroup) findViewById(mMainContentId);

// 第一次进来,savedState == null
if (savedState != null) {

} else {
if (!mIsShowingDashboard) {

} else {
// 切换到 DashboardSummary 这个fragment
switchToFragment(DashboardSummary.class.getName(), null, false, false,
mInitialTitleResId, mInitialTitle, false);
}
}
}

如上是 SettingsActivity 的 onCreate() 方法, 只保留界面加载相关的逻辑代码.

1
2
3
4
5
6
7
8
<!-- file: packages/apps/Settings/res/layout/settings_main_dashboard.xml --> 

<!-- 布局就只有一个控件。所以界面元素是在DashboardSummary中加载的 -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_content"
android:layout_height="match_parent"
android:layout_width="match_parent"
/>

3. 加载设置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// file: packages/apps/Settings/src/com/android/settings/dashboard/DashboardSummary.java

private void rebuildUI() {
if (!isAdded()) {
Log.w(TAG, "Cannot build the DashboardSummary UI yet as the Fragment is not added");
return;
}

// 这里通过调用"getDashboardCategories()" 加载categories
List<DashboardCategory> categories =
((SettingsActivity) getActivity()).getDashboardCategories();

// 将adapter中的数据更新并刷新显示
mAdapter.setCategories(categories);

// recheck to see if any suggestions have been changed.
new SuggestionLoader().execute();
}

getDashboardCategories() 方法在SettingsActivity 的父类 SettingsDrawerActivity 中, 需要注意 SettingsDrawerActivity 在 frameworks/base/packages/SettingsLib/ 模块中。

1
2
3
4
5
6
7
8
9
10
/// file: frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java

public List<DashboardCategory> getDashboardCategories() {
if (sDashboardCategories == null) {
...
// 关键代码
sDashboardCategories = TileUtils.getCategories(this, sTileCache);
}
return sDashboardCategories;
}

getDashboardCategories() 中关键代码如上面,调用TileUtils.getCategories()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/// file: frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java

// 用于查询的action
private static final String SETTINGS_ACTION =
"com.android.settings.action.SETTINGS";
private static final String OPERATOR_SETTINGS =
"com.android.settings.OPERATOR_APPLICATION_SETTING";

public static List<DashboardCategory> getCategories(Context context,
HashMap<Pair<String, String>, Tile> cache) {
// 是否已经初始化过
boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0)
!= 0;
ArrayList<Tile> tiles = new ArrayList<>();
UserManager userManager = UserManager.get(context);
for (UserHandle user : userManager.getUserProfiles()) {
// TODO: Needs much optimization, too many PM queries going on here.
if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
// 根据action获取设置菜单
// 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);
...
}

if (setup) {
getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
}
}
HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
for (Tile tile : tiles) {
DashboardCategory category = categoryMap.get(tile.category);
// 当前tile所属的category如果不存在,就创建一个并添加到categoryMap中
if (category == null) {
category = createCategory(context, tile.category);
if (category == null) {
Log.w(LOG_TAG, "Couldn't find category " + tile.category);
continue;
}
categoryMap.put(category.key, category);
}
// 将tile添加到对应的category中
category.addTile(tile);
}
ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
for (DashboardCategory category : categories) {
// 对category中的tiles排序
Collections.sort(category.tiles, TILE_COMPARATOR);
}
// 对categories 排序
Collections.sort(categories, CATEGORY_COMPARATOR);
return categories;
}

上面获取菜单项的重点在于”getTilesForAction()”方法,这是方法的功能核心功能是查询包含指定action的Activities, 最后一个参数”requireSettings” 决定是否只匹配 “com.android.settings” 包中的activities.

如下是Settings 清单中一个典型的设置菜单Activity定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- file: packages/apps/Settings/AndroidManifest.xml -->

<activity android:name="Settings$SoundSettingsActivity"
android:label="@string/sound_settings"
android:icon="@drawable/ic_settings_sound"
android:taskAffinity=""
android:exported="true">
<intent-filter android:priority="1">
<action android:name="com.android.settings.SOUND_SETTINGS" />
<action android:name="android.settings.SOUND_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE_LAUNCH" />
<category android:name="com.android.settings.SHORTCUT" />
</intent-filter>
<!-- 响应哪个action 以及 priority -->
<intent-filter android:priority="7">
<action android:name="com.android.settings.action.SETTINGS" />
</intent-filter>

<!-- 属于哪个category -->
<meta-data android:name="com.android.settings.category"
android:value="com.android.settings.category.device" />
<!-- 点击后切换到哪个fragment -->
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.settings.notification.SoundSettings" />

<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>

上面代码可以看到Activity定义中包括这个action:”com.android.settings.action.SETTINGS”, 同时还可以看到meta-data中包括category和FRAGMENT_CLASS。
下面是通过action查询activitis的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/// file: frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java

private static void getTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings) {
Intent intent = new Intent(action);
// 是否只查询"com.android.settings"包
if (requireSettings) {
intent.setPackage(SETTING_PKG);
}
getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
requireSettings, true);
}

public static void getTilesForIntent(Context context, UserHandle user, Intent intent,
Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles,
boolean usePriority, boolean checkCategory) {
PackageManager pm = context.getPackageManager();

// 查询"com.android.settings"中包含指定action的Activities
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
for (ResolveInfo resolved : results) {
...

// 如下就是新建一个tile的过程,也就是具体的一个菜单项
Pair<String, String> key = new Pair<String, String>(activityInfo.packageName,
activityInfo.name);
Tile tile = addedCache.get(key);
if (tile == null) {
tile = new Tile();
// 点击此tile发送的intent
tile.intent = new Intent().setClassName(
activityInfo.packageName, activityInfo.name);
tile.category = categoryKey;
tile.priority = usePriority ? resolved.priority : 0;
tile.metaData = activityInfo.metaData;
updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
pm);
if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);

addedCache.put(key, tile);
}
}
}

获取完设置项后会对tiles和categories进行排序,对应的Comparator 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// file: frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java

private static final Comparator<Tile> TILE_COMPARATOR =
new Comparator<Tile>() {
@Override
public int compare(Tile lhs, Tile rhs) {
// 比较的是priority
return rhs.priority - lhs.priority;
}
};

private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR =
new Comparator<DashboardCategory>() {
@Override
public int compare(DashboardCategory lhs, DashboardCategory rhs) {
// 比较的是priority
return rhs.priority - lhs.priority;
}
};

可以看到tiles和categories排序都是通过比较priority,tile的priority前面可以看到是在清单中activity定义处定义的,category的priority定义在Settings的应用清单中,具体代码可以查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// file: frameworks/base/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java

private static DashboardCategory createCategory(Context context, String categoryKey) {
...
List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0);
...
for (ResolveInfo resolved : results) {
...
category.priority = SETTING_PKG.equals(
resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0;
...
}
}

4. 菜单项适配器

前面有提到在rebuildUI()方法中,获取菜单项以后会将adapter中的数据更新并刷新显示

1
mAdapter.setCategories(categories);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// file: packages/apps/Settings/src/com/android/settings/dashboard/DashboardAdapter.java

public void setCategories(List<DashboardCategory> categories) {
// 赋值给全局变量
mCategories = categories;
...
// 适配菜单
recountItems();
}

private void recountItems() {
...
for (int i = 0; mCategories != null && i < mCategories.size(); i++) {
DashboardCategory category = mCategories.get(i);
countItem(category, R.layout.dashboard_category, mIsShowingAll, NS_ITEMS);
for (int j = 0; j < category.tiles.size(); j++) {
Tile tile = category.tiles.get(j);
// 将tile添加到菜单中
countItem(tile, R.layout.dashboard_tile, mIsShowingAll
|| ArrayUtils.contains(DashboardSummary.INITIAL_ITEMS,
tile.intent.getComponent().getClassName()), NS_ITEMS);
}
}
// 更新SummaryLoader
notifyDataSetChanged();
}

菜单项summary是如何显示的

1. Summary 被谁控制的?

1
2
3
4
5
6
7
8
9
/// file: packages/apps/Settings/src/com/android/settings/dashboard/DashboardSummary.java

public void onCreate(Bundle savedInstanceState) {
...
List<DashboardCategory> categories =
((SettingsActivity) getActivity()).getDashboardCategories();
mSummaryLoader = new SummaryLoader(getActivity(), categories);
...
}

从代码中可以看到,通过getDashboardCategories() 获取到菜单项后,new了一个SummaryLoader类的对象,SummaryLoader类的作用就是用来控制菜单的summary的,后面通过mSummaryLoader设置是否监听,当监听到某个事件时,就更新对应菜单的summary。

1
2
3
4
5
6
7
8
9
10
11
public void onStart() {
...
mSummaryLoader.setListening(true);
...
}

public void onStop() {
...
mSummaryLoader.setListening(false);
...
}

2. SummaryLoader类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// file: packages/apps/Settings/src/com/android/settings/dashboard/SummaryLoader.java

// 构造方法
public SummaryLoader(Activity activity, List<DashboardCategory> categories) {
// 用于更新summary
mHandler = new Handler();
// 用于创建Provider 和设置Listening的线程
mWorkerThread = new HandlerThread("SummaryLoader", Process.THREAD_PRIORITY_BACKGROUND);
mWorkerThread.start();
mWorker = new Worker(mWorkerThread.getLooper());
mActivity = activity;
for (int i = 0; i < categories.size(); i++) {
List<Tile> tiles = categories.get(i).tiles;
for (int j = 0; j < tiles.size(); j++) {
Tile tile = tiles.get(j);
// 发送消息通知worker生成provider
mWorker.obtainMessage(Worker.MSG_GET_PROVIDER, tile).sendToTarget();
}
}
}

// 内部类 Worker
private class Worker extends Handler {
private static final int MSG_GET_PROVIDER = 1;
private static final int MSG_SET_LISTENING = 2;

public Worker(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_GET_PROVIDER:
Tile tile = (Tile) msg.obj;
makeProviderW(tile);
break;
case MSG_SET_LISTENING:
boolean listening = msg.arg1 != 0;
setListeningW(listening);
break;
}
}
}

上面可以看出,在SummaryLoader的构造方法中,创建了一个线程用于生成Provider 和设置Listening, 然后紧跟着一个嵌套循环遍历菜单项, 为每一个菜单项发消息生成provider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/// file: packages/apps/Settings/src/com/android/settings/dashboard/SummaryLoader.java

private synchronized void makeProviderW(Tile tile) {
// 这里
SummaryProvider provider = getSummaryProvider(tile);
if (provider != null) {
if (DEBUG) Log.d(TAG, "Creating " + tile);
mSummaryMap.put(provider, tile.intent.getComponent());
}
}

private SummaryProvider getSummaryProvider(Tile tile) {
// 这段代码主要是获取类名,如果为空就return null
Bundle metaData = getMetaData(tile);
if (metaData == null) {
return null;
}
String clsName = metaData.getString(SettingsActivity.META_DATA_KEY_FRAGMENT_CLASS);
if (clsName == null) {
return null;
}

...
// Java反射机制
// 根据类的名字创建一个类对象
Class<?> cls = Class.forName(clsName);
// 获取类对象中的静态对象SUMMARY_PROVIDER_FACTORY
Field field = cls.getField(SUMMARY_PROVIDER_FACTORY);
SummaryProviderFactory factory = (SummaryProviderFactory) field.get(null);
// call createSummaryProvider() 方法获得菜单项的SummaryProvider对象
// 注意参数this,将当前SummaryLoader对象的引用出传递过去了
return factory.createSummaryProvider(mActivity, this);
...
}

上面这两段代码的主要思路就是获取菜单项对应的类名,然后通过Java反射机制,最终获得类中实现了SummaryLoader.SummaryProvider 接口的对象。

3. SummaryProvider 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/// 这里以"Sounds"菜单的SoundSettings类举例说明
/// file: packages/apps/Settings/src/com/android/settings/notification/SoundSettings.java

// SUMMARY_PROVIDER_FACTORY, 一个实现了SummaryLoader.SummaryProviderFactory 接口的匿名内部类对象
public static final SummaryLoader.SummaryProviderFactory SUMMARY_PROVIDER_FACTORY
= new SummaryLoader.SummaryProviderFactory() {
@Override
public SummaryLoader.SummaryProvider createSummaryProvider(Activity activity,
SummaryLoader summaryLoader) {
return new SummaryProvider(activity, summaryLoader);
}
};

// 实现了 SummaryLoader.SummaryProvider 接口
private static class SummaryProvider extends BroadcastReceiver
implements SummaryLoader.SummaryProvider {

public SummaryProvider(Context context, SummaryLoader summaryLoader) {
// 获取SummaryLoader的引用
mSummaryLoader = summaryLoader;
}

// SummaryLoader.SummaryProvider 接口中的方法
public void setListening(boolean listening) {
if (listening) {
// 设置监听内容并开始监听
IntentFilter filter = new IntentFilter();
filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
filter.addAction(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION);
filter.addAction(NotificationManager.ACTION_EFFECTS_SUPPRESSOR_CHANGED);
mContext.registerReceiver(this, filter);
} else {
// 取消监听
mContext.unregisterReceiver(this);
}
}

public void onReceive(Context context, Intent intent) {
String percent = NumberFormat.getPercentInstance().format(
(double) mAudioManager.getStreamVolume(AudioManager.STREAM_RING) / maxVolume);
// 接收到音量变化事件,调用SummaryLoader的setSummary()方法更新菜单的Summary
mSummaryLoader.setSummary(this,
mContext.getString(R.string.sound_settings_summary, percent));
}
}

上面代码的核心内容就是实现SummaryLoader.SummaryProvider接口的setListening()方法,根据是否监听来进行不同的动作。
当需要更新summary时,就通过SummaryLoader的setSummary()方法为当前菜单项更新summary。
注意不一定是要设置BroadcastReceiver,有的菜单在setListening()方法中就直接更新summary了。