美文网首页Android开发Android进阶之路
Android 13 蓝牙等快捷开关更新流程

Android 13 蓝牙等快捷开关更新流程

作者: 孤街酒客0911 | 来源:发表于2022-12-09 17:47 被阅读0次

    学习笔记:


    在这篇文章之前,有时候就纠结:快捷面板的点击事件是怎样的。

    点击后快捷面板图标怎么更改?

    下面以蓝牙 BluetoothTile 为例进行分析。

    BluetoothTile

    // BluetoothTile.java
    
    public class BluetoothTile extends QSTileImpl<BooleanState> {
        private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
    
        private final BluetoothController mController;
    
        @Inject
        public BluetoothTile(
                QSHost host,
                @Background Looper backgroundLooper,
                @Main Handler mainHandler,
                FalsingManager falsingManager,
                MetricsLogger metricsLogger,
                StatusBarStateController statusBarStateController,
                ActivityStarter activityStarter,
                QSLogger qsLogger,
                BluetoothController bluetoothController
        ) {
            super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                    statusBarStateController, activityStarter, qsLogger);
            mController = bluetoothController;
            mController.observe(getLifecycle(), mCallback);
        }
    
        @Override
        public BooleanState newTileState() {
            return new BooleanState();
        }
    
        // 处理点击事件;打开/关闭的动画也在这里调用;
        @Override
        protected void handleClick(@Nullable View view) {
            // 获取蓝牙此时的状态
            final boolean isEnabled = mState.value;
            // 刷新视图状态
            refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
            // 设置蓝牙状态
            mController.setBluetoothEnabled(!isEnabled);
        }
    
        @Override
        public Intent getLongClickIntent() {
            // 返回长按的意图,
            return new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
        }
        // 目前不知道,没调用这里
        @Override
        protected void handleSecondaryClick(@Nullable View view) {
            if (!mController.canConfigBluetooth()) {
                mActivityStarter.postStartActivityDismissingKeyguard(
                        new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
                return;
            }
            if (!mState.value) {
                mController.setBluetoothEnabled(true);
            }
        }
        // 返回 title 名字
        @Override
        public CharSequence getTileLabel() {
            return mContext.getString(R.string.quick_settings_bluetooth_label);
        }
    
        // 处理更新状态,点击后会回调到这里
        @Override
        protected void handleUpdateState(BooleanState state, Object arg) {
            // 省略部分代码......
        }
    
         // 获取 二级标签 描述
        @Nullable
        private String getSecondaryLabel(boolean enabled, boolean connecting, boolean connected,
                boolean isTransient) {
            if (connecting) {
                return mContext.getString(R.string.quick_settings_connecting);
            }
            if (isTransient) {
                return mContext.getString(R.string.quick_settings_bluetooth_secondary_label_transient);
            }
    
            List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices();
            if (enabled && connected && !connectedDevices.isEmpty()) {
    
                    // 省略部分代码......
                } else {
                    // 省略部分代码......
                }
            }
    
            return null;
        }
    
        @Override
        public int getMetricsCategory() {
            return MetricsEvent.QS_BLUETOOTH;
        }
        // 是否可用
        @Override
        public boolean isAvailable() {
            return mController.isBluetoothSupported();
        }
    
        // 蓝牙状态更改回调
        private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
            @Override
            public void onBluetoothStateChange(boolean enabled) {
                refreshState();
            }
    
            @Override
            public void onBluetoothDevicesChanged() {
                refreshState();
            }
        };
    
        // 蓝牙连接时的图标
        private class BluetoothConnectedTileIcon extends Icon {
    
            BluetoothConnectedTileIcon() {
                // Do nothing. Default constructor to limit visibility.
            }
    
            @Override
            public Drawable getDrawable(Context context) {
                // This method returns Pair<Drawable, String> - the first value is the drawable.
                return context.getDrawable(R.drawable.ic_bluetooth_connected);
            }
        }
    }
    

    BluetoothTile 主要还是实现了 QSTileImpl里面的一些抽象方法:

    // QSTileImpl.java
    
        /**
         * Provides a new {@link TState} of the appropriate type to use between this tile and the
         * corresponding view.
         *
         * @return new state to use by the tile.
         */
        public abstract TState newTileState();
    
        /**
         * Handles clicks by the user.
         *
         * Calls to the controller should be made here to set the new state of the device.
         *
         * @param view The view that was clicked.
         */
        protected abstract void handleClick(@Nullable View view);
    
        /**
         * Update state of the tile based on device state
         *
         * Called whenever the state of the tile needs to be updated, either after user
         * interaction or from callbacks from the controller. It populates {@code state} with the
         * information to display to the user.
         *
         * @param state {@link TState} to populate with information to display
         * @param arg additional arguments needed to populate {@code state}
         */
        abstract protected void handleUpdateState(TState state, Object arg);
    
    

    在这里主要分析点击事件,以 handleClick(@Nullable View view) 为例。当点击 title 时,就会调用 QSTileImpl 的 click(@Nullable View view) 方法,通过 Handler 发送消息,

    // QSTileImpl.java
        public void click(@Nullable View view) {
            mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
                    .addTaggedData(FIELD_STATUS_BAR_STATE,
                            mStatusBarStateController.getState())));
            mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
                    getInstanceId());
            mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
            if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
                mHandler.obtainMessage(H.CLICK, view).sendToTarget();
            }
        }
    
    
    
            @Override
            public void handleMessage(Message msg) {
                String name = null;
                try {
    
                    // 省略部分代码......
    
                     else if (msg.what == CLICK) {
                        name = "handleClick";
                        //  根据状态策略进行判断
                        if (mState.disabledByPolicy) {
                            Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
                                    mContext, mEnforcedAdmin);
                            mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
                        } else {
                            // 我们关注这里,这里将回调到 BluetoothTile 里的 handleClick() 方法。
                            handleClick((View) msg.obj);
                        }
                    } 
                    // 省略部分代码......
    
                } catch (Throwable t) {
                    final String error = "Error in " + name;
                    Log.w(TAG, error, t);
                    mHost.warn(error, t);
                }
            }
    
    

    下面一起看 BluetoothTile#handleClick() 方法:

    // BluetoothTile.java
    
        @Override
        protected void handleClick(@Nullable View view) {
            // 获取状态
            final boolean isEnabled = mState.value;
            // 刷新视图
            refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
            // 设置蓝牙状态
            mController.setBluetoothEnabled(!isEnabled);
        }
    

    在这里主要关注 refreshState() 方法,该方法是父类 QSTileImpl 的方法:
    QSTileImpl#refreshState()

    // QSTileImpl.java
    
        protected final void refreshState(@Nullable Object arg) {
            mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
        }
    

    这里又是通过 Handler 发送消息,将会调用里面的 handleRefreshState(msg.obj) 方法;
    这里看 QSTileImpl#handleRefreshState() 方法:

    // QSTileImpl.java
    
        protected final void handleRefreshState(@Nullable Object arg) {
            // 这里重点关注,处理状态更新
            handleUpdateState(mTmpState, arg);
            boolean changed = mTmpState.copyTo(mState);
            if (mReadyState == READY_STATE_READYING) {
                mReadyState = READY_STATE_READY;
                changed = true;
            }
            // 有改变
            if (changed) {
                mQSLogger.logTileUpdated(mTileSpec, mState);
                // 这里重点关注,处理状态改变
                handleStateChanged();
            }
            // 省略部分代码......
        }
    

    handleUpdateState() 是一个抽象方法给,所以会回调到 BluetoothTile 中的 handleUpdateState() 方法;
    一起看 BluetoothTile#handleUpdateState():

    // BluetoothTile.java
        // 该方法就是在点击后 设置图标、描述 等一些状态
        @Override
        protected void handleUpdateState(BooleanState state, Object arg) {
            checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH);
            final boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
            final boolean enabled = transientEnabling || mController.isBluetoothEnabled();
            final boolean connected = mController.isBluetoothConnected();
            final boolean connecting = mController.isBluetoothConnecting();
            state.isTransient = transientEnabling || connecting ||
                    mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON;
            state.dualTarget = true;
            state.value = enabled;
            if (state.slash == null) {
                state.slash = new SlashState();
            }
            state.slash.isSlashed = !enabled;
            state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
            state.secondaryLabel = TextUtils.emptyIfNull(
                    getSecondaryLabel(enabled, connecting, connected, state.isTransient));
            state.contentDescription = mContext.getString(
                    R.string.accessibility_quick_settings_bluetooth);
            state.stateDescription = "";
            if (enabled) {
                if (connected) {
                    state.icon = new BluetoothConnectedTileIcon();
                    if (!TextUtils.isEmpty(mController.getConnectedDeviceName())) {
                        state.label = mController.getConnectedDeviceName();
                    }
                    state.stateDescription =
                            mContext.getString(R.string.accessibility_bluetooth_name, state.label)
                                    + ", " + state.secondaryLabel;
                } else if (state.isTransient) {
                    state.icon = ResourceIcon.get(
                            com.android.internal.R.drawable.ic_bluetooth_transient_animation);
                    state.stateDescription = state.secondaryLabel;
                } else {
                    state.icon =
                            ResourceIcon.get(com.android.internal.R.drawable.ic_qs_bluetooth);
                    state.stateDescription = mContext.getString(R.string.accessibility_not_connected);
                }
                state.state = Tile.STATE_ACTIVE;
            } else {
                state.icon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_bluetooth);
                state.state = Tile.STATE_INACTIVE;
            }
            state.dualLabelContentDescription = mContext.getResources().getString(
                    R.string.accessibility_quick_settings_open_settings, getTileLabel());
            state.expandedAccessibilityClassName = Switch.class.getName();
        }
    

    在 QSTileImpl#handleRefreshState() 方法里紧接着会调用 handleStateChanged() 方法。
    一起看看 QSTileImpl#handleStateChanged():

    // QSTileImpl.java
    
        private void handleStateChanged() {
            if (mCallbacks.size() != 0) {
                for (int i = 0; i < mCallbacks.size(); i++) {
                    // 找到对应的回调,更新状态
                    mCallbacks.get(i).onStateChanged(mState);
                }
            }
        }
    

    这个回调将会到 QSPanel 类里面。
    一起看看 QSPanel#addTile():

    // QSPanel.java
    
        void addTile(QSPanelControllerBase.TileRecord tileRecord) {
            final QSTile.Callback callback = new QSTile.Callback() {
                @Override
                public void onStateChanged(QSTile.State state) {
                    drawTile(tileRecord, state);
                }
            };
            // 添加回调 
            tileRecord.tile.addCallback(callback);
            tileRecord.callback = callback;
            tileRecord.tileView.init(tileRecord.tile);
            tileRecord.tile.refreshState();
    
            if (mTileLayout != null) {
                mTileLayout.addTile(tileRecord);
            }
        }
    

    addTile() 方法是每个 title 创建时调用的,这里会为每个 title 添加一个 Callback。

    当状态改变时,就会回调到上述位置,跟进去将会到 QSTileViewImpl.kt 里的 onStateChanged(state: QSTile.State) 方法。

    接着看 QSTileViewImpl#onStateChanged()

    // QSTileViewImpl.kt
    
        override fun onStateChanged(state: QSTile.State) {
            post {
                handleStateChanged(state)
            }
        }
    
        // 处理状态变化相关方法
        protected open fun handleStateChanged(state: QSTile.State) {
    
            // 省略部分代码......
    
            // 设置图标,这里会回调到 QSIconViewImpl.java 里。
            icon.setIcon(state, allowAnimations)
            // 获取 内容说明
            contentDescription = state.contentDescription
    
            // State handling and description
            val stateDescription = StringBuilder()
            // 状态文本,这里我打印过:已关闭、不可用、已开启 等这种文本。
            val stateText = getStateText(state)
            if (!TextUtils.isEmpty(stateText)) {
                stateDescription.append(stateText)
                if (TextUtils.isEmpty(state.secondaryLabel)) {
                    // 设置二级标签 文本;二级标签 应该是 title 的状态
                    state.secondaryLabel = stateText
                }
            }
            if (!TextUtils.isEmpty(state.stateDescription)) {
                stateDescription.append(", ")
                stateDescription.append(state.stateDescription)
                if (lastState != INVALID && state.state == lastState &&
                        state.stateDescription != lastStateDescription) {
                    stateDescriptionDeltas = state.stateDescription
                }
            }
    
            setStateDescription(stateDescription.toString())
            lastStateDescription = state.stateDescription
    
            accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
                null
            } else {
                state.expandedAccessibilityClassName
            }
    
            if (state is BooleanState) {
                val newState = state.value
                if (tileState != newState) {
                    tileState = newState
                }
            }
            //
    
            // Labels
            if (!Objects.equals(label.text, state.label)) {
                // 本文是以蓝牙为例,所以这里是:state.labe 是 ‘蓝牙’
                label.text = state.label
            }
    
            if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
                // 设置 二级标签的文本,蓝牙的二级标签为:‘已关闭’ 或 ‘已开启’
                secondaryLabel.text = state.secondaryLabel
                // 判断二级标签是否显示,并设置状态否显示
                secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
                    GONE
                } else {
                    VISIBLE
                }
            }
    
            // 设置颜色,这里我去掉后,所有 title 不管是否被点击,都是白色的。
            if (state.state != lastState) {
                singleAnimator.cancel()
                if (allowAnimations) {
                    singleAnimator.setValues(
                            colorValuesHolder(
                                    BACKGROUND_NAME,
                                    paintColor,
                                    getBackgroundColorForState(state.state)
                            ),
                            colorValuesHolder(
                                    LABEL_NAME,
                                    label.currentTextColor,
                                    getLabelColorForState(state.state)
                            ),
                            colorValuesHolder(
                                    SECONDARY_LABEL_NAME,
                                    secondaryLabel.currentTextColor,
                                    getSecondaryLabelColorForState(state.state)
                            ),
                            colorValuesHolder(
                                    CHEVRON_NAME,
                                    chevronView.imageTintList?.defaultColor ?: 0,
                                    getChevronColorForState(state.state)
                            )
                        )
                    singleAnimator.start()
                } else {
                    setAllColors(
                        getBackgroundColorForState(state.state),
                        getLabelColorForState(state.state),
                        getSecondaryLabelColorForState(state.state),
                        getChevronColorForState(state.state)
                    )
                }
            }
    
            // Right side icon
            loadSideViewDrawableIfNecessary(state)
    
            label.isEnabled = !state.disabledByPolicy
            // 把当前状态设置成上一状态。
            lastState = state.state
        }
    

    至此分析完成,

    留一个问题: QSTileViewImpl#onStateChanged() 方法中设置图标的时 icon.setIcon(state, allowAnimations),这句话 icon 对象是怎么来的?

    相关文章

      网友评论

        本文标题:Android 13 蓝牙等快捷开关更新流程

        本文链接:https://www.haomeiwen.com/subject/azjnfdtx.html