Android13(T) Settings main page Suggestion menu source code analysis

1. What is the Suggestion menu?

Now, the following is the Suggestion menu, which usually appears at the top of the main setting interface.

pC61FA0.png

The timing needs to meet three conditions, 1. The device is not a LowRam device 2. The settings_contextual_home feature is enabled 3. After a certain period of time after booting (usually a few days, see the familiar configuration in AndroidManifest.xml)

Are you wondering how I know it so clearly? If you understand the loading process, you will be as clear as I am. Let's go.

1.1, Suggestion definition configuration

<activity
            android: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 menu loading process

First, a classic flow chart

pCcpf3T.jpg

2.1 Cut in from Settings

As we all know, the main entry interface of Settings is in SettingsHomepageActivity.java, find our concerned code as follows

The layout file settings_homepage_container.xml will not be mentioned, LinearLayout contains two 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(() -> {
    
    

See the above key points, isLowRamDevice and CONTEXTUAL_HOME are judged, and when the requirements are met at the same time, initialize ContextualCardsFragment to replace main_content

Next follow up ContextualCardsFragment.java to see that the corresponding layout file settings_homepage.xml contains a FocusRecyclerView,

This is easy to understand why the Suggestions displayed are all one by one

 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;
    }

Since it is RecyclerView, we only need to pay attention to the corresponding adapter to know the source of the data. Follow up ContextualCardsAdapter.java

First find the getItemCount method, the corresponding data source collection is mContextualCards, see how to add

final List<ContextualCard> mContextualCards;


 @Override
    public int getItemCount() {
    
    
        return mContextualCards.size();
    }

  @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);
            diffResult.dispatchUpdatesTo(this);
        }

        if (mRecyclerView != null && previouslyEmpty && !nowEmpty) {
    
    
            // Adding items to empty list, should animate.
            mRecyclerView.scheduleLayoutAnimation();
        }
    }

Find the key point and return the ContextualCard collection through the callback onContextualCardUpdated(), search the callback source globally in Settings, and find

LegacySuggestionContextualCardController.java:174: () -> mCardUpdateListener.onContextualCardUpdated(suggestionCards));
ConditionContextualCardController.java:111: mListener.onContextualCardUpdated(conditionalCards
ContextualCardManager.java:228: mListener.onContextualCardUpdated(cardsToUpdate);

Three places, after analysis and filtering (filtering by ContextualCard.CardType.DEFAULT), ConditionContextualCardController does not meet the conditions,

LegacySuggestionContextualCardController onContextualCardUpdated -> ContextualCardManager onContextualCardUpdated -> ContextualCardsAdapter onContextualCardUpdated

Enter ContextualCardManager.java, the key codes are listed below. Specify the type as LEGACY_SUGGESTION through setupController to initialize LegacySuggestionContextualCardController

And set setCardUpdateListener, when LegacySuggestionContextualCardController gets the data, directly call back onContextualCardUpdated to filter

 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);
        }
    }
	
	 @Override
    public 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 data
        mContextualCards.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);
        }
    }

Enter ControllerRendererPool.java, instantiate Controller through getController()

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;
    }

Set LOOKUP_TABLE is initialized in ContextualCardLookupTable.java, matched by 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));	

Let’s take a look at the key class LegacySuggestionContextualCardController.java and from here it extends to the other three sub-modules SettingsLib frameworks SettingsIntelligence

First see that there is a default configuration value config_use_legacy_suggestion in the construction method, whether to enable the suggestion function, if you do not need this function, just change it to flase

Then get ComponentName and create SuggestionController, and perform bindService operation in SuggestionController

When the Service is successfully bound, the callback onServiceConnected() parses the Suggestion data through loadSuggestions()

	 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 ContextualCard
                for (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();
        });
    }
	
	 @Override
    public void onServiceConnected() {
    
    
        loadSuggestions();
    }

    @Override
    public void onServiceDisconnected() {
    
    

    }
	

The Service to be bound in SuggestionFeatureProviderImpl.java corresponds to ComponentName

    @Override
    public ComponentName getSuggestionServiceComponent() {
    
    
        return new ComponentName(
                "com.android.settings.intelligence",
                "com.android.settings.intelligence.suggestions.SuggestionService");
    }

