android13(T) Settings 主页面 Suggestion 菜单源码分析

news2024/11/17 12:25:31

1、什么是 Suggestion 菜单

呐,下面这个就是 Suggestion 菜单,一般出现在设置主界面最上方位置。

pC61FA0.png

出现时机需要满足三个条件,1、设备不是 LowRam 设备 2、启用 settings_contextual_home 特性 3、在开机一定时间后(一般是几天,具体看 AndroidManifest.xml 中的熟悉配置)

你是不是在想我是怎么知道的这么清楚的,把加载流程搞懂你就和我一样清楚了,走起。

1.1、Suggestion 定义配置

<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 菜单加载流程

先上一张经典流程图

pCcpf3T.jpg

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;


 @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();
        }
    }

找到关键点通过回调 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);
        }
    }
	
	 @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);
        }
    }

进入 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 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() {

    }
	

SuggestionFeatureProviderImpl.java 中要绑定的 Service 对应 ComponentName

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

        <service
            android: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() {

            @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 进入 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 进入 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";

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

通过 FeatureFactoryImpl.java 实例化 SuggestionFeatureProvider

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

里面包含的 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
     */
    @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;
    }
}

至此,整个加载流程解析完毕

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/727381.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

山西电力市场日前价格预测【2023-07-08】

日前价格预测 预测明日&#xff08;2023-07-08&#xff09;山西电力市场全天平均日前电价为341.87元/MWh。其中&#xff0c;最高日前电价为871.53元/MWh&#xff0c;预计出现在22: 15。最低日前电价为143.16元/MWh&#xff0c;预计出现在13: 30。以上预测仅供学习参考&#xff…

缓存设计(本地缓存 + 分布式缓存)

缓存设计 前言正文缓存对象缓存服务缓存策略本地缓存Guava的使用 分布式缓存Redis缓存分布式缓存的生命周期分布式缓存的一致性问题 源码解读从缓存中获取秒杀品 分布式锁 总结参考链接 前言 大家好&#xff0c;我是练习两年半的Java练习生&#xff0c;本篇文章会分析秒杀系统…

el-form实现其中一个填写即可的校验

<el-formref"form":model"formData":rules"formRules"label-width"130px"><el-row :gutter"24"><el-col :span"12"><el-form-item label"司机姓名 :" prop"driverName"…

【贪心+最小子段和】EDU151 D

Problem - D - Codeforces 题意&#xff1a; 思路&#xff1a; 首先K是1e18的范围&#xff0c;不能去枚举&#xff0c;那么就去考虑猜测结论 手推样例&#xff1a; 初步可以猜测&#xff0c;K应该取的是某个峰值 结论是&#xff0c;K应该取最小子段和的左端点 因为当前缀和…

【Qt QML入门】第一个Quick应用

运行结果&#xff1a; 打开Qt Creator&#xff0c;创建一个Qt Quick Qpplication&#xff0c;IDE为我们创建一个应用工程&#xff0c;其中包含如下文件&#xff1a; .pro工程文件&#xff0c;我们通过它来打开整个工程&#xff1a; QT quick# You can make your code fail to…

这个618,项目经理竟然只能买它

早上好&#xff0c;我是老原。 转眼就来到了2023年年中&#xff0c;你们的个人成长计划启动了吗&#xff1f; 比如读书计划。 最近有不少粉丝朋友私信老原&#xff0c;希望能推荐一些可以帮自己“进化”的神作。 每个人的基础不同&#xff0c;想要“进化”还是得对症下药才…

C# --- 类型安全 与 var关键字

C# --- 类型安全 与 var关键字 什么是类型安全var关键字 什么是类型安全 类型安全就是编译器在编译阶段会检查变量内容和变量类型是否匹配, 如果不匹配会抛出错误类型安全的语言包括Java, C, C#等类型不安全的语言有JavaScript 下面这段代码是JavaScript, 编译器不会进行类型检…

git在工作中如何搭建和运用(巨详细!!)

最近有点闲&#xff0c;出一版git在实际公司上的一些运用 1&#xff0c;下载git&#xff0c; 下载git就不多说了&#xff0c;官方上下载安装就好了。 2&#xff0c;初始化 下载安装完成后&#xff0c;找个项目的空文件夹进去&#xff0c;右键点击git bash here &#xff0c;…

servlet和form和session表单实现最简单的登录跳转功能(详解,文末付源码)

