目录
1、设置首页SettingsHomepageActivity.java
2、ContextualCardsFragment.java
6、LegacySuggestionContextualCardController.java
8、SettingsIntelligence-SuggestionService.java
9、SuggestionFeatureProvider.java
一、概述
什么是建议(Suggestion)菜单,如下图。在设置首页的,第一个菜单上方,搜索框下方会弹出一些快捷菜单。提醒用户进行一些个性化设置,用户设置后或者关闭后则不再显示
二、Suggestion常见问题
1、如何打开原生Suggestion菜单
出现时机需要满足三个条件,1、设备不是 LowRam 设备 2、启用 settings_contextual_home 特性 3、在开机一定时间后(一般是几天,具体看 AndroidManifest.xml 中的有配置),为什么是这几个,后面的分析流程会详细解释。代码逻辑可以查看SettingsHomePageActivity-OnCreate
2、原生默认有哪些Suggestion菜单
在Settings AndroidManifest.xml中搜索以下cagegory
"com.android.settings.suggested.category.FIRST_IMPRESSION"
所有Suggestion都需要配置此category,其实其他应用配置该category应该也可以显示
原生默认共有七个:
ZenSuggestionActivity
WallpaperSuggestionActivity
StyleSuggestionActivity
Settings$NightDisplaySuggestionActivity
ScreenLockSuggestionActivity
FingerprintEnrollSuggestionActivity
WifiCallingSuggestionActivity
3、如何配置Suggestion菜单显示时机
所有Suggestion都有配置"com.android.settings.dismiss",如果第一个值为0,则显示直接显示。
非0时,则会根据取一个值,判断多少天后显示。逻辑在
DismissedChecker.java-parseAppearDay()中
<meta-data android:name="com.android.settings.dismiss" android:value="10,14,30" />
三、Suggestion菜单加载流程分析
整个加载过程涉及Settings、SettingsLib、SettingsIntelligence、framework。为何设计这么复杂,还是涉及到framework,我猜测google工程师初衷是为了在其他应用中也可以增加Suggestion菜单
1、设置首页SettingsHomepageActivity.java
判定显示Suggestion条件,条件满足时加载ContextualCardsFragment
SettingsHomepageActivity.java
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
....
if (!getSystemService(ActivityManager.class).isLowRamDevice()) { //非LowRamDevice才会加载
initAvatarView();
final boolean scrollNeeded = mIsEmbeddingActivityEnabled
&& !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);
showSuggestionFragment(scrollNeeded);
//FeatureFlags.CONTEXTUAL_HOME = "settings_contextual_home" 该功能为true时才会显示Suggestion
if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {
//加载ContextualCardsFragment
showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);
((FrameLayout) findViewById(R.id.main_content))
.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
}
}
...
}
2、ContextualCardsFragment.java
Suggestion菜单显示在Fragment中,所有的Suggestion最终是用ContextualCardsFragment来加载和展示
ContextualCardsFragment.java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = getContext();
if (savedInstanceState == null) {
FeatureFactory.getFactory(context).getSlicesFeatureProvider().newUiSession();
BluetoothUpdateWorker.initLocalBtManager(getContext());
}
//该Manager会管理Suggestion相关的Controller、Render
mContextualCardManager = new ContextualCardManager(context, getSettingsLifecycle(), //pri suggestion 2
savedInstanceState);
mKeyEventReceiver = new KeyEventReceiver();
}
@Override
public void onStart() {
super.onStart();
registerScreenOffReceiver();
registerKeyEventReceiver();
//loadCards,默认配置这里是不执行的
mContextualCardManager.loadContextualCards(LoaderManager.getInstance(this), //pri suggestion 加载
sRestartLoaderNeeded);
sRestartLoaderNeeded = false;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, //pri suggestion 2
Bundle savedInstanceState) {
final Context context = getContext();
final View rootView = inflater.inflate(R.layout.settings_homepage, container, false);
mCardsContainer = rootView.findViewById(R.id.card_container);
mLayoutManager = new GridLayoutManager(getActivity(), SPAN_COUNT,
GridLayoutManager.VERTICAL, false /* reverseLayout */); //设为网格布局
mCardsContainer.setLayoutManager(mLayoutManager);
//Suggestion显示的布局未FocusRecyclerView,这个是对应的Adapter
mContextualCardsAdapter = new ContextualCardsAdapter(context, this /* lifecycleOwner */,
mContextualCardManager);
mCardsContainer.setItemAnimator(null);
mCardsContainer.setAdapter(mContextualCardsAdapter);
mContextualCardManager.setListener(mContextualCardsAdapter); //数据会通过回调从这里返回
mCardsContainer.setListener(this);
mItemTouchHelper = new ItemTouchHelper(new SwipeDismissalDelegate(mContextualCardsAdapter));
mItemTouchHelper.attachToRecyclerView(mCardsContainer);
return rootView;
}
3、ContextualCardManager.java
This is a centralized manager of multiple {@link ContextualCardController}.
用于新建管理Controller,并将数据返回给Adapter
ContextualCardManager.java
public ContextualCardManager(Context context, Lifecycle lifecycle, Bundle savedInstanceState) {
mContext = context;
mLifecycle = lifecycle;
mContextualCards = new ArrayList<>();
mLifecycleObservers = new ArrayList<>();
mControllerRendererPool = new ControllerRendererPool(); //此为Controller和Render之间的桥梁
mLifecycle.addObserver(this);
if (savedInstanceState == null) {
mIsFirstLaunch = true;
mSavedCards = null;
} else {
mSavedCards = savedInstanceState.getStringArrayList(KEY_CONTEXTUAL_CARDS);
}
// for data provided by Settings
for (@ContextualCard.CardType int cardType : getSettingsCards()) { //LEGACY_SUGGESTION & CONDITIONAL
setupController(cardType); //安装Suggestion control
}
}
void setupController(@ContextualCard.CardType int cardType) {
final ContextualCardController controller = mControllerRendererPool.getController(mContext, //pri suggestion 4
cardType); //这里会返回 LegacySuggestionContextualCardController
if (controller == null) {
Log.w(TAG, "Cannot find ContextualCardController for type " + cardType);
return;
}
controller.setCardUpdateListener(this); //绑定control,control会回调onContextualCardUpdated
if (controller instanceof LifecycleObserver && !mLifecycleObservers.contains(controller)) {
mLifecycleObservers.add((LifecycleObserver) controller);
mLifecycle.addObserver((LifecycleObserver) controller);
}
}
@Override
public void onContextualCardUpdated(Map<Integer, List<ContextualCard>> updateList) {
final Set<Integer> cardTypes = updateList.keySet();
...
//replace with the new data
mContextualCards.clear();
final List<ContextualCard> sortedCards = sortCards(allCards);
mContextualCards.addAll(getCardsWithViewType(sortedCards)); //pri suggestion add 数据
loadCardControllers();
if (mListener != null) {
final Map<Integer, List<ContextualCard>> cardsToUpdate = new ArrayMap<>();
cardsToUpdate.put(ContextualCard.CardType.DEFAULT, mContextualCards);
mListener.onContextualCardUpdated(cardsToUpdate); //将数据传入ContextualCardsAdapter
}
}
4、ContextualCardsAdapter.java
Suggestion RecyclerView的Adapter。会根据viewType绑定不同的layout界面
ContextualCardsAdapter.java
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, @LayoutRes int viewType) {
//获取 LegacySuggestionContextualCardRenderer
final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByViewType(
mContext, mLifecycleOwner, viewType);
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
//调用LegacySuggestionContextualCardRenderer.createViewHolder
return renderer.createViewHolder(view, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
final ContextualCard card = mContextualCards.get(position);
//获取 LegacySuggestionContextualCardRenderer
final ContextualCardRenderer renderer = mControllerRendererPool.getRendererByViewType(
mContext, mLifecycleOwner, card.getViewType());
renderer.bindView(holder, card);
}
@Override
public void onContextualCardUpdated(Map<Integer, List<ContextualCard>> cards) {
final List<ContextualCard> contextualCards = cards.get(ContextualCard.CardType.DEFAULT);
final boolean previouslyEmpty = mContextualCards.isEmpty();
final boolean nowEmpty = contextualCards == null || contextualCards.isEmpty();
if (contextualCards == null) {
mContextualCards.clear();
notifyDataSetChanged();
} else {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new ContextualCardsDiffCallback(mContextualCards, contextualCards));
mContextualCards.clear();
mContextualCards.addAll(contextualCards); //Adapter所用的数据从这加载
diffResult.dispatchUpdatesTo(this);
}
if (mRecyclerView != null && previouslyEmpty && !nowEmpty) {
// Adding items to empty list, should animate.
mRecyclerView.scheduleLayoutAnimation();
}
}
5、ControllerRendererPool.java
顾名思义,是个pool池。主要用于管理不同的Controller和Render。创建什么类型的Controller和Render都在这里有判断
ControllerRendererPool.java
public <T extends ContextualCardController> T getController(Context context,
@ContextualCard.CardType int cardType) {
//获取Controller类名
final Class<? extends ContextualCardController> clz =
ContextualCardLookupTable.getCardControllerClass(cardType);
for (ContextualCardController controller : mControllers) {
if (controller.getClass().getName().equals(clz.getName())) {
Log.d(TAG, "Controller is already there.");
return (T) controller;
}
}
//创建Controller类,默认为LegacySuggestionContextualCardController
final ContextualCardController controller = createCardController(context, clz);
if (controller != null) {
mControllers.add(controller);
}
return (T) controller;
}
private ContextualCardController createCardController(Context context,
Class<? extends ContextualCardController> clz) {
if (ConditionContextualCardController.class == clz) {
return new ConditionContextualCardController(context);
} else if (SliceContextualCardController.class == clz) {
return new SliceContextualCardController(context);
} else if (LegacySuggestionContextualCardController.class == clz) {
//默认走的这里
return new LegacySuggestionContextualCardController(context);
}
return null;
}
6、LegacySuggestionContextualCardController.java
默认用的LegacySuggestion。Suggestion数据加载主要由这个类发起
LegacySuggestionContextualCardController.java
public LegacySuggestionContextualCardController(Context context) {
mContext = context;
mSuggestions = new ArrayList<>();
if (!mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) {
Log.w(TAG, "Legacy suggestion contextual card disabled, skipping.");
return;
}
//获取的为/*"com.android.settings.intelligence",
"com.android.settings.intelligence.suggestions.SuggestionService"*/
final ComponentName suggestionServiceComponent =
FeatureFactory.getFactory(mContext).getSuggestionFeatureProvider()
.getSuggestionServiceComponent();
mSuggestionController = new SuggestionController( //SuggestionService包名从这里传过去
mContext, suggestionServiceComponent, this /* listener */);
}
@Override
public void onStart() {
if (mSuggestionController == null) {
return;
}
mSuggestionController.start(); //bindService在此调用
}
7、SuggestionController.java
/** * A controller class to access suggestion data. */
绑定远程SuggestionService,从SuggestionService获取data
SuggestionController.java
public SuggestionController(Context context, ComponentName service,
ServiceConnectionListener listener) {
mContext = context.getApplicationContext();
mConnectionListener = listener;
mServiceIntent = new Intent().setComponent(service);
mServiceConnection = createServiceConnection(); //创建ServiceConnection
}
/**
* Create a new {@link ServiceConnection} object to handle service connect/disconnect event.
*/
/**
* Start the controller.
*/
public void start() {
//bindService 绑定远程Service
mContext.bindServiceAsUser(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE,
android.os.Process.myUserHandle());
}
@Nullable
@WorkerThread
public List<Suggestion> getSuggestions() {
if (!isReady()) {
return null;
}
try {
return mRemoteService.getSuggestions(); // 此处会调用framework SuggestionService.getSuggestions()方法
} catch (NullPointerException e) {
Log.w(TAG, "mRemote service detached before able to query", e);
return null;
} catch (RemoteException | RuntimeException e) {
Log.w(TAG, "Error when calling getSuggestion()", e);
return null;
}
}
private ServiceConnection createServiceConnection() {
return new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
if (DEBUG) {
Log.d(TAG, "Service is connected");
}
//持有远程Service对象
mRemoteService = ISuggestionService.Stub.asInterface(service);
if (mConnectionListener != null) {
mConnectionListener.onServiceConnected(); //pri suggestion 9 controller回调onServiceConnected
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (mConnectionListener != null) {
mRemoteService = null;
mConnectionListener.onServiceDisconnected();
}
}
};
}
8、SettingsIntelligence-SuggestionService.java
该Service会通过SuggestionFeatureProvider获取List<Suggestion>数据
public List<Suggestion> onGetSuggestions() {final List<Suggestion> list = FeatureFactory.get(this) .suggestionFeatureProvider() .getSuggestions(this);
9、SuggestionFeatureProvider.java
调用SuggestionParser返回List<Suggestion>数据
SuggestionFeatureProvider.java
public List<Suggestion> getSuggestions(Context context) { //pri suggestion 11
final SuggestionParser parser = new SuggestionParser(context);
final List<Suggestion> list = parser.getSuggestions(); //获取Suggestion数据
final List<Suggestion> rankedSuggestions = getRanker(context).rankRelevantSuggestions(list);
final SuggestionEventStore eventStore = SuggestionEventStore.get(context);
for (Suggestion suggestion : rankedSuggestions) {
eventStore.writeEvent(suggestion.getId(), SuggestionEventStore.EVENT_SHOWN);
}
return rankedSuggestions;
}
10、SuggestionParser.java
从Manifest中读取查询相关数据
SuggestionParser.java
public List<Suggestion> getSuggestions() { //pri suggestion 11.2
final SuggestionListBuilder suggestionBuilder = new SuggestionListBuilder();
for (SuggestionCategory category : CATEGORIES) { //CATEGORIES中存放了Suggestion category信息,可获取配置了该category的Activity
if (category.isExclusive() && !isExclusiveCategoryExpired(category)) {
// If suggestions from an exclusive category are present, parsing is stopped
// and only suggestions from that category are displayed. Note that subsequent
// exclusive categories are also ignored.
// Read suggestion and force ignoreSuggestionDismissRule to be false so the rule
// defined from each suggestion itself is used.
//读取数据readSuggestions
final List<Suggestion> exclusiveSuggestions =
readSuggestions(category, false /* ignoreDismissRule */); //pri suggestion 12
if (!exclusiveSuggestions.isEmpty()) {
suggestionBuilder.addSuggestions(category, exclusiveSuggestions);
return suggestionBuilder.build();
}
} else {
// Either the category is not exclusive, or the exclusiveness expired so we should
// treat it as a normal category.
final List<Suggestion> suggestions =
readSuggestions(category, true /* ignoreDismissRule */);
suggestionBuilder.addSuggestions(category, suggestions);
}
}
return suggestionBuilder.build();
}
@VisibleForTesting
List<Suggestion> readSuggestions(SuggestionCategory category, boolean ignoreDismissRule) { //pri suggestion 12.1
final List<Suggestion> suggestions = new ArrayList<>();
final Intent probe = new Intent(Intent.ACTION_MAIN);
probe.addCategory(category.getCategory());
List<ResolveInfo> results = mPackageManager
.queryIntentActivities(probe, PackageManager.GET_META_DATA); //从Manifest中读取查询相关信息
// Build a list of eligible candidates
final List<CandidateSuggestion> eligibleCandidates = new ArrayList<>();
for (ResolveInfo resolved : results) {
final CandidateSuggestion candidate = new CandidateSuggestion(mContext, resolved, //pri suggestion 13.1
ignoreDismissRule);
if (!candidate.isEligible()) { //pri suggestion 13 判断是否符合条件 会判断是否为system app
continue;
}
eligibleCandidates.add(candidate);
}
// Then remove completed ones
final List<CandidateSuggestion> incompleteSuggestions = CandidateSuggestionFilter
.getInstance()
.filterCandidates(mContext, eligibleCandidates);
// Convert the rest to suggestion.
for (CandidateSuggestion candidate : incompleteSuggestions) {
final String id = candidate.getId();
Suggestion suggestion = mAddCache.get(id);
if (suggestion == null) {
suggestion = candidate.toSuggestion();
mAddCache.put(id, suggestion);
}
if (!suggestions.contains(suggestion)) {
suggestions.add(suggestion);
}
}
return suggestions;
}
11、CandidateSuggestion.java &DismissedChecker.java
是否显示的判定条件
CandidateSuggestion.java
public CandidateSuggestion(Context context, ResolveInfo resolveInfo,
boolean ignoreAppearRule) {
...
mIsEligible = initIsEligible(); //pri suggestion 13.2
}
private boolean initIsEligible() {
if (!ProviderEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
return false;
}
if (!ConnectivityEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
return false;
}
if (!FeatureEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
return false;
}
if (!AccountEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
return false;
}
//判定条件
if (!DismissedChecker.isEligible(mContext, mId, mResolveInfo, mIgnoreAppearRule)) {
return false;
}
if (!AutomotiveEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
return false;
}
return true;
}
DismissedChecker.java
private static final int DEFAULT_FIRST_APPEAR_DAY = 0;
@VisibleForTesting
static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss"; //pri suggestion 11 如果配置 0,则会立即显示,配置其它数字则在对应天数后显示
public static boolean isEligible(Context context, String id, ResolveInfo info, //pri suggestion 13.4 判断是否符合条件,符合的建议显示出来
boolean ignoreAppearRule) {
final SuggestionFeatureProvider featureProvider = FeatureFactory.get(context)
.suggestionFeatureProvider();
//会将第一次调用的时间保存在sharedPreferences数据中,后续会根据和这个值的差值来判断过了多少天,以此为显示依据
final SharedPreferences prefs = featureProvider.getSharedPrefs(context);
final long currentTimeMs = System.currentTimeMillis();
final String keySetupTime = id + SETUP_TIME;
if (!prefs.contains(keySetupTime)) {
prefs.edit()
.putLong(keySetupTime, currentTimeMs)
.apply();
}
// Check if it's already manually dismissed
final boolean isDismissed = featureProvider.isSuggestionDismissed(context, id); //判断是否为Dismissed状态
if (isDismissed) {
return false;
}
// Parse when suggestion should first appear. Hide suggestion before then.
int firstAppearDay = ignoreAppearRule
? DEFAULT_FIRST_APPEAR_DAY //为0时则直接显示
: parseAppearDay(info); //获取dismiss时间
long setupTime = prefs.getLong(keySetupTime, 0);
if (setupTime > currentTimeMs) {
// SetupTime is the future, user's date/time is probably wrong at some point.
// Force setupTime to be now. So we get a more reasonable firstAppearDay.
setupTime = currentTimeMs;
}
final long firstAppearDayInMs = getFirstAppearTimeMillis(setupTime, firstAppearDay);
if (currentTimeMs >= firstAppearDayInMs) {
// Dismiss timeout has passed, undismiss it.
featureProvider.markSuggestionNotDismissed(context, id);
return true;
}
return false;
}
/**
* Parse the first int from a string formatted as "0,1,2..."
* The value means suggestion should first appear on Day X.
*/
private static int parseAppearDay(ResolveInfo info) {
if (!info.activityInfo.metaData.containsKey(META_DATA_DISMISS_CONTROL)) {
return 0;
}
final Object firstAppearRule = info.activityInfo.metaData
.get(META_DATA_DISMISS_CONTROL);
if (firstAppearRule instanceof Integer) { //计算配置时间,int则直接返回,String则返回第一个
return (int) firstAppearRule;
} else {
try {
final String[] days = ((String) firstAppearRule).split(",");
return Integer.parseInt(days[0]);
} catch (Exception e) {
Log.w(TAG, "Failed to parse appear/dismiss rule, fall back to 0");
return 0;
}
}
}