packages\apps\SettingsIntelligence\AndroidManifest.xml

Declare SuggestionService BIND_SETTINGS_SUGGESTIONS_SERVICE in SettingsIntelligence

        <service
            android:name=".suggestions.SuggestionService"
            android:exported="true"
            android:permission="android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE" />

2.2 Enter SettingsLib

frameworks\base\packages\SettingsLib\src\com\android\settingslib\suggestions\SuggestionController.java

Perform binding service operations and declare the callback interface 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() {
    
    

            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
    
    
                if (DEBUG) {
    
    
                    Log.d(TAG, "Service is connected");
                }
                mRemoteService = ISuggestionService.Stub.asInterface(service);
                if (mConnectionListener != null) {
    
    
                    mConnectionListener.onServiceConnected();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
    
    
                if (mConnectionListener != null) {
    
    
                    mRemoteService = null;
                    mConnectionListener.onServiceDisconnected();
                }
            }
        };
    }

2.3 Enter the 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;

    @Override
    public IBinder onBind(Intent intent) {
    
    
        return new ISuggestionService.Stub() {
    
    
            @Override
            public List<Suggestion> getSuggestions() {
    
    
                if (DEBUG) {
    
    
                    Log.d(TAG, "getSuggestions() " + getPackageName());
                }
                return onGetSuggestions();
            }

    public abstract List<Suggestion> onGetSuggestions();

2.4 Enter Settings Intelligence

packages\apps\SettingsIntelligence\src\com\android\settings\intelligence\suggestions\SuggestionService.java

SuggestionService inherits SuggestionService from frameworks

public class SuggestionService extends android.service.settings.suggestions.SuggestionService {
    
    

    private static final String TAG = "SuggestionService";

    @Override
    public 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;
    }

Instantiate SuggestionFeatureProvider via FeatureFactoryImpl.java

    @Override
    public SuggestionFeatureProvider suggestionFeatureProvider() {
    
    
        if (mSuggestionFeatureProvider == null) {
    
    
            mSuggestionFeatureProvider = new SuggestionFeatureProvider();
        }
        return mSuggestionFeatureProvider;
    }

In fact, it is to call getSuggestions() in SuggestionFeatureProvider.java

    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

The name is reliable as soon as it sounds, parse Suggestion, traverse the CATEGORIES collection (the category type is initialized by default), and declare it in the SuggestionCategoryRegistry below

readSuggestions(category, true /* ignoreDismissRule */) Get suggestions from each category, and see the second parameter corresponding to the display rules, which will be discussed below

In readSuggestions, the intent action main category is constructed, and the entire system query matches the corresponding project through packagemanage, which is why the gms package is added

In the future, some other suggestion menus will also appear on the Settings main interface. The matching type of category is described in CATEGORIES, in Settings AndroidManifest.xml

There are many declared types in . After querying all suggestions, perform corresponding filtering and finally return the data collection suggestions to be displayed

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 candidates
        final 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 ones
        final 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

The category type contained in it can be seen in 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 has a key method, isEligible(), which is used to judge whether the conditions are met, which determines whether it can be added in readSuggestions()

    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;
    }

Here I picked a DismissedChecker.java to take a look, we need isEligible() to return true

You can see the comment, if META_DATA_DISMISS_CONTROL is configured with 0, it will be displayed immediately, and other numbers will be displayed after the corresponding number of days

In parseAppearDay(), parse the value corresponding to META_DATA_DISMISS_CONTROL, if it is an int value, it will return directly, if it is a string, it will take the first place

Get the current time and compare it with the parsing time, if >=, return true and the corresponding entry should be displayed

The ignoreAppearRule mentioned above, if it is true, ignore the META_DATA_DISMISS_CONTROL configuration rule and display it directly

/**
     * 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
     */
    @VisibleForTesting
    static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss";

    // Shared prefs keys for storing dismissed state.
    // Index into current dismissed state.
    @VisibleForTesting
    static 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 dismissed
        final 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;
    }
}

So far, the entire loading process has been parsed

Guess you like

Origin blog.csdn.net/u012932409/article/details/131594158