Android系统有线设备插拔监听机制详解
概述
Android系统通过WiredAccessoryManager类来监听HDMI和有线耳机等有线设备的插拔事件。该管理器使用两种主要机制来检测设备状态变化:
- Extcon子系统 - 通过监听
/sys/class/extcon目录下的UEvent事件 - 传统Switch机制 - 通过监听
/sys/class/switch目录下的状态文件 - Input事件机制 - 通过InputManagerService监听设备输入事件
一、核心类架构
1.1 主要类关系
WiredAccessoryManager
├── WiredAccessoryObserver (传统Switch机制)
│ └── UEventObserver
└── WiredAccessoryExtconObserver (Extcon机制)
└── ExtconStateObserver<Pair<Integer, Integer>>
└── ExtconUEventObserver
└── UEventObserver
1.2 核心文件位置
- 主类 :
frameworks/base/services/core/java/com/android/server/WiredAccessoryManager.java - Extcon观察者 :
frameworks/base/services/core/java/com/android/server/ExtconUEventObserver.java - 状态观察者 :
frameworks/base/services/core/java/com/android/server/ExtconStateObserver.java
二、监听的设备类型
2.1 支持的设备类型(位掩码定义)
java
private static final int BIT_HEADSET = (1 << 0); // 带麦克风的耳机
private static final int BIT_HEADSET_NO_MIC = (1 << 1); // 不带麦克风的耳机
private static final int BIT_USB_HEADSET_ANLG = (1 << 2); // USB模拟耳机
private static final int BIT_USB_HEADSET_DGTL = (1 << 3); // USB数字耳机
private static final int BIT_HDMI_AUDIO = (1 << 4); // HDMI音频
private static final int BIT_LINEOUT = (1 << 5); // 线路输出
2.2 设备名称定义
java
private static final String NAME_H2W = "h2w"; // 有线耳机
private static final String NAME_USB_AUDIO = "usb_audio"; // USB音频
private static final String NAME_HDMI_AUDIO = "hdmi_audio";// HDMI音频
private static final String NAME_HDMI = "hdmi"; // HDMI(备用)
三、监听机制详解
3.1 Extcon子系统监听(推荐方式)
3.1.1 监听的Extcon类型
WiredAccessoryExtconObserver监听以下4种Extcon设备类型:
java
mExtconInfos = ExtconInfo.getExtconInfoForTypes(new String[] {
ExtconInfo.EXTCON_HEADPHONE, // 耳机
ExtconInfo.EXTCON_MICROPHONE, // 麦克风
ExtconInfo.EXTCON_HDMI, // HDMI
ExtconInfo.EXTCON_LINE_OUT, // 线路输出
});
3.1.2 监听的目录和文件
主目录 : /sys/class/extcon/
对于每个extcon设备(如extcon0, extcon1等),系统会:
-
读取设备类型:
- 路径:
/sys/class/extcon/extcon[X]/cable.[Y]/name - 示例:
/sys/class/extcon/extcon0/cable.0/name可能包含 "HEADPHONE"
- 路径:
-
监听设备路径:
- 通过符号链接解析实际设备路径
- 路径格式:
/devices/...(通过/sys/class/extcon/extcon[X]解析得到)
-
读取状态文件:
- 路径:
/sys/class/extcon/extcon[X]/state - 内容格式:
MICROPHONE=1\nHEADPHONE=0(Kernel 4.9+)
- 路径:
3.1.3 UEvent监听
系统通过startObserving("DEVPATH=" + devicePath)监听UEvent,当设备插拔时会收到:
UEvent内容示例:
DEVPATH=/devices/virtual/extcon/extcon0
NAME=h2w
STATE=MICROPHONE=1
HEADPHONE=1
3.1.4 状态解析逻辑
java
public Pair<Integer, Integer> parseState(ExtconInfo extconInfo, String status) {
int[] maskAndState = {0, 0}; // [0]=mask, [1]=state
// 解析HEADPHONE状态
if (extconInfo.hasCableType(ExtconInfo.EXTCON_HEADPHONE)) {
updateBit(maskAndState, BIT_HEADSET_NO_MIC, status, ExtconInfo.EXTCON_HEADPHONE);
}
// 解析MICROPHONE状态
if (extconInfo.hasCableType(ExtconInfo.EXTCON_MICROPHONE)) {
updateBit(maskAndState, BIT_HEADSET, status, ExtconInfo.EXTCON_MICROPHONE);
}
// 解析HDMI状态
if (extconInfo.hasCableType(ExtconInfo.EXTCON_HDMI)) {
updateBit(maskAndState, BIT_HDMI_AUDIO, status, ExtconInfo.EXTCON_HDMI);
}
// 解析LINE_OUT状态
if (extconInfo.hasCableType(ExtconInfo.EXTCON_LINE_OUT)) {
updateBit(maskAndState, BIT_LINEOUT, status, ExtconInfo.EXTCON_LINE_OUT);
}
return Pair.create(maskAndState[0], maskAndState[1]);
}
3.2 传统Switch机制监听(备用方式)
3.2.1 监听的Switch设备
WiredAccessoryObserver监听以下3种switch设备:
- h2w - 有线耳机
- usb_audio - USB音频设备
- hdmi_audio 或 hdmi - HDMI音频
3.2.2 监听的目录和文件
主目录 : /sys/class/switch/
对于每个switch设备:
-
状态文件路径:
- h2w:
/sys/class/switch/h2w/state - usb_audio:
/sys/class/switch/usb_audio/state - hdmi_audio:
/sys/class/switch/hdmi_audio/state - hdmi:
/sys/class/switch/hdmi/state(备用)
- h2w:
-
设备路径:
- 格式:
/devices/virtual/switch/{设备名} - 示例:
/devices/virtual/switch/h2w
- 格式:
3.2.3 状态值含义
状态文件包含一个整数值:
0- 设备未连接1- 状态1(通常表示主设备类型连接)2- 状态2(表示次设备类型连接)N- 其他状态值
每个设备的状态映射:
java
// h2w设备
new UEventInfo(NAME_H2W,
BIT_HEADSET, // state=1时的位
BIT_HEADSET_NO_MIC, // state=2时的位
BIT_LINEOUT) // state=N时的位
// usb_audio设备
new UEventInfo(NAME_USB_AUDIO,
BIT_USB_HEADSET_ANLG, // state=1
BIT_USB_HEADSET_DGTL, // state=2
0)
// hdmi_audio设备
new UEventInfo(NAME_HDMI_AUDIO,
BIT_HDMI_AUDIO, // state=1
0,
0)
3.2.4 UEvent监听
监听的UEvent格式:
DEVPATH=/devices/virtual/switch/h2w
SWITCH_NAME=h2w
SWITCH_STATE=1
3.3 Input事件机制监听
3.3.1 配置条件
当配置项config_useDevInputEventForAudioJack为true时启用。
3.3.2 监听的Switch事件
通过InputManagerService监听以下3种switch状态:
java
// 耳机插入开关
SW_HEADPHONE_INSERT (对应位: SW_HEADPHONE_INSERT_BIT)
// 麦克风插入开关
SW_MICROPHONE_INSERT (对应位: SW_MICROPHONE_INSERT_BIT)
// 线路输出插入开关
SW_LINEOUT_INSERT (对应位: SW_LINEOUT_INSERT_BIT)
3.3.3 状态读取
系统启动时通过以下方式读取初始状态:
java
int switchValues = 0;
if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_HEADPHONE_INSERT) == 1) {
switchValues |= SW_HEADPHONE_INSERT_BIT;
}
if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_MICROPHONE_INSERT) == 1) {
switchValues |= SW_MICROPHONE_INSERT_BIT;
}
if (mInputManager.getSwitchState(-1, InputDevice.SOURCE_ANY, SW_LINEOUT_INSERT) == 1) {
switchValues |= SW_LINEOUT_INSERT_BIT;
}
3.3.4 Switch组合到设备类型的映射
java
switch (mSwitchValues & (SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT)) {
case 0:
headset = 0; // 无设备
break;
case SW_HEADPHONE_INSERT_BIT:
headset = BIT_HEADSET_NO_MIC; // 仅耳机,无麦克风
break;
case SW_LINEOUT_INSERT_BIT:
headset = BIT_LINEOUT; // 线路输出
break;
case SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT:
headset = BIT_HEADSET; // 带麦克风的耳机
break;
case SW_MICROPHONE_INSERT_BIT:
headset = BIT_HEADSET; // 仅麦克风(视为耳机)
break;
default:
headset = 0;
break;
}
四、初始化流程
4.1 系统启动时的初始化顺序
java
public void systemReady() {
synchronized (mLock) {
mWakeLock.acquire();
Message msg = mHandler.obtainMessage(MSG_SYSTEM_READY, 0, 0, null);
mHandler.sendMessage(msg);
}
}
private void onSystemReady() {
// 1. 如果使用Input事件机制,先读取当前状态
if (mUseDevInputEventForAudioJack) {
// 读取所有switch状态并通知
notifyWiredAccessoryChanged(...);
}
// 2. 选择监听机制
if (ExtconUEventObserver.extconExists()) {
// 优先使用Extcon机制
if (mUseDevInputEventForAudioJack) {
Log.w(TAG, "Both input event and extcon are used for audio jack");
}
mExtconObserver.init();
} else {
// 使用传统Switch机制
mObserver.init();
}
}
4.2 Extcon Observer初始化
java
private void init() {
for (ExtconInfo extconInfo : mExtconInfos) {
// 1. 读取初始状态
Pair<Integer, Integer> state = parseStateFromFile(extconInfo);
// 2. 更新状态
if (state != null) {
updateState(extconInfo, extconInfo.getName(), state);
}
// 3. 开始监听UEvent
startObserving(extconInfo);
}
}
4.3 Switch Observer初始化
java
void init() {
synchronized (mLock) {
// 1. 读取所有switch设备的初始状态
for (int i = 0; i < mUEventInfo.size(); ++i) {
UEventInfo uei = mUEventInfo.get(i);
FileReader file = new FileReader(uei.getSwitchStatePath());
int curState = Integer.parseInt(file.read(...));
if (curState > 0) {
updateStateLocked(uei.getDevPath(), uei.getDevName(), curState);
}
}
}
// 2. 开始监听所有设备的UEvent
for (int i = 0; i < mUEventInfo.size(); ++i) {
UEventInfo uei = mUEventInfo.get(i);
startObserving("DEVPATH=" + uei.getDevPath());
}
}
五、状态更新和通知流程
5.1 状态更新流程图
设备插拔
↓
内核驱动检测
↓
更新sysfs文件 (/sys/class/extcon/*/state 或 /sys/class/switch/*/state)
↓
发送UEvent到用户空间
↓
UEventObserver接收事件
↓
ExtconStateObserver.onUEvent() 或 WiredAccessoryObserver.onUEvent()
↓
parseState() - 解析状态
↓
updateState() / updateStateLocked()
↓
updateLocked() - 更新内部状态
↓
mHandler.sendMessage(MSG_NEW_DEVICE_STATE)
↓
setDevicesState()
↓
setDeviceStateLocked() - 针对每个设备类型
↓
AudioManager.setWiredDeviceConnectionState()
↓
通知AudioService设备连接/断开
5.2 关键方法说明
5.2.1 updateLocked()
java
private void updateLocked(String newName, int newState, boolean isSynchronous) {
int headsetState = newState & SUPPORTED_HEADSETS;
// 检查状态是否真的改变
if (mHeadsetState == headsetState) {
return;
}
// 验证状态转换的有效性
// 拒绝无效的组合(如同时有多种耳机类型)
if (isSynchronous) {
setDevicesState(headsetState, mHeadsetState, "");
} else {
// 异步处理,通过Handler发送消息
Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE,
headsetState,
mHeadsetState,
"");
mHandler.sendMessage(msg);
}
mHeadsetState = headsetState;
}
5.2.2 setDeviceStateLocked()
java
private void setDeviceStateLocked(int headset, int headsetState,
int prevHeadsetState, String headsetName) {
if ((headsetState & headset) != (prevHeadsetState & headset)) {
int outDevice = 0;
int inDevice = 0;
int state = (headsetState & headset) != 0 ? 1 : 0;
// 根据设备类型映射到AudioManager的设备常量
if (headset == BIT_HEADSET) {
outDevice = AudioManager.DEVICE_OUT_WIRED_HEADSET;
inDevice = AudioManager.DEVICE_IN_WIRED_HEADSET;
} else if (headset == BIT_HEADSET_NO_MIC) {
outDevice = AudioManager.DEVICE_OUT_WIRED_HEADPHONE;
} else if (headset == BIT_LINEOUT) {
outDevice = AudioManager.DEVICE_OUT_LINE;
} else if (headset == BIT_USB_HEADSET_ANLG) {
outDevice = AudioManager.DEVICE_OUT_ANLG_DOCK_HEADSET;
} else if (headset == BIT_USB_HEADSET_DGTL) {
outDevice = AudioManager.DEVICE_OUT_DGTL_DOCK_HEADSET;
} else if (headset == BIT_HDMI_AUDIO) {
outDevice = AudioManager.DEVICE_OUT_HDMI;
}
// 通知AudioManager
if (outDevice != 0) {
mAudioManager.setWiredDeviceConnectionState(outDevice, state, "", headsetName);
}
if (inDevice != 0) {
mAudioManager.setWiredDeviceConnectionState(inDevice, state, "", headsetName);
}
}
}
六、监听机制选择逻辑
6.1 优先级顺序
系统按以下优先级选择监听机制:
- Input事件 + Extcon (如果两者都启用会有警告)
- Extcon子系统 (如果
/sys/class/extcon存在) - 传统Switch机制 (备用方案)
6.2 判断代码
java
// 检查是否使用Input事件
mUseDevInputEventForAudioJack =
context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
// 检查Extcon是否存在
public static boolean extconExists() {
File extconDir = new File("/sys/class/extcon");
return extconDir.exists() && extconDir.isDirectory();
}
// 选择机制
if (ExtconUEventObserver.extconExists()) {
if (mUseDevInputEventForAudioJack) {
Log.w(TAG, "Both input event and extcon are used for audio jack, please just choose one.");
}
mExtconObserver.init(); // 使用Extcon
} else {
mObserver.init(); // 使用Switch
}
七、HDMI设备监听特殊说明
7.1 HDMI监听的两种方式
方式1:hdmi_audio switch(推荐)
- 路径 :
/sys/class/switch/hdmi_audio/state - 优点: 仅在HDMI驱动配置了视频模式且下游设备的EDID表明支持音频时才会发出信号
- 更准确: 确保HDMI真正支持音频输出
方式2:hdmi switch(备用)
- 路径 :
/sys/class/switch/hdmi/state - 使用场景: 当内核不支持hdmi_audio时使用
- 缺点: 可能在HDMI不支持音频时也报告连接
7.2 HDMI监听代码
java
// 优先尝试hdmi_audio
uei = new UEventInfo(NAME_HDMI_AUDIO, BIT_HDMI_AUDIO, 0, 0);
if (uei.checkSwitchExists()) {
retVal.add(uei);
} else {
// 备用方案:使用hdmi
uei = new UEventInfo(NAME_HDMI, BIT_HDMI_AUDIO, 0, 0);
if (uei.checkSwitchExists()) {
retVal.add(uei);
} else {
Slog.w(TAG, "This kernel does not have HDMI audio support");
}
}
7.3 Extcon方式的HDMI监听
java
// 在Extcon机制中,监听EXTCON_HDMI类型
mExtconInfos = ExtconInfo.getExtconInfoForTypes(new String[] {
ExtconInfo.EXTCON_HEADPHONE,
ExtconInfo.EXTCON_MICROPHONE,
ExtconInfo.EXTCON_HDMI, // HDMI监听
ExtconInfo.EXTCON_LINE_OUT,
});
八、关键sysfs路径总结
8.1 Extcon机制路径
| 设备类型 | 主目录 | 状态文件 | Cable类型文件 |
|---|---|---|---|
| 通用 | /sys/class/extcon/ |
/sys/class/extcon/extcon[X]/state |
/sys/class/extcon/extcon[X]/cable.[Y]/name |
| 耳机 | 同上 | 同上 | name内容: "HEADPHONE" |
| 麦克风 | 同上 | 同上 | name内容: "MICROPHONE" |
| HDMI | 同上 | 同上 | name内容: "HDMI" |
| 线路输出 | 同上 | 同上 | name内容: "LINE-OUT" |
8.2 Switch机制路径
| 设备名称 | 状态文件路径 | 设备路径 |
|---|---|---|
| h2w | /sys/class/switch/h2w/state |
/devices/virtual/switch/h2w |
| usb_audio | /sys/class/switch/usb_audio/state |
/devices/virtual/switch/usb_audio |
| hdmi_audio | /sys/class/switch/hdmi_audio/state |
/devices/virtual/switch/hdmi_audio |
| hdmi | /sys/class/switch/hdmi/state |
/devices/virtual/switch/hdmi |
8.3 Input事件机制
Input事件机制不直接读取sysfs文件,而是通过InputManagerService从内核的input子系统获取switch事件。
九、调试方法
9.1 查看当前Extcon设备
bash
# 列出所有extcon设备
ls -l /sys/class/extcon/
# 查看某个extcon设备支持的cable类型
ls -l /sys/class/extcon/extcon0/
# 读取cable名称
cat /sys/class/extcon/extcon0/cable.0/name
# 读取当前状态
cat /sys/class/extcon/extcon0/state
9.2 查看Switch设备
bash
# 列出所有switch设备
ls -l /sys/class/switch/
# 读取h2w状态
cat /sys/class/switch/h2w/state
# 读取HDMI状态
cat /sys/class/switch/hdmi_audio/state
# 或
cat /sys/class/switch/hdmi/state
9.3 监听UEvent
bash
# 使用getevent监听所有UEvent
getevent -l
# 或使用udevadm(如果可用)
udevadm monitor --kernel --property
9.4 查看日志
bash
# 查看WiredAccessoryManager相关日志
adb logcat | grep WiredAccessoryManager
# 查看ExtconUEventObserver日志
adb logcat | grep ExtconUEventObserver
# 查看所有音频设备变化
adb logcat | grep "setWiredDeviceConnectionState"
9.5 启用详细日志
修改源码中的LOG常量:
java
// WiredAccessoryManager.java
private static final boolean LOG = true; // 改为true
// ExtconUEventObserver.java
private static final boolean LOG = true; // 改为true
// ExtconStateObserver.java
private static final boolean LOG = true; // 改为true
十、常见问题
10.1 为什么同时有Extcon和Switch两种机制?
- 历史原因: Switch是较老的机制,Extcon是新的标准化接口
- 兼容性: 不同内核版本和硬件平台支持不同
- 平滑过渡: 保留Switch机制确保在旧设备上仍能工作
10.2 如何确定系统使用哪种机制?
检查以下条件:
- 查看
/sys/class/extcon是否存在 → 使用Extcon - 否则查看
/sys/class/switch下的设备 → 使用Switch - 检查配置
config_useDevInputEventForAudioJack→ 可能同时使用Input事件
10.3 HDMI音频检测不准确怎么办?
确保内核驱动支持hdmi_audio switch而不是仅支持hdmi switch。hdmi_audio会检查EDID信息,更准确。
10.4 耳机插入但系统无响应
可能原因:
- 内核驱动未正确上报UEvent
- SELinux策略阻止读取sysfs文件
- 状态文件路径不正确
- UEventObserver未正确启动
调试步骤:
- 手动读取状态文件确认值是否变化
- 使用
getevent或udevadm monitor确认UEvent是否发送 - 检查logcat中的错误信息
- 确认SELinux权限
十一、总结
Android系统监听HDMI和有线耳机设备插拔的核心机制:
监听的消息/事件
- UEvent消息 - 来自内核的设备状态变化通知
- Input Switch事件 - 通过InputManagerService的switch状态变化
监听的目录
-
Extcon目录 :
/sys/class/extcon/extcon[X]/- 状态文件:
state - Cable信息:
cable.[Y]/name
- 状态文件:
-
Switch目录 :
/sys/class/switch/{设备名}/- 状态文件:
state
- 状态文件:
关键设备类型
- 有线耳机: h2w, HEADPHONE, MICROPHONE
- HDMI音频: hdmi_audio, hdmi, HDMI
- USB音频: usb_audio
- 线路输出: LINE-OUT
工作流程
- 系统启动时读取初始状态
- 注册UEvent观察者监听设备路径
- 设备插拔时内核更新sysfs文件并发送UEvent
- Java层接收UEvent并解析状态
- 更新内部状态并通知AudioManager
- AudioManager更新音频路由策略
这个机制确保了Android系统能够及时、准确地检测和响应有线音频设备的插拔事件。