前言:客制化开发的过程中接触Setting次数挺多的,但是在接触的过程中发现Setting和其他应用的逻辑很不一样,Setting到底是怎么在实现逻辑的,这个问题一直环绕着在我心里,趁现在有时间,决定写个博客记录一下,温故而知新。
从启动开始说起
进入setting的AndroidManifest.xml里看一看,找启动Activity
<activity-alias android:name="Settings"
android:taskAffinity="com.android.settings"
android:label="@string/settings_label_launcher"
android:launchMode="singleTask"
android:targetActivity="Settings">
<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所标注的Settings。
走进Settings.java
public class Settings extends SettingsActivity {
public static class AssistGestureSettingsActivity extends SettingsActivity { /* empty */}
public static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }
public static class SimSettingsActivity extends SettingsActivity { /* empty */ }
public static class TetherSettingsActivity extends SettingsActivity { /* empty */ }
public static class VpnSettingsActivity extends SettingsActivity { /* empty */ }
public static class DateTimeSettingsActivity extends SettingsActivity { /* empty */ }
public static class PrivateVolumeForgetActivity extends SettingsActivity { /* empty */ }
public static class PrivateVolumeSettingsActivity extends SettingsActivity { /* empty */ }
public static class PublicVolumeSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiSettingsActivity extends SettingsActivity { /* empty */ }
public static class WifiP2pSettingsActivity extends SettingsActivity { /* empty */ }
public static class AvailableVirtualKeyboardActivity extends SettingsActivity { /* empty */ }
public static class KeyboardLayoutPickerActivity extends SettingsActivity { /* empty */ }
...//代码省略
第一次打开Setting.java这个文件的时候我被吓到了,里面全是静态内部类,而且都是跟Settings一样,继承了SettingsActivity,但是所有的类内容都是empty,可想而知这里是使用了特殊的抽象才这样设计的,啥也没有,我们就需要去他们的父类去看了。
SettingsActivity
@Override
protected void onCreate(Bundle savedState) {
...
final ComponentName cn = intent.getComponent();
final String className = cn.getClassName();
mIsShowingDashboard = className.equals(Settings.class.getName());
setContentView(mIsShowingDashboard ?
R.layout.settings_main_dashboard :R.layout.settings_main_prefs);
...
mContent = findViewById(R.id.main_content);
...
if (savedState != null) {
setTitleFromIntent(intent);
ArrayList<DashboardCategory> categories =
savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);
if (categories != null) {
mCategories.clear();
mCategories.addAll(categories);
setTitleFromBackStack();
}
mDisplayHomeAsUpEnabled = savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);
} else {
launchSettingFragment(initialFragmentName, isSubSettings, intent);
}
...//代码省略
正常第一次启动自然是进入launchSettingFragment了,
void launchSettingFragment(String initialFragmentName, boolean isSubSettings, Intent intent) {
if (!mIsShowingDashboard && initialFragmentName != null) {
// UP will be shown only if it is a sub settings
if (mIsShortcut) {
mDisplayHomeAsUpEnabled = isSubSettings;
} else if (isSubSettings) {
mDisplayHomeAsUpEnabled = true;
} else {
mDisplayHomeAsUpEnabled = false;
}
setTitleFromIntent(intent);
Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true, false,
mInitialTitleResId, mInitialTitle, false);
} else {
// Show search icon as up affordance if we are displaying the main Dashboard
mDisplayHomeAsUpEnabled = true;
mInitialTitleResId = R.string.dashboard_title;
switchToFragment(DashboardSummary.class.getName(), null /* args */, false, false,
mInitialTitleResId, mInitialTitle, false);
}
}
接着就去开启Fragment
private Fragment switchToFragment(String fragmentName, Bundle args, boolean validate,
boolean addToBackStack, int titleResId, CharSequence title, boolean withTransition) {
if (validate && !isValidFragment(fragmentName)) {
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();
return f;
}
可以看到,这里是直接初始化创建一个fragmentName指定的Fragment,
由于前面mIsShowingDashboard = className.equals(Settings.class.getName());
而第一次启动,当然是Settings.class,所以mIsShowingDashboard = true;
那么这里的初始化第一个Fragment就是DashboardSummary.class了
这样,第一次初始化就是打开Settings这个空Activity,然后加载DashboardSummary这个Fragment。
DashboardSummary
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.dashboard, container, false);
}
dashboard.xml
<com.android.settings.dashboard.conditional.FocusRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dashboard_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:focusable="false"
android:paddingStart="@dimen/dashboard_padding_start"
android:paddingEnd="@dimen/dashboard_padding_end"
android:paddingTop="@dimen/dashboard_padding_top"
android:paddingBottom="@dimen/dashboard_padding_bottom"
android:scrollbars="vertical"/>
DashboardSummary的布局文件里,只有一个FocusRecyclerView,所有的视图内容都是加载到这个RecyclerView里的,这也是我们第一次打开Setting,看到的那个页面。
接着
@Override
public void onViewCreated(View view, Bundle bundle) {
long startTime = System.currentTimeMillis();
mDashboard = view.findViewById(R.id.dashboard_container);
mLayoutManager = new LinearLayoutManager(getContext());
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
if (bundle != null) {
int scrollPosition = bundle.getInt(EXTRA_SCROLL_POSITION);
mLayoutManager.scrollToPosition(scrollPosition);
}
mDashboard.setLayoutManager(mLayoutManager);
mDashboard.setHasFixedSize(true);
mDashboard.setListener(this);
mDashboard.setDetachListener(this);
mAdapter = new DashboardAdapter(getContext(), bundle,mConditionManager.getConditions(),
mSuggestionParser, this /* SuggestionDismissController.Callback */);
mDashboard.setAdapter(mAdapter);
mDashboard.setItemAnimator(new DashboardItemAnimator());
mSummaryLoader.setSummaryConsumer(mAdapter);
ActionBarShadowController.attachToRecyclerView(
getActivity().findViewById(R.id.search_bar_container), getLifecycle(), mDashboard);
rebuildUI();
}
在onViewCreated里,处理了Recyclerview的数据加载,其中mDashboard就是Recyclerview,然后我们需要去看
它的setAdapter,这个mAdapter就是DashboardAdapter。
DashboardAdapter
public class DashboardAdapter extends RecyclerView.Adapter<DashboardAdapter.DashboardItemHolder>
构造方法
public DashboardAdapter(Context context, Bundle savedInstanceState,
List<Condition> conditions, SuggestionParser suggestionParser,
SuggestionDismissController.Callback callback) {
List<Tile> suggestions = null;
DashboardCategory category = null;
int suggestionConditionMode = DashboardData.HEADER_MODE_DEFAULT;
mContext = context;
final FeatureFactory factory = FeatureFactory.getFactory(context);
mMetricsFeatureProvider = factory.getMetricsFeatureProvider();
mDashboardFeatureProvider = factory.getDashboardFeatureProvider(context);
mSuggestionFeatureProvider = factory.getSuggestionFeatureProvider(context);
mCache = new IconCache(context);
mSuggestionParser = suggestionParser;
mCallback = callback;
setHasStableIds(true);
if (savedInstanceState != null) {
suggestions = savedInstanceState.getParcelableArrayList(STATE_SUGGESTION_LIST);
category = savedInstanceState.getParcelable(STATE_CATEGORY_LIST);
suggestionConditionMode = savedInstanceState.getInt(
STATE_SUGGESTION_CONDITION_MODE, suggestionConditionMode);
mSuggestionsShownLogged = savedInstanceState.getStringArrayList(
STATE_SUGGESTIONS_SHOWN_LOGGED);
} else {
mSuggestionsShownLogged = new ArrayList<>();
}
mDashboardData = new DashboardData.Builder()
.setConditions(conditions)
.setSuggestions(suggestions)
.setCategory(category)
.setSuggestionConditionMode(suggestionConditionMode)
.build();
}
其中数据的初始化来自于mDashboardData,这里使用的是建造者模式,builder构造。
其中,设置了三个参数setConditions,setSuggestions,setCategory。
public DashboardData build() {
return new DashboardData(this);
}
private DashboardData(Builder builder) {
mCategory = builder.mCategory;
mConditions = builder.mConditions;
mSuggestions = builder.mSuggestions;
mSuggestionConditionMode = builder.mSuggestionConditionMode;
mItems = new ArrayList<>();
buildItemsData();
}
private void buildItemsData() {
final boolean hasSuggestions = sizeOf(mSuggestions) > 0;
final List<Condition> conditions = getConditionsToShow(mConditions);
final boolean hasConditions = sizeOf(conditions) > 0;
final List<Tile> suggestions = getSuggestionsToShow(mSuggestions);
final int hiddenSuggestion =
hasSuggestions ? sizeOf(mSuggestions) - sizeOf(suggestions) : 0;
final boolean hasSuggestionAndCollapsed = hasSuggestions
&& mSuggestionConditionMode == HEADER_MODE_COLLAPSED;
final boolean onlyHasConditionAndCollapsed = !hasSuggestions
&& hasConditions
&& mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED;
/* Top suggestion/condition header. This will be present when there is any suggestion
* and the mode is collapsed */
addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
R.layout.suggestion_condition_header,
STABLE_ID_SUGGESTION_CONDITION_TOP_HEADER, hasSuggestionAndCollapsed);
/* Use mid header if there is only condition & it's in collapsed mode */
addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
R.layout.suggestion_condition_header,
STABLE_ID_SUGGESTION_CONDITION_MIDDLE_HEADER, onlyHasConditionAndCollapsed);
/* Suggestion container. This is the card view that contains the list of suggestions.
* This will be added whenever the suggestion list is not empty */
addToItemList(suggestions, R.layout.suggestion_condition_container,
STABLE_ID_SUGGESTION_CONTAINER, sizeOf(suggestions) > 0);
/* Second suggestion/condition header. This will be added when there is at least one
* suggestion or condition that is not currently displayed, and the user can expand the
* section to view more items. */
addToItemList(new SuggestionConditionHeaderData(conditions, hiddenSuggestion),
R.layout.suggestion_condition_header,
STABLE_ID_SUGGESTION_CONDITION_MIDDLE_HEADER,
mSuggestionConditionMode != HEADER_MODE_COLLAPSED
&& mSuggestionConditionMode != HEADER_MODE_FULLY_EXPANDED
&& (hiddenSuggestion > 0 || hasConditions && hasSuggestions));
/* Condition container. This is the card view that contains the list of conditions.
* This will be added whenever the condition list is not empty */
addToItemList(conditions, R.layout.suggestion_condition_container,
STABLE_ID_CONDITION_CONTAINER,
hasConditions && mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED);
/* Suggestion/condition footer. This will be present when the section is fully expanded
* or when there is no conditions and no hidden suggestions */
addToItemList(null /* item */, R.layout.suggestion_condition_footer,
STABLE_ID_SUGGESTION_CONDITION_FOOTER,
(hasConditions || hasSuggestions)
&& mSuggestionConditionMode == HEADER_MODE_FULLY_EXPANDED
|| hasSuggestions
&& !hasConditions
&& hiddenSuggestion == 0);
if(mCategory != null) {
for (int j = 0; j < mCategory.tiles.size(); j++) {
final Tile tile = mCategory.tiles.get(j);
addToItemList(tile, R.layout.dashboard_tile, Objects.hash(tile.title),
true /* add */);
}
}
}
这里加载了很多tile,然后全都调用了addToItemList,
private void addToItemList(Object item, int type, int stableId, boolean add) {
if (add) {
mItems.add(new Item(item, type, stableId));
}
}
这个Item就是存储设置选项的信息
static class Item {
// valid types in field type
private static final int TYPE_DASHBOARD_TILE = R.layout.dashboard_tile;
private static final int TYPE_SUGGESTION_CONDITION_CONTAINER =
R.layout.suggestion_condition_container;
private static final int TYPE_SUGGESTION_CONDITION_HEADER =
R.layout.suggestion_condition_header;
private static final int TYPE_SUGGESTION_CONDITION_FOOTER =
R.layout.suggestion_condition_footer;
private static final int TYPE_DASHBOARD_SPACER = R.layout.dashboard_spacer;
@IntDef({TYPE_DASHBOARD_TILE, TYPE_SUGGESTION_CONDITION_CONTAINER,
TYPE_SUGGESTION_CONDITION_HEADER, TYPE_SUGGESTION_CONDITION_FOOTER,
TYPE_DASHBOARD_SPACER})
public Item(Object entity, @ItemTypes int type, int id) {
this.entity = entity;
this.type = type;
this.id = id;
}
这里的构造方法显示三个参数,其中根据不同的TYPE,存储不同的layout的ID,可以知道有五种类型的layout,分别是Tile,Container,Header,Footer,Space,我们在设置里能看到的就这几种条目类型。
我们最基础的类型就是TYPE_DASHBOARD_TILE了,
<LinearLayout>
//省略细节
<ImageView
android:id="@android:id/icon"
<LinearLayout
<TextView android:id="@android:id/title"
<TextView android:id="@android:id/summary"
</LinearLayout>
</LinearLayout>
可以看到在普通的设置选项条目里,我们能看到的就是一个图标,一个标题,和一个摘要。
就像下面这样
上面Item的构造方法这里传入的其中一个参数是Object类型,但是我们需要知道到底这个Object的实例是什么。
看看前面的addToItemList,分别传入的是
“SuggestionConditionHeaderData”,
“List suggestions”,
“List conditions”,
DashboardCategory mCategory的“Tile”。
suggestions,conditions是在DashboardAdapter创建的时候传递的,我们看看这里的数据来源。
conditions
当时DashboardAdapter的第二个构造参数是
mConditionManager.getConditions();
mConditionManager = ConditionManager.get(activity, false);
单例模式
public static ConditionManager get(Context context, boolean loadConditionsNow) {
if (sInstance == null) {
sInstance = new ConditionManager(context.getApplicationContext(), loadConditionsNow);
}
return sInstance;
}
private ConditionManager(Context context, boolean loadConditionsNow) {
mContext = context;
mConditions = new ArrayList<>();
if (loadConditionsNow) {
Log.d(TAG, "conditions loading synchronously");
ConditionLoader loader = new ConditionLoader();
loader.onPostExecute(loader.doInBackground());
} else {
Log.d(TAG, "conditions loading asychronously");
new ConditionLoader().execute();
}
}
new ConditionLoader().execute();
protected ArrayList<Condition> doInBackground(Void... params) {
Log.d(TAG, "loading conditions from xml");
ArrayList<Condition> conditions = new ArrayList<>();
mXmlFile = new File(mContext.getFilesDir(), FILE_NAME);
if (mXmlFile.exists()) {
readFromXml(mXmlFile, conditions);
}
addMissingConditions(conditions);
return conditions;
}
@Override
protected void onPostExecute(ArrayList<Condition> conditions) {
Log.d(TAG, "conditions loaded from xml, refreshing conditions");
mConditions.clear();
mConditions.addAll(conditions);
refreshAll();
}
所以,mConditions来自这里,这里显示的却是readFromXml。从xml里去读吗?
private static final String FILE_NAME = "condition_state.xml";
然后很尴尬的发现系统里找不到这个文件,这样只能去看addMissingConditions了。
private void addMissingConditions(ArrayList<Condition> conditions) {
addIfMissing(AirplaneModeCondition.class, conditions);
addIfMissing(HotspotCondition.class, conditions);
addIfMissing(DndCondition.class, conditions);
addIfMissing(BatterySaverCondition.class, conditions);
addIfMissing(CellularDataCondition.class, conditions);
addIfMissing(BackgroundDataCondition.class, conditions);
addIfMissing(WorkModeCondition.class, conditions);
addIfMissing(NightDisplayCondition.class, conditions);
Collections.sort(conditions, CONDITION_COMPARATOR);
}
private void addIfMissing(Class<? extends Condition> clz, ArrayList<Condition> conditions) {
if (getCondition(clz, conditions) == null) {
if (DEBUG) Log.d(TAG, "Adding missing " + clz.getName());
Condition condition = createCondition(clz);
if (condition != null) {
conditions.add(condition);
}
}
}
好吧,原来是这些condition,那这些condition是什么呢?
举个例子,如果我们打开飞行模式,就会看到这个条目了。
condition已经搞明白了,剩下的还有suggestions和category了。
suggestions
suggestions是一个Tile的列表
在DashboardSummary的onViewCreated里,最后调用的rebuildUI
rebuildUI
@VisibleForTesting
void rebuildUI() {
if (!mSuggestionFeatureProvider.isSuggestionEnabled(getContext())) {
Log.d(TAG, "Suggestion feature is disabled, skipping suggestion entirely");
updateCategoryAndSuggestion(null /* tiles */);
} else {
new SuggestionLoader().execute();
// Set categories on their own if loading suggestions takes too long.
mHandler.postDelayed(() -> {
updateCategoryAndSuggestion(null /* tiles */);
}, MAX_WAIT_MILLIS);
}
}
这里做了一个判断,isSuggestionEnabled就是建议是否开启,然后决定是否去加载Suggestion。
我们需要去看看isSuggestionEnabled是什么
mSuggestionFeatureProvider.isSuggestionEnabled(getContext())
----------------
private SuggestionFeatureProvider mSuggestionFeatureProvider;
----------------
public interface SuggestionFeatureProvider
----------------
public class SuggestionFeatureProviderImpl implements SuggestionFeatureProvider
SuggestionFeatureProviderImpl是接口SuggestionFeatureProvider的实例,
@Override
public boolean isSuggestionEnabled(Context context) {
final ActivityManager am =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
return !am.isLowRamDevice();
}
我们可以看到,这里决定是否isSuggestionEnabled的结果是判断当前设备是否是低Ram设备。
所以,如果我们是Ram低于512M的设备就返回false,如果是大于512M设备就返回true。
当前我们开发的设备是1G Ram的,所以返回True
new SuggestionLoader().execute();
// Set categories on their own if loading suggestions takes too long.
mHandler.postDelayed(() -> {
updateCategoryAndSuggestion(null /* tiles */);
}, MAX_WAIT_MILLIS);
SuggestionLoader加载suggestions
@Override
protected List<Tile> doInBackground(Void... params) {
final Context context = getContext();
boolean isSmartSuggestionEnabled =
mSuggestionFeatureProvider.isSmartSuggestionEnabled(context);
final SuggestionList sl = mSuggestionParser.getSuggestions(isSmartSuggestionEnabled);
final List<Tile> suggestions = sl.getSuggestions();
if (isSmartSuggestionEnabled) {
List<String> suggestionIds = new ArrayList<>(suggestions.size());
for (Tile suggestion : suggestions) {
suggestionIds.add(mSuggestionFeatureProvider.getSuggestionIdentifier(
context, suggestion));
}
// TODO: create a Suggestion class to maintain the id and other info
mSuggestionFeatureProvider.rankSuggestions(suggestions, suggestionIds);
}
for (int i = 0; i < suggestions.size(); i++) {
Tile suggestion = suggestions.get(i);
if (mSuggestionsChecks.isSuggestionComplete(suggestion)) {
suggestions.remove(i--);
}
}
if (sl.isExclusiveSuggestionCategory()) {
mSuggestionFeatureProvider.filterExclusiveSuggestions(suggestions);
}
return suggestions;
}
其中
mSuggestionParser.getSuggestions(isSmartSuggestionEnabled);
public SuggestionList getSuggestions(boolean isSmartSuggestionEnabled) {
final SuggestionList suggestionList = new SuggestionList();
final int N = mSuggestionList.size();
for (int i = 0; i < N; i++) {
final SuggestionCategory category = mSuggestionList.get(i);
if (category.exclusive && !isExclusiveCategoryExpired(category)) {
final List<Tile> exclusiveSuggestions = new ArrayList<>();
readSuggestions(category, exclusiveSuggestions, false );
if (!exclusiveSuggestions.isEmpty()) {
final SuggestionList exclusiveList = new SuggestionList();
exclusiveList.addSuggestions(category, exclusiveSuggestions);
return exclusiveList;
}
} else {
final List<Tile> suggestions = new ArrayList<>();
readSuggestions(category, suggestions, isSmartSuggestionEnabled);
suggestionList.addSuggestions(category, suggestions);
}
}
return suggestionList;
}
void readSuggestions(
SuggestionCategory category, List<Tile> suggestions, boolean isSmartSuggestionEnabled) {
int countBefore = suggestions.size();
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(category.category);
if (category.pkg != null) {
intent.setPackage(category.pkg);
}
TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent,
mAddCache, null, suggestions, true, false, false, true );
filterSuggestions(suggestions, countBefore, isSmartSuggestionEnabled);
if (!category.multiple && suggestions.size() > (countBefore + 1)) {
Tile item = suggestions.remove(suggestions.size() - 1);
while (suggestions.size() > countBefore) {
Tile last = suggestions.remove(suggestions.size() - 1);
if (last.priority > item.priority) {
item = last;
}
}
if (!isCategoryDone(category.category)) {
suggestions.add(item);
}
}
}
其中就是从SuggestionCategory来加载Tile到suggestions,但是我们需要知道category是从哪里来的,
final SuggestionCategory category = mSuggestionList.get(i);
mSuggestionList = suggestionList;
在SuggestionParser的构造时候,传入了suggestionList,
(List<SuggestionCategory>) new SuggestionOrderInflater(context).parse(orderXml)
然后
public Object parse(int resource) {
XmlPullParser parser = mContext.getResources().getXml(resource);
final AttributeSet attrs = Xml.asAttributeSet(parser);
try {
int type;
do {
type = parser.next();
} while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
Object xmlRoot = onCreateItem(parser.getName(), attrs);
rParse(parser, xmlRoot, attrs);
return xmlRoot;
} catch (XmlPullParserException | IOException e) {
Log.w(TAG, "Problem parser resource " + resource, e);
return null;
}
}
也是从xml里读取
继续找,发现前面传递的是 R.xml.suggestion_ordering
mSuggestionParser = new SuggestionParser(activity,
mSuggestionFeatureProvider.getSharedPrefs(activity), R.xml.suggestion_ordering);
<optional-steps>
<step category="com.android.settings.suggested.category.DEFERRED_SETUP"
exclusive="true" />
<step category="com.android.settings.suggested.category.LOCK_SCREEN" />
<step category="com.android.settings.suggested.category.TRUST_AGENT" />
<step category="com.android.settings.suggested.category.EMAIL" />
<step category="com.android.settings.suggested.category.PARTNER_ACCOUNT"
multiple="true" />
<step category="com.android.settings.suggested.category.GESTURE" />
<step category="com.android.settings.suggested.category.HOTWORD" />
<step category="com.android.settings.suggested.category.DEFAULT"
multiple="true" />
<step category="com.android.settings.suggested.category.SETTINGS_ONLY"
multiple="true" />
</optional-steps>
parse解析的过程里
rParse(parser, xmlRoot, attrs);
private void rParse(XmlPullParser parser, Object parent, final AttributeSet attrs)
throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
Object item = onCreateItem(name, attrs);
onAddChildItem(parent, item);
rParse(parser, item, attrs);
}
}
protected Object onCreateItem(String name, AttributeSet attrs) {
if (name.equals(TAG_LIST)) {
return new ArrayList<SuggestionCategory>();
} else if (name.equals(TAG_ITEM)) {
SuggestionCategory category = new SuggestionCategory();
category.category = attrs.getAttributeValue(null, ATTR_CATEGORY);
category.pkg = attrs.getAttributeValue(null, ATTR_PACKAGE);
String multiple = attrs.getAttributeValue(null, ATTR_MULTIPLE);
category.multiple = !TextUtils.isEmpty(multiple) && Boolean.parseBoolean(multiple);
String exclusive = attrs.getAttributeValue(null, ATTR_EXCLUSIVE);
category.exclusive =
!TextUtils.isEmpty(exclusive) && Boolean.parseBoolean(exclusive);
String expireDaysAttr = attrs.getAttributeValue(null,ATTR_EXCLUSIVE_EXPIRE_DAYS);
long expireDays = !TextUtils.isEmpty(expireDaysAttr)
? Integer.parseInt(expireDaysAttr)
: -1;
category.exclusiveExpireDaysInMillis = DateUtils.DAY_IN_MILLIS *expireDays;
return category;
} else {
throw new IllegalArgumentException("Unknown item " + name);
}
}
当扫描到step的时候去创建SuggestionCategory,
所以这里的SuggestionCategory列表最后Item内容就是上面XML里的item内容。
我们仔细看这个category,发现很像我们在AndroidManifest.xml里定义的category。
那么前面mSuggestionList就是parse解析的结果new ArrayList();
在前面readSuggestions的时候传入的就是携带这xml里step的category。
我们继续回到readSuggestions
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(category.category);
if (category.pkg != null) {
intent.setPackage(category.pkg);
}
TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent,
mAddCache, null, suggestions, true, false, false, true /* shouldUpdateTiles */);
这里把category的category给了Intent然后调用了addCategory
然后走到TileUtils.getTilesForIntent,去把对应的Tile加到maddCache里。
private final ArrayMap<Pair<String, String>, Tile> mAddCache = new ArrayMap<>();
找到完了suggestion,最后找category
category
在DashboardAdapter构造的时候category = null;一直到setCategory都是null,那么category到底是什么情况呢?
最后我们需要看看**DashboardAdapter的setCategory(category)**这里的category可是null无疑,
但是前面 SuggestionLoader().execute() 获取 suggestion完了,后面还有个post更新
mHandler.postDelayed(() -> {
updateCategoryAndSuggestion(null /* tiles */);
}, MAX_WAIT_MILLIS);
updateCategoryAndSuggestion
看到上面传入的tiles 是NULL。
@VisibleForTesting
void updateCategoryAndSuggestion(List<Tile> suggestions) {
final Activity activity = getActivity();
if (activity == null) {
return;
}
final DashboardCategory category = mDashboardFeatureProvider.getTilesForCategory(
CategoryKey.CATEGO RY_HOMEPAGE);
mSummaryLoader.updateSummaryToCache(category);
if (suggestions != null) {
mAdapter.setCategoriesAndSuggestions(category, suggestions);
} else {
mAdapter.setCategory(category);
}
}
DashboardCategory
mDashboardFeatureProvider.getTilesForCategory(CategoryKey.CATEGO RY_HOMEPAGE);
找到实例
@Override
public DashboardCategory getTilesForCategory(String key) {
return mCategoryManager.getTilesByCategory(mContext, key);
}
---------------------------------------------
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); //
return mCategoryByKeyMap.get(categoryKey);
}
在获取category的时候,有一个初始化的过程
private synchronized void tryInitCategories(Context context, String settingPkg) {
tryInitCategories(context, false /* forceClearCache */, settingPkg);
}
---------------------------------------------
private synchronized void tryInitCategories(Context context, booleanforceClearCache,
String settingPkg) {
if (mCategories == null) {
if (forceClearCache) {
mTileByComponentCache.clear();
}
mCategoryByKeyMap.clear();
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);
}
}
其关键在于mCategories当我们第一次去获取category的时候mCategories == null。
于是
mCategories = TileUtils.getCategories(context, mTileByComponentCache,false, mExtraAction, settingPkg);
TileUtils.getCategories
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.
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);
}
...//省略
}
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) {
Collections.sort(category.tiles, TILE_COMPARATOR);
}
Collections.sort(categories, CATEGORY_COMPARATOR);
return categories;
}
我们发现,在初始化categories的时候,做了很多事情,
其中第一步是查询并填充ArrayList tiles,
通过
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);
这三步去填充tiles,
然后再用for循环,创建category
for (Tile tile : tiles)
。。。省略
category = createCategory(context, tile.category, categoryDefinedInManifest);
。。。
这里的category是DashboardCategory,
category.addTile(tile);
public void addTile(Tile tile) {
tiles.add(tile);
}
/**
* List of the category's children
*/
public List<Tile> tiles = new ArrayList<>();
所以,category.addTile的时候,是把Tile给加到了一个内部List里,这就说明,一个Category就是一些Tile的集合。
getTilesForAction
private static void getTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
String settingPkg) {
getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles,
requireSettings, requireSettings, settingPkg);
}
private static void getTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings,
boolean usePriority, String settingPkg) {
Intent intent = new Intent(action);
if (requireSettings) {
intent.setPackage(settingPkg);
}
getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles,
usePriority, true, true);
}
然后走到了和前面获取suggestion一样的地方,都是getTilesForIntent,只不过这里的category和suggestion那里的xml的category不一样。
这里三个category分别是
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";
最后一起来看看是怎么获取Tile的。
TileUtils
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, boolean forceTintExternalIcon,
boolean shouldUpdateTiles) {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
Map<String, IContentProvider> providerMap = new HashMap<>();
for (ResolveInfo resolved : results) {
if (!resolved.system && !ArrayUtils.contains(EXTRA_PACKAGE_WHITE_LIST,
resolved.activityInfo.packageName)) {
continue;
}
ActivityInfo activityInfo = resolved.activityInfo;
Bundle metaData = activityInfo.metaData;
String categoryKey = defaultCategory;
// Load category
if (checkCategory && ((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));
continue;
} else {
categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
/// M: set default category when extra data not set category
if (categoryKey == null) {
categoryKey = defaultCategory;
}
}
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 = new Intent().setClassName(
activityInfo.packageName, activityInfo.name);
tile.category = categoryKey;
/// M: white list could use priority
tile.priority = usePriority || ArrayUtils.contains(EXTRA_PACKAGE_WHITE_LIST,
activityInfo.packageName) ? resolved.priority : 0;
tile.metaData = activityInfo.metaData;
/// M: Support priority in whitelist app
if (tile.metaData.containsKey(META_DATA_KEY_PRIORITY) && ArrayUtils.contains(
EXTRA_PACKAGE_WHITE_LIST, activityInfo.packageName)) {
tile.priority = tile.metaData.getInt(META_DATA_KEY_PRIORITY);
}
updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
pm, providerMap, forceTintExternalIcon);
if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
addedCache.put(key, tile);
} else if (shouldUpdateTiles) {
updateSummaryAndTitle(context, providerMap, tile);
}
if (!tile.userHandle.contains(user)) {
tile.userHandle.add(user);
}
if (!outTiles.contains(tile)) {
outTiles.add(tile);
}
}
}
List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
这个Results是用PackageManager来query的结果,最后查询的结果是当前包里有相同的category的activity信息
ActivityInfo activityInfo = resolved.activityInfo;
Bundle metaData = activityInfo.metaData;
String categoryKey = defaultCategory;
然后我们看到了下面是创建Tile的过程
Tile tile = addedCache.get(key);
if (tile == null) {
tile = new Tile();
tile.intent = new Intent().setClassName(
activityInfo.packageName, activityInfo.name);
tile.category = categoryKey;
/// M: white list could use priority
tile.priority = usePriority ||
ArrayUtils.contains(EXTRA_PACKAGE_WHITE_LIST,activityInfo.packageName) ? resolved.priority : 0;
tile.metaData = activityInfo.metaData;
/// M: Support priority in whitelist app
if (tile.metaData.containsKey(META_DATA_KEY_PRIORITY) &&ArrayUtils.contains(
EXTRA_PACKAGE_WHITE_LIST, activityInfo.packageName)) {
tile.priority = tile.metaData.getInt(META_DATA_KEY_PRIORITY);
}
updateTileData(context, tile, activityInfo, activityInfo.applicationInfo,
pm, providerMap, forceTintExternalIcon);
if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title);
addedCache.put(key, tile);
} else if (shouldUpdateTiles) {
updateSummaryAndTitle(context, providerMap, tile);
}
分别把activityInfo.packageName, activityInfo.name,categoryKey等这些信息赋值给Tile,然后把tile加入到addedCache,
private final ArrayMap<Pair<String, String>, Tile> mAddCache = new ArrayMap<>();
就这样通过不同的category来得到不同activity在xml里定义的信息。
三个类型,分别对应着
.setConditions(conditions)
.setSuggestions(suggestions)
.setCategory(category)
这就是DashboardAdapter在初始化的时候,获取的数据的过程。
这里有一点需要说明的是,我在使用suggestion_ordering.xml里的category去AndroidMannifest.xml找,没有找到一个对应的activity,而使用SETTINGS_ACTION却能找到许多acticity。
比如
<activity android:name=".Settings$NetworkDashboardActivity"
android:taskAffinity="com.android.settings"
android:label="@string/network_dashboard_title"
android:icon="@drawable/ic_settings_wireless"
android:parentActivityName="Settings">
<intent-filter android:priority="1">
<action android:name="android.settings.WIRELESS_SETTINGS" />
<action android:name="android.settings.AIRPLANE_MODE_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" />
</intent-filter>
<intent-filter android:priority="11">
<action android:name="com.android.settings.action.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.FRAGMENT_CLASS"
android:value="com.android.settings.network.NetworkDashboardFragment"/>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true" />
</activity>
这里的NetworkDashboardActivity是在Setting里面定义了的。
这里可以看到,它其实是action。
而我们就是根据这个action来分门别类的找到对应的目录。
加载view
数据加载告一段落,回到DashboardAdapter的加载view的流程。
DashboardAdapter
构造ViewHolder,根据viewType不同来加载不同的viewType并创建Viewholder
这里的viewtype就是layoutID
@Override
public DashboardItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
if (viewType == R.layout.suggestion_condition_header) {
return new SuggestionAndConditionHeaderHolder(view);
}
if (viewType == R.layout.suggestion_condition_container) {
return new SuggestionAndConditionContainerHolder(view);
}
return new DashboardItemHolder(view);
}
默认ViewHolder就是DashboardItemHolder
public DashboardItemHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(android.R.id.icon);
title = itemView.findViewById(android.R.id.title);
summary = itemView.findViewById(android.R.id.summary);
}
各个item的type就是前面说的那个layout的ID
@Override
public int getItemViewType(int position) {
return mDashboardData.getItemTypeByPosition(position);
}
------------------------
public int getItemTypeByPosition(int position) {
return mItems.get(position).type;
}
然后我们看onBindViewHolder
@Override
public void onBindViewHolder(DashboardItemHolder holder, int position) {
final int type = mDashboardData.getItemTypeByPosition(position);
switch (type) {
case R.layout.dashboard_tile:
final Tile tile = (Tile)mDashboardData.getItemEntityByPosition(position);
onBindTile(holder, tile);
holder.itemView.setTag(tile);
holder.itemView.setOnClickListener(mTileClickListener);
break;
case R.layout.suggestion_condition_container:
onBindConditionAndSuggestion(
(SuggestionAndConditionContainerHolder) holder, position);
break;
case R.layout.suggestion_condition_header:
onBindSuggestionConditionHeader((SuggestionAndConditionHeaderHolder)holder,
(SuggestionConditionHeaderData)mDashboardData.getItemEntityByPosition(position));
break;
case R.layout.suggestion_condition_footer:
holder.itemView.setOnClickListener(v -> {
mMetricsFeatureProvider.action(mContext,
MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, false);
DashboardData prevData = mDashboardData;
mDashboardData = new DashboardData.Builder(prevData).setSuggestionConditionMode(
DashboardData.HEADER_MODE_COLLAPSED).build();
notifyDashboardDataChanged(prevData);
mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION);
});
break;
}
}
在Bindview的时候分别根据Tile,Container,header和footer来创建view。
看看onBindTile
private void onBindTile(DashboardItemHolder holder, Tile tile) {
if (tile.remoteViews != null) {
final ViewGroup itemView = (ViewGroup) holder.itemView;
itemView.removeAllViews();
itemView.addView(tile.remoteViews.apply(itemView.getContext(), itemView));
} else {
holder.icon.setImageDrawable(mCache.getIcon(tile.icon));
holder.title.setText(tile.title);
if (!TextUtils.isEmpty(tile.summary)) {
holder.summary.setText(tile.summary);
holder.summary.setVisibility(View.VISIBLE);
} else {
holder.summary.setVisibility(View.GONE);
}
}
}
就是这样,根据不同的layout,来加载不同的item,实现RecyclerView的填充。
点击事件
显示问题搞清楚了,接着我们看点击事件
holder.itemView.setTag(tile);
holder.itemView.setOnClickListener(mTileClickListener);
在bindview的时候设置了OnClickListener
private View.OnClickListener mTileClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
mDashboardFeatureProvider.openTileIntent((Activity) mContext, (Tile) v.getTag());
}
};
DashboardFeatureProviderImpl实现了DashboardFeatureProvider接口,这里是面向接口编程的思想。
@Override
public void openTileIntent(Activity activity, Tile tile) {
if (tile == null) {
Intent intent = new Intent(Settings.ACTION_SETTINGS).addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TASK);
mContext.startActivity(intent);
return;
}
if (tile.intent == null) {
return;
}
final Intent intent = new Intent(tile.intent)
.putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY,
MetricsEvent.DASHBOARD_SUMMARY)
.putExtra(SettingsDrawerActivity.EXTRA_SHOW_MENU, true)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
launchIntentOrSelectProfile(activity, tile, intent,MetricsEvent.DASHBOARD_SUMMARY);
}
在前面创建每个Tile的时候,Intent是这样的,
tile.intent = new Intent().setClassName(
activityInfo.packageName, activityInfo.name);
里面包含了对应包名,activity的名字。
private void launchIntentOrSelectProfile(Activity activity, Tile tile, Intent intent,
int sourceMetricCategory) {
if (!isIntentResolvable(intent)) {
Log.w(TAG, "Cannot resolve intent, skipping. " + intent);
return;
}
ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile);
if (tile.userHandle == null) {
mMetricsFeatureProvider.logDashboardStartIntent(mContext, intent, sourceMetricCategory);
activity.startActivityForResult(intent, 0);
} else if (tile.userHandle.size() == 1) {
mMetricsFeatureProvider.logDashboardStartIntent(mContext, intent, sourceMetricCategory);
activity.startActivityForResultAsUser(intent, 0, tile.userHandle.get(0));
} else {
ProfileSelectDialog.show(activity.getFragmentManager(), tile);
}
}
activity.startActivityForResultAsUser(intent, 0, tile.userHandle.get(0));
直接开启对应的activity,然后这里几乎所有activity都是继承自SettingsActivity,所有,就是启动它本身,换一个子类来显示。
然后在SettingsActivity的oncreate里,这时候的mIsShowingDashboard就是false,
就会显示R.layout.settings_main_prefs这个layout了,而且这时候的Intent是携带者具体要启动的activity信息的
final String initialFragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
然后就回到循环之中。
总结:Setting里所有的Activity都继承了SettingsActivity,通过action条目来统一管理加载,切换不同的activity实际也是调用它本身的不同子类,然后根据不同子类加载不同的fragment,所以说到底,Setting只有一个父Activity,所有的显示通过不同的Fragment来处理。实在是厉害。