本文基于Android 4.4撰写,另外也参看了一下4.2,机制相同,也许细节方面会有所不同,这里以4.4为主。
Android耳机插拔可以有两个机制实现:
1. InputEvent
2. UEvent
其中UEvent是Android系统默认的耳机插拔机制,所以我这里最终代码是基于UEvent实现的,对于InputEvent机制只是大概看了看,并没有具体实现,因此不能保证一定正确,寻求解决方法的同学可以直接移步只对UEvent方式的介绍。
首先我们看看耳机检测的原理。一般的耳机检测包含普通的耳机检测和带mic的耳机检测两种,这两种耳机统称为Headset,而对于不带mic的耳机,一般称之为Headphone。
对于Headset装置的插入检测,一般通过Jack即耳机插座来完成,大致的原理是使用带检测机械结构的耳机插座,将检测脚连到可GPIO中断上,当耳机插入时,耳机插头的金属会碰到检测脚,使得检测脚的电平产生变化,从而引起中断。这样就可以在中断处理函数中读取GPIO的的值,进一步判断出耳机是插入还是拔出。
而对于Headset是否带mic的检测,需要通过codec附加的micbias电流的功能,具体可以参考我的下一篇文章。
前面提到Android默认提供了两种解决方法,那么一定也提供了两种方式的切换,这个提供切换的设置名为config_useDevInputEventForAudioJack,对Android源代码进行全局搜索,可以看到它在frameworks/base/core/res/res/values/config.xml中,默认为false,即不使用InputEvent方式,另外在源码包的厂商相关的文件夹中也找到了相关的设置,如下:
/android/4.4/device/asus/flo/overlay/frameworks/base/core/res/res/values/config.xml
/android/4.4/device/samsung/manta/overlay/frameworks/base/core/res/res/values/config.xml
/android/4.4/device/asus/deb/overlay/frameworks/base/core/res/res/values/config.xml
/android/4.4/device/lge/hammerhead/overlay/frameworks/base/core/res/res/values/config.xml
/android/4.4/device/lge/mako/overlay/frameworks/base/core/res/res/values/config.xml
可以看到有些厂商的确是使用了InputEvent的方式来进行耳机检测。具体对这个变量的修改是在device下还是frameworks下我想应该都可以,device下可能更好。
1) Android上层的大概机制
InputEvent部分的大概机制可以在网上搜索文章,具体流程我也不是特别清楚,这里大概说一下。
InputEvent的处理主要在InputManagerService.java中。在InputManagerService构造函数中,通过如下函数,
mUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
判断当前是否通过InputEvent实现耳机插拔检测。
当Android得到InputEvent后,会调用InputManagerService.java中notifySwitch的函数,进而转至WiredAccessoryManager.java文件中的notifyWiredAccessoryChanged函数,之后的流程就和UEvent相同了,在后续会讲到。
2) Kernel层的机制
Kernel层对耳机插拔InputEvent处理主要是通过input_report_key/input_report_switch来实现,而在实际使用中,ASOC已经为我们封装好了相应Jack接口函数,只要符合规范就可以拿来使用。下面列出几个常用的接口函数。
生成一个新的jack对象,定义其被检测的类型,即可能插入的设备类型。一般定义为SND_JACK_HEADSET,其余也可以根据接口支持种类添加SND_JACK_LINEOUT,SND_JACK_AVOUT等。
这个函数中调用了snd_jack_new,而在snd_jack_new中可以看到调用 input_allocate_device()分配了input device,就可以在后续产生input event了。
将之前定义好的pins加入dapm widgets中,方便dapm统一管理。这一步和InputEvent没有一定联系,可以不调用,主要是可以将耳机插座定义为widgets加入dapm进行省电管理。
汇报jack插拔状态,主要完成以下两个工作:
a) 根据插入拔出状态更新前面通过snd_soc_jack_add_pins加入的dapm pin的状态,对其进行上电下电管理。
‚ b) 调用snd_jack_report,在其中通过input_report_key/input_report_switch来向上层汇报input event。
基于上面的函数,可以用以下做法来实现基于InputEvent机制的耳机插拔检测:
a) snd_soc_jack_new 创建jack对象
b) snd_soc_jack_add_pins将其加入到dapm wigets中
c) 通过request irq申请耳机插拔中断,在中断处理函数中通过检测线高低电平判断耳机是插入还是拔出,通过读取codec寄存器来判断是headset还是headphone
d) 根据判断结果调用snd_soc_jack_report发送InputEvent
此外,ASOC还提供了一个封装好的函数来实现上述c)和d)步骤的功能:
该函数通过标准GPIO驱动申请GPIO及GPIO对应中断,并提供了统一的中断处理函数来汇报事件。此函数只适用于耳机中断接至GPIO且GPIO驱动为Linux标准驱动的情况下,并且不支持mic检测,因此不建议使用。
UEvent机制比较简单,它基于switch driver,switch driver会在Android建立耳机插拔的目录/sys/devices/virtual/switch/h2w,在此目录下有个设备结点名为state,driver通过更新state的值,从而通知Android上层耳机状态的改变。
1) Android上层机制
针对UEvent机制,Android上层在WiredAccessoryManager.java中实现。
在这个文件中,从UEventObserver中继承了类WiredAccessoryObserver,在makeObservedUEventList中将要观察的事件加入到UEvent系统中:
if(!mUseDevInputEventForAudioJack) {
uei = new UEventInfo(NAME_H2W,BIT_HEADSET, BIT_HEADSET_NO_MIC);
……
……
}
可以看到,只有当不使用InputEvent时才添加UEvent事件,NAME_H2W就是headphone对应的switch driver的名字。BIT_HEADSET和BIT_HEADSET_NO_MIC是state结点的两个值,分别表示有mic和无mic的耳机。
当UEvent事件到来时,类WiredAccessoryObserver中重载的onUEvent函数会被回调,从而调用updateStateLocked(devPath,name, state) ,其中state的值就是通过/sys/devices/virtual/switch/h2w/state结点来获得。
最后,程序会进入setDeviceStateLocked函数中处理,在setDeviceStateLocked中根据state的值设置device,然后调用mAudioManager.setWiredDeviceConnectionState,最后进入AudioPolicyManagerBase::setDeviceConnectionState。
2) Kernel层的机制
前面说过,基于UEvent的耳机检测机制需要实现一只switch driver,它会建立一个用于耳机插拔检测的目录/sys/devices/virtual/switch/h2w,在此目录下有个设备结点名为state,switch driver通过更新state的值,从而通知Android上层耳机状态的改变。
switch driver的目录在Linux kernel的drivers/staging/android/switch目录下,可以从目录名称中看到这只driver是为了Android专门产生的。在switch目录下有两个已有文件,switch_class.c是switch driver的内部实现,它提供了switch driver所需的一些API;switch_gpio.c是一个例子,它实现了一个基于GPIO中断的switch driver。
另外,在drivers/switch目录下也有同样的文件,不同之处是两者在Android下生成的结点的位置不同,我们也可以按照drivers/switch目录下的switch driver来实现,不过需要更改WiredAccessoryManager.java文件中getDevPath和getSwitchStatePath下的路径。
下面讲讲如何添加switch driver。添加switch driver很简单,可以仿照switch_gpio.c,大致步骤如下:
a) 在drivers/staging/android/switch目录下新建一个platform driver,其中包含一个全局变量struct switch_dev sdev,即要注册的switch device。
b) 在platformdriver的probe函数中调用switch_dev_register将前面的sdev注册到系统中。
int switch_dev_register(struct switch_dev *sdev)
c) 申请用于耳机检测的中断处理函数。对于耳机插拔来说,由于用户的插拔快慢等可能产生多次中断,所以一般是在中断处理函数中实现一个延时工作队列,即INIT_DELAYED_WORK,在队列的回调函数中来进行实际判断。
d) 当中断发生后,通过switch_set_state设置state节点的值,这个值要和WiredAccessoryManager.java文件中定义的一致,可参看BIT_HEADSET和BIT_HEADSET_NO_MIC的定义。目前是0表示无耳机插入,1表示带Mic的耳机,2表示不带Mic的耳机。
void switch_set_state(struct switch_dev *sdev, int state)
我们进一步看看switch_set_state这个函数,在这个函数中调用了kobject_uevent_env/kobject_uevent,这两个函数就是kernel通过uevent来通知user space的核心函数了。