目录 第一步&#xff1a;配置环境 在pom.xml引入servlet等依赖 这段代码赋值粘贴进web.xml 第二步&#xff1a;编写前端html的form表单 html代码&#xff08;复制这个&#xff09; 第三步&#xff1a;编写登录的java loginservlet代码&#xff08;复制这个&#xff09; 解释…

vue使用element plus引入ElMessage样式失效的问题

样式失效如图&#xff1a; 我使用的是按需引用&#xff0c;所以在main.js中直接导入下面样式就行&#xff1a; import element-plus/theme-chalk/index.css

Luogu P1280.尼克的任务

Luogu P1280.尼克的任务 原题点这里 思路 方法一&#xff1a;动态规划 这是一道动态规划的题目。 步骤主要分 5 5 5 步&#xff1a; 状态的定义转移式的推到递推顺序的判定边界的确定结果的输出 下面&#xff0c;我们针对这道题&#xff0c;细细地讲解一下每一个步骤 一…

MYSQL单表数据量达到多少时性能会严重下降的问题探讨!

不知从什么时候开始&#xff0c;有着MySQL单表数据量超过2000万性能急剧下降的说法。 在中国互联网技术圈流传着这么一个说法&#xff1a;MySQL 单表数据量大于 2000 万行&#xff0c;性能会明显下降。事实上&#xff0c;这个传闻据说最早起源于百度。具体情况大概是这样的&am…

PS 魔棒选区工具使用方法

我们现在PS中打开一个项目 然后 如下图 在工具左侧 选择魔棒工具 选择魔棒工具之后 我们的鼠标会变成像一个魔法棒一样的东西 我们拿着魔棒工具 在下图指向位置点一下 就可以看到 它在我们整个图上生成了一些选区 这个工具本身也带有一些色彩识别的功能 就相当于 你点的这…

【MySQL系列】MySQL库的学习及基本操作(增删查改)

「前言」文章内容大致是数据库的基本操作 「归属专栏」MySQL 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「句子分享」 哪里会有人喜欢孤独&#xff0c;不过是不喜欢失望罢了。 ——村上春树《挪威的森林》 目录 一、创建/查看数据库二、删除数据库三、…

Java8 lambda 表达式 forEach 如何提前终止?

首先&#xff0c;让我们看一下Java 8中如何使用forEach()方法。forEach()方法接受一个Consumer接口作为参数&#xff0c;该接口定义了一个accept()方法&#xff0c;该方法接受一个对象并对其执行一些操作。因此&#xff0c;我们可以通过Lambda表达式来实现Consumer接口。下面是…

Camtasia2023中文版电脑屏幕记录和课件制作工具

TechSmith Camtasia是一个非常容易使用的电脑屏幕记录和课件制作工具。Camtasia 2023软件集强大的录屏、视频编辑编辑、视频菜单制作、视频影院和视频播放功能于一体&#xff0c;可以轻松制作各种教学课件、微课堂等。Camtasia 2023支持一键录制和共享高质量截屏视频&#xff0…

Pytorch: 数据读取机制Dataloader与Dataset

文章和代码已经归档至【Github仓库&#xff1a;https://github.com/timerring/dive-into-AI 】或者公众号【AIShareLab】回复 pytorch教程 也可获取。 文章目录 数据读取机制Dataloader与DatasetDataLoader 与 Datasettorch.utils.data.DataLoader区分Epoch、Iteration、Batchs…

2022年真题 - 18 - 磁盘快照

磁盘快照 题目新增 15 G 的磁盘创建 LVM 卷挂载 LVM 卷写入文本创建快照挂载快照验证配置题目 StorageSrv - 磁盘快照 新增 15G 的磁盘,并将其做成 LVM 卷,VG 名称为 snapvg,LV名称为 snaplv 大小为 5G,挂载至 /snapdata 目录下;写入文本的文件数据至 /snapdata 目录下,…

2.5 DNS 应用 -- 2. DNS记录和消息格式

2.5 DNS 应用 -- 2. DNS记录和消息格式 DNS记录DNS协议与消息如何注册域名&#xff1f; DNS记录 资源记录(RR, resource records)TypeA Name: 主机域名Value: IP地址 TypeNS Name: 域(edu.cn)Value: 该域权威域名解析服务器的主机域名 TypeCNAME Name: 某一真实域名的别名 www.…

2.常见网页布局

2.1常见网页布局 第一种 第二种 第三种&#xff08;最常见的&#xff09; 代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-wi…