1、什么是 Suggestion 菜单
呐,下面这个就是 Suggestion 菜单,一般出现在设置主界面最上方位置。
出现时机需要满足三个条件,1、设备不是 LowRam 设备 2、启用 settings_contextual_home 特性 3、在开机一定时间后(一般是几天,具体看 AndroidManifest.xml 中的熟悉配置)
你是不是在想我是怎么知道的这么清楚的,把加载流程搞懂你就和我一样清楚了,走起。
1.1、Suggestion 定义配置
<activityandroid:name="Settings$NightDisplaySuggestionActivity"android:enabled="@*android:bool/config_nightDisplayAvailable"android:exported="true"android:icon="@drawable/ic_suggestion_night_display"><!-- 配置关键,可被查询到 --><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="com.android.settings.suggested.category.FIRST_IMPRESSION" /></intent-filter><!-- 配置显示时间 --><meta-data android:name="com.android.settings.dismiss"android:value="7,1,30" /><!-- 配置对应标题和内容 --><meta-data android:name="com.android.settings.title"android:resource="@string/night_display_suggestion_title" /><meta-data android:name="com.android.settings.summary"android:resource="@string/night_display_suggestion_summary" />....
2、Suggestion 菜单加载流程
先上一张经典流程图
2.1 从 Settings 切入
众所周知 Settings 主入口界面在 SettingsHomepageActivity.java 中,找到我们关注代码如下
布局文件 settings_homepage_container.xml 就不说了,LinearLayout 中包含两 FrameLayout
final String highlightMenuKey = getHighlightMenuKey();// Only allow features on high ram devices.if (!getSystemService(ActivityManager.class).isLowRamDevice()) {initAvatarView();final boolean scrollNeeded = mIsEmbeddingActivityEnabled&& !TextUtils.equals(getString(DEFAULT_HIGHLIGHT_MENU_KEY), highlightMenuKey);showSuggestionFragment(scrollNeeded);if (FeatureFlagUtils.isEnabled(this, FeatureFlags.CONTEXTUAL_HOME)) {showFragment(() -> new ContextualCardsFragment(), R.id.contextual_cards_content);((FrameLayout) findViewById(R.id.main_content)).getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);}}mMainFragment = showFragment(() -> {
看到上面关键点,isLowRamDevice 和 CONTEXTUAL_HOME 进行了判断,当同时符合要求时,初始化 ContextualCardsFragment 替换 main_content
接下来跟进 ContextualCardsFragment.java 看到对应布局文件 settings_homepage.xml 中就包含了一个 FocusRecyclerView,
这就好理解为什么看到展示的 Suggestion 都是一条一条的
public View onCreateView(LayoutInflater inflater, ViewGroup container,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);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;}
既然是 RecyclerView 那我们只需要关注对应的 adapter 就知道数据来源了,跟进 ContextualCardsAdapter.java
先找 getItemCount 方法,对应数据源集合为 mContextualCards,查看是如何 add
final List<ContextualCard> mContextualCards;@Overridepublic int getItemCount() {return mContextualCards.size();}@Overridepublic 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);diffResult.dispatchUpdatesTo(this);}if (mRecyclerView != null && previouslyEmpty && !nowEmpty) {// Adding items to empty list, should animate.mRecyclerView.scheduleLayoutAnimation();}}
找到关键点通过回调 onContextualCardUpdated() 返回 ContextualCard 集合,在 Settings 中全局搜索回调来源,找到
LegacySuggestionContextualCardController.java:174: () -> mCardUpdateListener.onContextualCardUpdated(suggestionCards));
ConditionContextualCardController.java:111: mListener.onContextualCardUpdated(conditionalCards
ContextualCardManager.java:228: mListener.onContextualCardUpdated(cardsToUpdate);
三个地方,经过分析过滤(通过 ContextualCard.CardType.DEFAULT 过滤) ,ConditionContextualCardController 不符合情况,
LegacySuggestionContextualCardController onContextualCardUpdated -> ContextualCardManager onContextualCardUpdated -> ContextualCardsAdapter onContextualCardUpdated
进入 ContextualCardManager.java,下面列出关键代码。通过 setupController 指定 type 为 LEGACY_SUGGESTION,进行初始化 LegacySuggestionContextualCardController
并设置 setCardUpdateListener,当 LegacySuggestionContextualCardController 获取到数据后直接回调 onContextualCardUpdated 进行过滤
int[] getSettingsCards() {if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlags.CONDITIONAL_CARDS)) {return new int[] {ContextualCard.CardType.LEGACY_SUGGESTION};}return new int[]{ContextualCard.CardType.CONDITIONAL, ContextualCard.CardType.LEGACY_SUGGESTION};}void setupController(@ContextualCard.CardType int cardType) {final ContextualCardController controller = mControllerRendererPool.getController(mContext,cardType);if (controller == null) {Log.w(TAG, "Cannot find ContextualCardController for type " + cardType);return;}controller.setCardUpdateListener(this);if (controller instanceof LifecycleObserver && !mLifecycleObservers.contains(controller)) {mLifecycleObservers.add((LifecycleObserver) controller);mLifecycle.addObserver((LifecycleObserver) controller);}}@Overridepublic void onContextualCardUpdated(Map<Integer, List<ContextualCard>> updateList) {final Set<Integer> cardTypes = updateList.keySet();// Remove the existing data that matches the certain cardType before inserting new data.List<ContextualCard> cardsToKeep;// We are not sure how many card types will be in the database, so when the list coming// from the database is empty (e.g. no eligible cards/cards are dismissed), we cannot// assign a specific card type for its map which is sending here. Thus, we assume that// except Conditional cards, all other cards are from the database. So when the map sent// here is empty, we only keep Conditional cards.if (cardTypes.isEmpty()) {final Set<Integer> conditionalCardTypes = new TreeSet<Integer>() {{add(ContextualCard.CardType.CONDITIONAL);add(ContextualCard.CardType.CONDITIONAL_HEADER);add(ContextualCard.CardType.CONDITIONAL_FOOTER);}};cardsToKeep = mContextualCards.stream().filter(card -> conditionalCardTypes.contains(card.getCardType())).collect(Collectors.toList());} else {cardsToKeep = mContextualCards.stream().filter(card -> !cardTypes.contains(card.getCardType())).collect(Collectors.toList());}final List<ContextualCard> allCards = new ArrayList<>();allCards.addAll(cardsToKeep);allCards.addAll(updateList.values().stream().flatMap(List::stream).collect(Collectors.toList()));//replace with the new datamContextualCards.clear();final List<ContextualCard> sortedCards = sortCards(allCards);mContextualCards.addAll(getCardsWithViewType(sortedCards));loadCardControllers();if (mListener != null) {final Map<Integer, List<ContextualCard>> cardsToUpdate = new ArrayMap<>();cardsToUpdate.put(ContextualCard.CardType.DEFAULT, mContextualCards);mListener.onContextualCardUpdated(cardsToUpdate);}}
进入 ControllerRendererPool.java,通过 getController() 实例化 Controller
public <T extends ContextualCardController> T getController(Context context,@ContextualCard.CardType int cardType) {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;}}final ContextualCardController controller = createCardController(context, clz);if (controller != null) {mControllers.add(controller);}return (T) controller;}
在 ContextualCardLookupTable.java 中初始化了 Set LOOKUP_TABLE, 通过 key CardType.LEGACY_SUGGESTION 匹配
public static Class<? extends ContextualCardController> getCardControllerClass(@CardType int cardType) {for (ControllerRendererMapping mapping : LOOKUP_TABLE) {if (mapping.mCardType == cardType) {return mapping.mControllerClass;}}return null;}static final Set<ControllerRendererMapping> LOOKUP_TABLE =new TreeSet<ControllerRendererMapping>() {{...add(new ControllerRendererMapping(CardType.LEGACY_SUGGESTION,LegacySuggestionContextualCardRenderer.VIEW_TYPE,LegacySuggestionContextualCardController.class,LegacySuggestionContextualCardRenderer.class));
来看下关键类 LegacySuggestionContextualCardController.java 从这里就延伸到了其它三个子模块 SettingsLib frameworks SettingsIntelligence
先看到构造方法中有个默认配置值 config_use_legacy_suggestion,是否启用 suggestion 功能,如果不需要该功能则直接改为 flase 就行
紧接着获取 ComponentName 并创建 SuggestionController,在 SuggestionController 中进行 bindService 操作
当 Service 成功绑定,回调 onServiceConnected() 通过 loadSuggestions() 解析 Suggestion 数据
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;}final ComponentName suggestionServiceComponent =FeatureFactory.getFactory(mContext).getSuggestionFeatureProvider(mContext).getSuggestionServiceComponent();mSuggestionController = new SuggestionController(mContext, suggestionServiceComponent, this /* listener */);}private void updateAdapter() {final Map<Integer, List<ContextualCard>> suggestionCards = new ArrayMap<>();suggestionCards.put(ContextualCard.CardType.LEGACY_SUGGESTION, mSuggestions);ThreadUtils.postOnMainThread(() -> mCardUpdateListener.onContextualCardUpdated(suggestionCards));}private void loadSuggestions() {ThreadUtils.postOnBackgroundThread(() -> {if (mSuggestionController == null || mCardUpdateListener == null) {return;}final List<Suggestion> suggestions = mSuggestionController.getSuggestions();final String suggestionCount = suggestions == null? "null": String.valueOf(suggestions.size());Log.d(TAG, "Loaded suggests: " + suggestionCount);final List<ContextualCard> cards = new ArrayList<>();if (suggestions != null) {// Convert suggestion to ContextualCardfor (Suggestion suggestion : suggestions) {final LegacySuggestionContextualCard.Builder cardBuilder =new LegacySuggestionContextualCard.Builder();if (suggestion.getIcon() != null) {cardBuilder.setIconDrawable(suggestion.getIcon().loadDrawable(mContext));}cardBuilder.setPendingIntent(suggestion.getPendingIntent()).setSuggestion(suggestion).setName(suggestion.getId()).setTitleText(suggestion.getTitle().toString()).setSummaryText(suggestion.getSummary().toString()).setViewType(LegacySuggestionContextualCardRenderer.VIEW_TYPE);cards.add(cardBuilder.build());}}mSuggestions.clear();mSuggestions.addAll(cards);updateAdapter();});}@Overridepublic void onServiceConnected() {loadSuggestions();}@Overridepublic void onServiceDisconnected() {}
SuggestionFeatureProviderImpl.java 中要绑定的 Service 对应 ComponentName
@Overridepublic ComponentName getSuggestionServiceComponent() {return new ComponentName("com.android.settings.intelligence","com.android.settings.intelligence.suggestions.SuggestionService");}
packages\apps\SettingsIntelligence\AndroidManifest.xml
在 SettingsIntelligence 中声明 SuggestionService BIND_SETTINGS_SUGGESTIONS_SERVICE
<serviceandroid:name=".suggestions.SuggestionService"android:exported="true"android:permission="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE" />
2.2 进入 SettingsLib
frameworks\base\packages\SettingsLib\src\com\android\settingslib\suggestions\SuggestionController.java
进行绑定服务操作,并声明回调接口 ServiceConnectionListener
public SuggestionController(Context context, ComponentName service,ServiceConnectionListener listener) {mContext = context.getApplicationContext();mConnectionListener = listener;mServiceIntent = new Intent().setComponent(service);mServiceConnection = createServiceConnection();}public void start() {mContext.bindServiceAsUser(mServiceIntent, mServiceConnection, Context.BIND_AUTO_CREATE,android.os.Process.myUserHandle());}public List<Suggestion> getSuggestions() {if (!isReady()) {return null;}try {return mRemoteService.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() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {if (DEBUG) {Log.d(TAG, "Service is connected");}mRemoteService = ISuggestionService.Stub.asInterface(service);if (mConnectionListener != null) {mConnectionListener.onServiceConnected();}}@Overridepublic void onServiceDisconnected(ComponentName name) {if (mConnectionListener != null) {mRemoteService = null;mConnectionListener.onServiceDisconnected();}}};}
2.3 进入 frameworks
frameworks\base\core\java\android\service\settings\suggestions\SuggestionService.java
public abstract class SuggestionService extends Service {private static final String TAG = "SuggestionService";private static final boolean DEBUG = false;@Overridepublic IBinder onBind(Intent intent) {return new ISuggestionService.Stub() {@Overridepublic List<Suggestion> getSuggestions() {if (DEBUG) {Log.d(TAG, "getSuggestions() " + getPackageName());}return onGetSuggestions();}public abstract List<Suggestion> onGetSuggestions();
2.4 进入 SettingsIntelligence
packages\apps\SettingsIntelligence\src\com\android\settings\intelligence\suggestions\SuggestionService.java
SuggestionService 继承 frameworks 中 SuggestionService
public class SuggestionService extends android.service.settings.suggestions.SuggestionService {private static final String TAG = "SuggestionService";@Overridepublic List<Suggestion> onGetSuggestions() {final long startTime = System.currentTimeMillis();final List<Suggestion> list = FeatureFactory.get(this).suggestionFeatureProvider().getSuggestions(this);final List<String> ids = new ArrayList<>(list.size());for (Suggestion suggestion : list) {ids.add(suggestion.getId());}final long endTime = System.currentTimeMillis();FeatureFactory.get(this).metricsFeatureProvider(this).logGetSuggestion(ids, endTime - startTime);return list;}
通过 FeatureFactoryImpl.java 实例化 SuggestionFeatureProvider
@Overridepublic SuggestionFeatureProvider suggestionFeatureProvider() {if (mSuggestionFeatureProvider == null) {mSuggestionFeatureProvider = new SuggestionFeatureProvider();}return mSuggestionFeatureProvider;}
其实是调用 SuggestionFeatureProvider.java 中 getSuggestions()
public List<Suggestion> getSuggestions(Context context) {final SuggestionParser parser = new SuggestionParser(context);final List<Suggestion> list = parser.getSuggestions();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;}
SuggestionParser.java
这个名字一听就靠谱了,解析 Suggestion, 遍历 CATEGORIES 集合(默认初始化了category类型),声明在下面 SuggestionCategoryRegistry 中
readSuggestions(category, true /* ignoreDismissRule */) 从每一个 category 中获取 suggestion,看第二个参数对应显示规则,下面会讲
readSuggestions 中通过构建 intent action main category,通过 packagemanage 整个系统 query 符合对应项目,这就是为什么加了 gms 包
以后 Settings 主界面也会出现一些其它 suggestion 菜单。 category 对应匹配类型就在 CATEGORIES 中描述,在 Settings AndroidManifest.xml
中就有很多声明的类型。查询到所有 suggestion 以后,再进行对应过滤最后就返回了要显示的数据集合 suggestions
public List<Suggestion> getSuggestions() {final SuggestionListBuilder suggestionBuilder = new SuggestionListBuilder();for (SuggestionCategory category : CATEGORIES) {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.final List<Suggestion> exclusiveSuggestions =readSuggestions(category, false /* ignoreDismissRule */);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();} List<Suggestion> readSuggestions(SuggestionCategory category, boolean ignoreDismissRule) {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);// Build a list of eligible candidatesfinal List<CandidateSuggestion> eligibleCandidates = new ArrayList<>();for (ResolveInfo resolved : results) {final CandidateSuggestion candidate = new CandidateSuggestion(mContext, resolved,ignoreDismissRule);if (!candidate.isEligible()) {continue;}eligibleCandidates.add(candidate);}android.util.Log.d("pppp","eligibleCandidates="+eligibleCandidates.size());// Then remove completed onesfinal List<CandidateSuggestion> incompleteSuggestions = CandidateSuggestionFilter.getInstance().filterCandidates(mContext, eligibleCandidates);android.util.Log.d("pppp","1111incompleteSuggestions="+incompleteSuggestions.size());// 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);android.util.Log.d("pppp","suggestions ="+suggestion.getTitle().toString());}android.util.Log.d("pppp","suggestions size="+suggestions.size());android.util.Log.d("pppp","suggestions ="+suggestions.contains(suggestion));if (!suggestions.contains(suggestion)) {suggestions.add(suggestion);android.util.Log.d("pppp","suggestions add=");}}return suggestions;}
SuggestionCategoryRegistry.java
里面包含的 category 类型在 Settings AndroidManifest.xml 中可看到对应
static {CATEGORIES = new ArrayList<>();CATEGORIES.add(buildCategory(CATEGORY_KEY_DEFERRED_SETUP,true /* exclusive */, 14 * DateUtils.DAY_IN_MILLIS));CATEGORIES.add(buildCategory(CATEGORY_KEY_HIGH_PRIORITY,true /* exclusive */, 3 * DateUtils.DAY_IN_MILLIS));CATEGORIES.add(buildCategory(CATEGORY_KEY_FIRST_IMPRESSION,true /* exclusive */, 14 * DateUtils.DAY_IN_MILLIS));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.LOCK_SCREEN",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.TRUST_AGENT",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.EMAIL",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.PARTNER_ACCOUNT",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.GESTURE",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.HOTWORD",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.DEFAULT",false /* exclusive */, NEVER_EXPIRE));CATEGORIES.add(buildCategory("com.android.settings.suggested.category.SETTINGS_ONLY",false /* exclusive */, NEVER_EXPIRE));}
CandidateSuggestion.java 其中有一个很关键方法 isEligible() 用于判断是否符合条件,这决定到 readSuggestions() 中能否被 add
public CandidateSuggestion(Context context, ResolveInfo resolveInfo,boolean ignoreAppearRule) {mContext = context;mIgnoreAppearRule = ignoreAppearRule;mResolveInfo = resolveInfo;mIntent = new Intent().setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);mComponent = mIntent.getComponent();mId = generateId();mIsEligible = initIsEligible();}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 看一下,我们需要其中 isEligible() 返回 true
可以看到注释,META_DATA_DISMISS_CONTROL 如果配置 0,则会立即显示,配置其它数字则在对应天数后显示
parseAppearDay() 中解析 META_DATA_DISMISS_CONTROL 对应 value 值,如果是int值则直接返回,如果是字符串则取第一位
获取当前时间和解析时间比较,>= 则返回 true 对应条目就应该显示
上面提到 ignoreAppearRule ,如果为 true 则忽略 META_DATA_DISMISS_CONTROL 配置规则,直接显示
/*** Allows suggestions to appear after a certain number of days, and to re-appear if dismissed.* For instance:* 0,10* Will appear immediately, the 10 is ignored.** 10* Will appear after 10 days*/@VisibleForTestingstatic final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss";// Shared prefs keys for storing dismissed state.// Index into current dismissed state.@VisibleForTestingstatic final String SETUP_TIME = "_setup_time";// Default dismiss rule for suggestions.private static final int DEFAULT_FIRST_APPEAR_DAY = 0;private static final String TAG = "DismissedChecker";public static boolean isEligible(Context context, String id, ResolveInfo info,boolean ignoreAppearRule) {final SuggestionFeatureProvider featureProvider = FeatureFactory.get(context).suggestionFeatureProvider();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 dismissedfinal boolean isDismissed = featureProvider.isSuggestionDismissed(context, id);if (isDismissed) {return false;}// Parse when suggestion should first appear. Hide suggestion before then.int firstAppearDay = ignoreAppearRule? DEFAULT_FIRST_APPEAR_DAY: parseAppearDay(info);Log.d(TAG, "firstAppearDay="+firstAppearDay);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);Log.d(TAG, "currentTimeMs="+currentTimeMs+" firstAppearDayInMs="+firstAppearDayInMs);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) {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;}}}private static long getFirstAppearTimeMillis(long setupTime, int daysDelay) {long days = daysDelay * DateUtils.DAY_IN_MILLIS;return setupTime + days;}
}
至此,整个加载流程解析完毕