Android 13 蓝牙等快捷开关更新流程
学习笔记:
在这篇文章之前,有时候就纠结:快捷面板的点击事件是怎样的。
点击后快捷面板图标怎么更改?
下面以蓝牙 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
对象是怎么来的?