Settings首页Suggestion菜单加载流程

目录

一、概述

二、Suggestion常见问题

1、如何打开原生Suggestion菜单

2、原生默认有哪些Suggestion菜单

3、如何配置Suggestion菜单显示时机

三、Suggestion菜单加载流程分析

1、设置首页SettingsHomepageActivity.java

2、ContextualCardsFragment.java

3、ContextualCardManager.java

4、ContextualCardsAdapter.java

5、ControllerRendererPool.java

6、LegacySuggestionContextualCardController.java

7、SuggestionController.java

8、SettingsIntelligence-SuggestionService.java

9、SuggestionFeatureProvider.java

10、SuggestionParser.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;
            }
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值