【Android Audio】Android Audio有线设备插拔监听机制

Android系统有线设备插拔监听机制详解

概述

Android系统通过WiredAccessoryManager类来监听HDMI和有线耳机等有线设备的插拔事件。该管理器使用两种主要机制来检测设备状态变化:

  1. Extcon子系统 - 通过监听/sys/class/extcon目录下的UEvent事件
  2. 传统Switch机制 - 通过监听/sys/class/switch目录下的状态文件
  3. 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等),系统会:

  1. 读取设备类型

    • 路径:/sys/class/extcon/extcon[X]/cable.[Y]/name
    • 示例:/sys/class/extcon/extcon0/cable.0/name 可能包含 "HEADPHONE"
  2. 监听设备路径

    • 通过符号链接解析实际设备路径
    • 路径格式:/devices/... (通过/sys/class/extcon/extcon[X]解析得到)
  3. 读取状态文件

    • 路径:/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设备:

  1. h2w - 有线耳机
  2. usb_audio - USB音频设备
  3. hdmi_audiohdmi - HDMI音频
3.2.2 监听的目录和文件

主目录 : /sys/class/switch/

对于每个switch设备:

  1. 状态文件路径

    • 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 (备用)
  2. 设备路径

    • 格式:/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 优先级顺序

系统按以下优先级选择监听机制:

  1. Input事件 + Extcon (如果两者都启用会有警告)
  2. Extcon子系统 (如果/sys/class/extcon存在)
  3. 传统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 如何确定系统使用哪种机制?

检查以下条件:

  1. 查看/sys/class/extcon是否存在 → 使用Extcon
  2. 否则查看/sys/class/switch下的设备 → 使用Switch
  3. 检查配置config_useDevInputEventForAudioJack → 可能同时使用Input事件

10.3 HDMI音频检测不准确怎么办?

确保内核驱动支持hdmi_audio switch而不是仅支持hdmi switch。hdmi_audio会检查EDID信息,更准确。

10.4 耳机插入但系统无响应

可能原因:

  1. 内核驱动未正确上报UEvent
  2. SELinux策略阻止读取sysfs文件
  3. 状态文件路径不正确
  4. UEventObserver未正确启动

调试步骤:

  1. 手动读取状态文件确认值是否变化
  2. 使用geteventudevadm monitor确认UEvent是否发送
  3. 检查logcat中的错误信息
  4. 确认SELinux权限

十一、总结

Android系统监听HDMI和有线耳机设备插拔的核心机制:

监听的消息/事件

  1. UEvent消息 - 来自内核的设备状态变化通知
  2. Input Switch事件 - 通过InputManagerService的switch状态变化

监听的目录

  1. Extcon目录 : /sys/class/extcon/extcon[X]/

    • 状态文件: state
    • Cable信息: cable.[Y]/name
  2. Switch目录 : /sys/class/switch/{设备名}/

    • 状态文件: state

关键设备类型

  • 有线耳机: h2w, HEADPHONE, MICROPHONE
  • HDMI音频: hdmi_audio, hdmi, HDMI
  • USB音频: usb_audio
  • 线路输出: LINE-OUT

工作流程

  1. 系统启动时读取初始状态
  2. 注册UEvent观察者监听设备路径
  3. 设备插拔时内核更新sysfs文件并发送UEvent
  4. Java层接收UEvent并解析状态
  5. 更新内部状态并通知AudioManager
  6. AudioManager更新音频路由策略

这个机制确保了Android系统能够及时、准确地检测和响应有线音频设备的插拔事件。

相关推荐
June bug2 小时前
【领域知识】一个休闲游戏产品(安卓和iOS)从0到1
android·ios
ZHANG13HAO2 小时前
android13 系统强制wifi连接到内网,假装具有互联网能力
android
2501_915106322 小时前
iOS 如何绕过 ATS 发送请求,iOS调试
android·ios·小程序·https·uni-app·iphone·webview
JMchen1232 小时前
Android相机硬件抽象层(HAL)逆向工程:定制ROM的相机优化深度指南
android·开发语言·c++·python·数码相机·移动开发·android studio
ANYOUZHEN11 小时前
bugku shell
android
南宫码农13 小时前
我的电视 - Android原生电视直播软件 完整使用教程
android·开发语言·windows·电视盒子
道亦无名14 小时前
音频数据特征值提取 方法和步骤
android·音视频
Lancker14 小时前
定制侠 一个国产纯血鸿蒙APP的诞生过程
android·华为·智能手机·鸿蒙·国产操作系统·纯血鸿蒙·华为鸿蒙
2601_9498095915 小时前
flutter_for_openharmony家庭相册app实战+通知设置实现
android·javascript·flutter