一、驅動程序上報耳麥拔插事件
1. 在有些Android版本中並不會在狀態欄上顯示耳麥圖標。切換聲道也不在系統中實現,而是在驅動中實現的。
2. headset headPhone lineOut
headset:既有聽筒又有Mic
headPhone:只有聽筒,沒有Mic
lineOut: 就是輸出模擬信號到音箱
驅動需要上報三種設備的拔插:headset、headPhone、lineOut。
3. 怎么上報:
(1) 輸入子系統:可以上報按鍵事件也可以上報開關事件(EV_SW),事件類型包括headset、headPhone、lineOut。
或
(2) switch class子系統:通過uevent向用戶空間發送數據,Android中有個線程專門監聽這類事件。
具體使用哪一個要看Android源代碼。
對於輸入設備都需要指定能產生同步類事件EV_SYN. 對於按鍵事件,輸入子系統還需要設置按鍵值的范圍,但是對於開關類事件不需要設置。
使用switch dev子系統時,名字必須要設置為"h2w",Android系統監聽/sys/class/switch/h2w這個虛擬設備。
4. 驅動Demo代碼
由於耳塞的檢查Codec給過來的硬件中斷線沒有接到Soc上所以使用軟件模擬。

#include <linux/module.h> #include <linux/types.h> #include <linux/init.h> #include <linux/device.h> #include <linux/fs.h> #include <linux/err.h> #include <linux/switch.h> #include <linux/input.h> static struct input_dev *g_virtual_input; static ssize_t input_test_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { long code; long val; char *endp; /* 如果字符串前面含有非數字, simple_strtol不能處理 */ while ((*buf == ' ') || (*buf == '\t')) buf++; code = simple_strtol(buf, &endp, 0); /* 如果字符串前面含有非數字, simple_strtol不能處理 */ while ((*endp == ' ') || (*endp == '\t')) endp++; val = simple_strtol(endp, NULL, 0); printk("emulate to report EV_SW: 0x%x 0x%x\n", code, val); input_event(g_virtual_input, EV_SW, code, val); input_sync(g_virtual_input); return count; } static DEVICE_ATTR(test_input, S_IRUGO | S_IWUSR, NULL, input_test_store); static int register_input_device_for_jack(void) { int err; /* 分配input_dev */ g_virtual_input = input_allocate_device(); /* 設置 */ /* 2.1 能產生哪類事件 */ set_bit(EV_SYN, g_virtual_input->evbit); set_bit(EV_SW, g_virtual_input->evbit); /* 2.2 能產生這類事件中的哪些 */ /* headset = 聽筒 + MIC = SW_HEADPHONE_INSERT + SW_MICROPHONE_INSERT * 同時上報 SW_HEADPHONE_INSERT 和 SW_MICROPHONE_INSERT, 就表示headset * 為了簡化, 對於android系統只上報SW_MICROPHONE_INSERT也表示headset */ set_bit(SW_HEADPHONE_INSERT, g_virtual_input->swbit); set_bit(SW_MICROPHONE_INSERT, g_virtual_input->swbit); set_bit(SW_LINEOUT_INSERT, g_virtual_input->swbit); /* 2.3 這些事件的范圍 */ g_virtual_input->name = "alsa_switch"; /* 不重要 */ /* 注冊 */ err = input_register_device(g_virtual_input); if (err) { input_free_device(g_virtual_input); printk("input_register_device for virtual jack err!\n"); return err; } /* * sysfs文件創建方法: * 創建/sys/class/input/inputX/test_input文件 * 可以執行類似下面的命令來模擬耳麥的動作: * 觸發上報headset插入: echo 4 1 > /sys/class/input/inputX/test_input * 觸發上報headset取下: echo 4 0 > /sys/class/input/inputX/test_input * 觸發上報headphone插入: echo 2 1 > /sys/class/input/inputX/test_input * 觸發上報headphonet取下: echo 2 0 > /sys/class/input/inputX/test_input */ err = device_create_file(&g_virtual_input->dev, &dev_attr_test_input); if (err) { printk("device_create_file for test_input err!\n"); input_unregister_device(g_virtual_input); input_free_device(g_virtual_input); return err; } return 0; } static void unregister_input_device_for_jack(void) { device_remove_file(&g_virtual_input->dev, &dev_attr_test_input); input_unregister_device(g_virtual_input); input_free_device(g_virtual_input); } /**************************************************************************************************************/ static struct switch_dev g_virtual_switch; static ssize_t state_test_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { long val; val = simple_strtol(buf, NULL, 0); printk("emulate to report swtich state: 0x%x\n", val); switch_set_state(&g_virtual_switch, val); return count; } static DEVICE_ATTR(test_state, S_IRUGO | S_IWUSR, NULL, state_test_store); static int register_switch_device_for_jack(void) { int err; g_virtual_switch.name = "h2w"; /*名字必須是這個,Android系統中使用它判斷*/ err = switch_dev_register(&g_virtual_switch); if (err) { printk("switch_dev_register h2w err!\n"); return err; } /* 創建/sys/class/switch/h2w/test_state文件 * 可以執行類似下面的命令來模擬耳麥的動作: * 觸發上報headset插入: echo 1 > /sys/class/switch/h2w/test_state * 觸發上報headset取下: echo 0 > /sys/class/switch/h2w/test_state */ err = device_create_file(g_virtual_switch.dev, &dev_attr_test_state); if (err) { printk("device_create_file test err!\n"); switch_dev_unregister(&g_virtual_switch); return err; } return 0; } static void unregister_switch_device_for_jack(void) { device_remove_file(g_virtual_switch.dev, &dev_attr_test_state); switch_dev_unregister(&g_virtual_switch); } /**************************************************************************************************************/ static int __init virtual_jack_init(void) { int err; err = register_input_device_for_jack(); err = register_switch_device_for_jack(); return 0; } static void __exit virtual_jack_exit(void) { unregister_input_device_for_jack(); unregister_switch_device_for_jack(); } module_init(virtual_jack_init); module_exit(virtual_jack_exit); MODULE_AUTHOR("weidongshan@qq.com"); MODULE_DESCRIPTION("Virutal jack driver for sound card"); MODULE_LICENSE("GPL"); /* 對應的Makefile: KERN_DIR = /media/ubuntu/works/tiny4412/linux-3.0.86 obj-m += virtual_jack.o all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order */
二、在狀態欄顯示耳麥圖標
1. 優秀相關博文:
Android4.4監聽耳機插入處理方法:https://blog.csdn.net/anndy_peng/article/details/30240135
直接給出的是補丁和圖片①,可直接參考它來實現再狀態欄上添加耳塞圖標的App:
https://github.com/fire855/android_frameworks_base-mtk/commit/7661c081b037a32e273afaf70349a6a1518dab48
[整理]Android屏幕適配(不同的屏幕分辨率和尺寸),選擇mic的圖標大小的時候會使用到:https://blog.csdn.net/ttkatrina/article/details/50623043
2. 參考①,tiny4412上操作步驟:
a. 確定在狀態欄上圖標的位置 修改 frameworks/base/core/res/res/values/config.xml 添加一行:<item><xliff:g id="id">headset</xliff:g></item> b. 創建圖標文件 從①中下載對應圖片保存在如下位置 frameworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_with_mic.png frameworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_without_mic.png c. 修改源碼, 當接收到消息時顯示/清除圖標 : frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java 參考①中的修改。 d. 編譯源文件 d.1 mmm frameworks/base/core/res // 編譯config.xml 得到 out/target/product/tiny4412/system/framework/framework-res.apk d.2 mmm frameworks/base/packages/SystemUI // 編譯圖標文件, 編譯源碼 得到 out/target/product/tiny4412/system/priv-app/SystemUI/SystemUI.apk e. 替換單板上的文件 adb push到 /system/framework/framework-res.apk adb push到 /system/priv-app/SystemUI/SystemUI.apk f. 測試 使用5.1所編譯得到的zImage啟動開發板, 執行以下命令: 觸發上報headset插入: echo 4 1 > /sys/class/input/input0/test_input #4=SW_MICROPHONE_INSERT,1=PlugIn 觸發上報headset取下: echo 4 0 > /sys/class/input/input0/test_input 觸發上報headphone插入: echo 2 1 > /sys/class/input/input0/test_input #2=SW_HEADPHONE_INSERT,1=PlugIn 觸發上報headphone取下: echo 2 0 > /sys/class/input/input0/test_input 觸發上報lineout插入: echo 6 1 > /sys/class/input/input0/test_input #6=SW_LINEOUT_INSERT,1=PlugIn 觸發上報lineout取下: echo 6 0 > /sys/class/input/input0/test_input 測試結果:input的測試成功。但是uevent的失敗,原因見下文
三、耳麥拔插事件調用流程分析
1. Android系統使用input子系統還是使用Switch class(uevent)上報拔插操作,取決於 config_useDevInputEventForAudioJack 配置值,
該值為true時使用input子系統, 為false時使用uevent機制, 該值在下述文件中定義, 后一個文件會覆蓋前一個文件,目前在4412的配置文件
中選擇的是使用輸入子系統的。
frameworks/base/core/res/res/values/config.xml
device/friendly-arm/tiny4412/overlay/frameworks/base/core/res/res/values/config.xml
mUseDevInputEventForAudioJack的賦值: InputManagerService(Context context) //InputManagerService.java mUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
2. 輸入子系統對耳麥插拔事件上報流程
InputReader::loopOnce() //InputReader.cpp mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE); processEventsLocked(mEventBuffer, count); InputReader::processEventsForDeviceLocked device->process(rawEvents, count); //輸入子系統中為每一個/dev/input/eventX都創建一個InputDevice mapper->process(rawEvent); //對於開關事件,這里的InputMapper mapper是SwitchInputMapper SwitchInputMapper::process(const RawEvent* rawEvent) case EV_SW: processSwitch(rawEvent->code, rawEvent->value); //只是簡單記錄下來按鍵值 case EV_SYN: sync(rawEvent->when); //收到sync(type=EV_SYN,code=SYN_REPORT)后處理 //獲取監聽器,調用監聽器的notifySwitch,這里的監聽器是InputDispatch getListener()->notifySwitch(&args); InputDispatcher::notifySwitch(const NotifySwitchArgs* args) //InputDispatcher.cpp mPolicy->notifySwitch(args->eventTime, args->switchValues, args->switchMask, policyFlags); //上面調用的就是com_android_server_input_InputManagerService.cpp中的下面這個函數 NativeInputManager::notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask, uint32_t policyFlags) //com_android_server_input_InputManagerService.cpp //調用java的同名函數notifySwitch,位於InputManagerService.java中 env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifySwitch, when, switchValues, switchMask); notifySwitch(long whenNanos, int switchValues, int switchMask) //InputManagerService.java //調用到mWiredAccessoryCallbacks中的notifyWiredAccessoryChanged if (mUseDevInputEventForAudioJack) //這個就是來自上面的config.xml文件配置,為true才會選擇輸入子系統 mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask); mWiredAccessoryCallbacks是何時注冊的: startOtherServices() //SystemServer.java //WiredAccessoryManager作為一個callback傳給了inputManager inputManager.setWiredAccessoryCallbacks(new WiredAccessoryManager(context, inputManager)); mWiredAccessoryCallbacks = callbacks; 因此上面調用的就是 WiredAccessoryManager.notifyWiredAccessoryChanged mSwitchValues = (mSwitchValues & ~switchMask) | switchValues; //根據上報的值確定變量headset的取值,顯示那個圖標由變量headset決定的 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; } //WiredAccessoryManager的函數,這個函數將會進入Audio系統,uevent分支最終也是調用這個函數。 updateLocked(NAME_H2W, (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
3. Switch class使用uevent對耳麥插拔事件上報流程
有一個UeventThread線程,循環讀取socket的uevent事件並且發送uevent.
(1)初始化: WiredAccessoryManager //WiredAccessoryManager.java //創建一個觀察者 mObserver = new WiredAccessoryObserver(); //WiredAccessoryObserver構造函數調用 WiredAccessoryObserver() //創建一個觀察者事件的列表 mUEventInfo = makeObservedUEventList(); //當上面的config.xml中配置這個變量為false時就使用uevent機制 if (!mUseDevInputEventForAudioJack) { //用於監聽名為"h2w"這個Switch class驅動中創建的虛擬文件 //shell@tiny4412:/sys # find ./ -name h2w //./devices/virtual/switch/h2w //./class/switch/h2w //這里只監聽這三個事件,所以驅動程序通過uevent上報事件也只能上報這三個事件之一 uei = new UEventInfo("h2w", BIT_HEADSET, BIT_HEADSET_NO_MIC, BIT_LINEOUT); init() //WiredAccessoryManager.java startObserving("DEVPATH="+uei.getDevPath()); //往UEventThread里面添加觀察者 UEventThread t = getThread(); //這里的this就是子類WiredAccessoryObserver t.addObserver(match, this); //① (2)從native代碼中獲取uevent事件 run() //本地初始化 nativeSetup(); uevent_init() //android_os_UEventObserver.cpp s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); //hardware/uevent.c setsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz)); bind(s, (struct sockaddr *) &addr, sizeof(addr)) //等待下一個事件 nativeWaitForNextEvent(); //UEventObserver.java uevent_next_event(char* buffer, int buffer_length) //hardware/uevent.c poll(&fds, 1, -1); recv(fd, buffer, buffer_length, 0); //讀取到數據后發送 sendEvent(message); observer.onUEvent(event); //取出上面①中設置的觀察者 UEventObserver observer = mTempObserversToSignal.get(i); observer.onUEvent(UEventObserver.UEvent event) //WiredAccessoryManager.java String devPath = event.get("DEVPATH"); String name = event.get("SWITCH_NAME"); int state = Integer.parseInt(event.get("SWITCH_STATE")); updateStateLocked(devPath, name, state); //WiredAccessoryManager的函數,兩條分支(input和Switch class)在這里匯合了 updateLocked(String newName, int newState)
4. uevent實現和input實現在這里匯合位置
updateLocked(String newName, int newState) //WiredAccessoryManager.java //獲得message,然后發送message Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState, mHeadsetState, newName); mHandler.sendMessage(msg); 上面發送消息會觸發消息的handler被調用: handleMessage(Message msg) { case MSG_NEW_DEVICE_STATE: setDevicesState(msg.arg1, msg.arg2, (String)msg.obj); setDeviceStateLocked(curHeadset, headsetState, prevHeadsetState, headsetName); //終於進入了Audio系統了,設置有線設備的連接狀態 mAudioManager.setWiredDeviceConnectionState(inDevice, state, headsetName); IAudioService service = getService(); //AudioManager.java service.setWiredDeviceConnectionState(device, state, name); //指定一個延時時間后又把消息放到一個消息隊列中 delay = checkSendBecomingNoisyIntent(device, state); queueMsgUnderWakeLock(mAudioHandler, MSG_SET_WIRED_DEVICE_CONNECTION_STATE, delay); //會觸發消息的handler被調用 handleMessage(Message msg) onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj); sendDeviceConnectionIntent(device, state, name); //構建一個Intent結構,然后向應用程序廣播它,注冊對這個Intent感興趣的App就會收到它 Intent intent = new Intent(); intent.putExtra("state", state); intent.putExtra("name", name); intent.setAction(Intent.ACTION_HEADSET_PLUG); ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL);
5. App注冊感興趣的Intent:
//構造函數中添加 PhoneStatusBarPolicy(Context context, CastController cast) //PhoneStatusBarPolicy.java filter.addAction(Intent.ACTION_HEADSET_PLUG); 收到消息后onReceive被調用,updateHeadset是處理函數 onReceive(Context context, Intent intent) if (action.equals(Intent.ACTION_HEADSET_PLUG)) updateHeadset(intent); 在狀態欄中設置head set圖標的狀態 private final void updateHeadset(Intent intent) { final String action = intent.getAction(); final int state = intent.getIntExtra("state", 4); final int mic = intent.getIntExtra("microphone", 4); switch (state) { case 0: //拔出 mService.setIconVisibility("headset", false); break; case 1: //插入 if (mic == 1) { //耳機上有mic顯示這張圖標 mService.setIcon("headset", R.drawable.stat_sys_headset_with_mic, 0, null); } else { //耳機上沒有mic顯示這張圖標 mService.setIcon("headset", R.drawable.stat_sys_headset_without_mic, 0, null); } mService.setIconVisibility("headset", true); break; } }
6. native函數有些是在framework中,有些是在system中,有些是在hardware中,有些是在external中。
四、切換聲音通道流程
1. 切換聲音通道流程
a.在驅動程序中切換:
比如: 插上耳麥發生中斷, 在中斷處理程序中設置聲卡讓聲音從耳機中輸出
b.把輸出通道的選擇權交給android系統
目前這是主流的做法,驅動應該提供切換的能力,但是不應該替App進行決策。此處講解此種方法。
3. 有個查看Android源碼非常方便的網站:androidxref.com/5.0.0_r2/
輸入audio_policy.conf,選擇右邊的"select all", 點search
一個完整復雜的audio_policy.conf的例子:http://androidxref.com/5.0.0_r2/xref/device/asus/grouper/audio_policy.conf
4. App只指定stream type, 從哪個聲卡播放出去那是AudioFlinger的內容。
5. output支持哪些device在策略配置文件/system/etc/audio_policy.conf(/etc下也有?)中就指定了。
6. 對應關系:
一個output對應一個播放線程,也對應一個聲卡,可能對應多個device; 一個device對應一個播放設備,比如喇叭或者耳機.
7. 怎樣切換通道:
主要涉及的文件是AudioPolicyManager.cpp,參考0006_handleDeviceConnection UML 一旦插上USB聲卡,就會創建一個對應的播放線程 驅動程序上報音頻拔插事件, 該事件為某個device插入或拔出, Android系統需要切換聲音通道。 過程為(核心文件為frameworks/av/services/audiopolicy/AudioPolicyManager.cpp): (核心函數為 setDeviceConnectionState) a. checkOutputsForDevice 針對該device, 打開新的output, 創建新的playbackthread. 方法: 從audio_policy.conf中確定"本該有多少個output"可以支持它, mOutputs表示"已經打開的output", 兩者對比即可確定"尚未打開的output" b. checkOutputForAllStrategies / checkOutputForStrategy 對所有的strategy分組聲音, 判斷是否需要遷移到新的output, 如果需要則遷移對應Track到新的output,方法: b.1 判斷是否需要遷移 對於該strategy, 得到它的oldDevice, 進而得到它的outputs (srcOutputs); 對於該strategy, 得到它的newDevice, 進而得到它的outputs (dstOutputs); 如果這2個srcOutputs、dstOutputs不相同, 表示需要遷移 b.2 如果遷移: 把對應的Track設置為invalidate狀態即可, App寫AudioTrack時發現它是invalidate狀態, 就會重新創建新的Track audio_devices_t oldDevice = getDeviceForStrategy(strategy, true /*fromCache*/); audio_devices_t newDevice = getDeviceForStrategy(strategy, false /*fromCache*/); SortedVector<audio_io_handle_t> srcOutputs = getOutputsForDevice(oldDevice, mPreviousOutputs); SortedVector<audio_io_handle_t> dstOutputs = getOutputsForDevice(newDevice, mOutputs); c. getNewOutputDevice/setOutputDevice 這要操作HAL
8. output的參數信息會構成一個profile結構描述
audio_hw_modules { primary { outputs { primary { //這些參數信息會構造成一個profile