Android 耳機插拔流程源碼跟蹤淺析

Android 開發(fā)過程中,使用耳機控制拍照,控制音樂播放,控制打電話等,線控再到藍牙控... 耳機也在不斷升級,耳機插拔的程序這一塊也在不斷完善。因此,在定制開發(fā)過程中,閱讀這部分流程代碼是必修的功課了,至少首先的要搞清楚程序走的線路流程。下面結合我在實際工作過程中遇到的bug,需求定制等做個簡單的總結。

第一節(jié),插拔耳機時,事件上報

抓取事件命令:

  • 查看有哪些事件可以get
PS C:\Users\xxxx> adb shell getevent -l
add device 1: /dev/input/event11
  name:     "comp"
add device 2: /dev/input/event10
  name:     "accel"
add device 3: /dev/input/event9
  name:     "gyro"
add device 4: /dev/input/event0
  name:     "Power Button"
add device 5: /dev/input/event5
  name:     "Video Bus"
add device 6: /dev/input/event8
  name:     "baytrailaudio Intel MID Audio Jack"
add device 7: /dev/input/event6
  name:     "gpio-lesskey"
add device 8: /dev/input/event7
  name:     "dollar_cove_power_button"
add device 9: /dev/input/event3
  name:     "jsa1212_als"
add device 10: /dev/input/event2
  name:     "jsa1212_ps"
add device 11: /dev/input/event1
  name:     "sx9500"
add device 12: /dev/input/event4
  name:     "goodix_ts"
  • event8正式我們想要查看的event。
add device 6: /dev/input/event8 
name:     "baytrailaudio Intel MID Audio Jack"

下面我們開始get event8 詳細信息,下面事件信息分別是拔出和插入耳機時事件信息。<code>SW_MICROPHONE_INSERT </code>帶mic的耳機。

PS C:\Users\xxxx> adb shell getevent -l  /dev/input/event8
EV_SW        SW_HEADPHONE_INSERT  00000000
EV_SW        SW_MICROPHONE_INSERT 00000000
EV_SYN       SYN_REPORT           00000000
EV_SW        SW_HEADPHONE_INSERT  00000001
EV_SW        SW_MICROPHONE_INSERT 00000001
EV_SYN       SYN_REPORT           00000000

從上面的輸出數(shù)據(jù)中我們可以看到,插入耳機上報1,拔出是0。
另外,我們可以從get parameter命令看到當前信息(注:這個命令是特別方案才有,取決于芯片商)

PS C:\Users\xxxx> adb shell parameter status
...
Last Applied [Pending] Configurations:
======================================
OutputDevice.Private.Selected: WiredSpeakers [<none>]
IHF.SDRC: Enabled [<none>]
InputDevice.Selected: HeadsetMic [<none>]
OutputDevice.Selected: Multimedia.IHF.Headset [<none>]
IHF.StereoEq: Enabled [<none>]
Headset.Selected: Digital [<none>]
Voip.Tuning: Default [<none>]
Calibration: Default [<none>]
LPE_Mixer: Default [<none>]
Audio.voice: Default [<none>]
...

<code>InputDevice.Selected</code> ,<code>OutputDevice.Selected</code>等我們可以看到是使用耳機狀態(tài)。拔掉耳機,我們看一下具體的parameter信息:

Last Applied [Pending] Configurations:
======================================
OutputDevice.Private.Selected: WiredSpeakers [<none>]
IHF.SDRC: Enabled [<none>]
InputDevice.Selected: VoiceRecgnition.FrontMic [<none>]
OutputDevice.Selected: Multimedia.IHF [<none>]
IHF.StereoEq: Enabled [<none>]
Headset.Selected: Digital [<none>]
Voip.Tuning: Default [<none>]
Calibration: Default [<none>]
LPE_Mixer: Default [<none>]

此時相關參數(shù)發(fā)生了變化:<code>InputDevice.Selected</code>,<code>OutputDevice.Selected</code> 等已經(jīng)發(fā)生了變化。

第二節(jié),插入拔出framework部分相關源碼流程分析

涉及到的類文件

  • InputManagerService.java
    ./framework/base/services/core/java/com/android/server/input/InputManagerService.java
    *WiredAccessoryManager.java.
    ./framework/base/services/core/java/com/android/server/WiredAccessoryManager.java
  • config.xml
    ./framework/base/core/res/res/values/config.xml
  • SystemServer.java
    ./framework/base/services/java/com/android/server/SystemServer.java
  • AudioManager.java
    ./base/media/java/android/media/AudioManager.java
  • AudioService.java
    ./base/media/java/android/media/AudioService.java

UEvent和InputEvent的選擇
這兩個的切換主要是通過設置屬性來的。該屬性開關位于config.xml中:

    <!-- When true use the linux /dev/input/event subsystem to detect the switch changes
         on the headphone/microphone jack. When false use the older uevent framework. -->
    <bool name="config_useDevInputEventForAudioJack">false</bool>

從注釋里面我們可以看到設置為true,選擇/dev/input/event ,設置為false 選擇uevent 來控制事件的上報。

在InputManagerService.java 構造方法中,config_useDevInputEventForAudioJack的值初始化mUseDevInputEventForAudioJack 決定采用哪種方式。

    public InputManagerService(Context context) {
        this.mContext = context;
        this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());

        //config_useDevInputEventForAudioJack 
        mUseDevInputEventForAudioJack =
                context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
        Slog.i(TAG, "Initializing input manager, mUseDevInputEventForAudioJack="
                + mUseDevInputEventForAudioJack);
        mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());

        LocalServices.addService(InputManagerInternal.class, new LocalService());
    }

插入耳機底層kernal事件上報后(注:這段過程需要研究),轉到<code>InputManagerService.java</code>中的<code>notifySwitch</code>方法中。
接下來我們先看<code>notifySwitch</code>方法:

    // Native callback. 
    private void notifySwitch(long whenNanos, int switchValues, int switchMask) {
        
        ... 因為當前我們只看耳機插拔的模式,因此,其他的先排除。
        if (mUseDevInputEventForAudioJack && (switchMask & SW_JACK_BITS) != 0) {
            mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues,
                    switchMask);
        }
    }

接下來是走到<code>mWiredAccessoryCallbacks</code> 回調(diào)中。
有個疑問:<code>mWiredAccessoryCallbacks</code>是什么呢?*
先看看回調(diào)
接下來我們先順藤摸瓜從這個callBack的聲明初始化開始,找到目標。
<code>WiredAccessoryCallbacks </code>接口的聲明,它在<code>WiredAccessoryManager.java</code>類的內(nèi)部,接口有兩個方法,一個是<code>notifyWiredAccessoryChanged</code>另外一個是<code>systemReady</code> 。

    /**
     * Callback interface implemented by WiredAccessoryObserver.
     */
    public interface WiredAccessoryCallbacks {
        public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask);
        public void systemReady();
    }

再看何時將它初始化:

    public void setWindowManagerCallbacks(WindowManagerCallbacks callbacks) {
        mWindowManagerCallbacks = callbacks;
    }

<code>setWindowManagerCallbacks</code>的調(diào)用在<code>SystemServer.java</code>中,也就是<code>InputManagerService</code>被創(chuàng)建的時候。
接下來轉到SystemServer.java中:

            inputManager = new InputManagerService(context);
            wm = WindowManagerService.main(context, inputManager,
                    mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL/* always false*/,
                    !mFirstBoot/* always true*/,mOnlyCore/* always false*/);
            ServiceManager.addService(Context.WINDOW_SERVICE, wm);
            //register the inputManagerService to ServiceManager
            ServiceManager.addService(Context.INPUT_SERVICE, inputManager); 
...
            if (!disableMedia) {
                try {
                    Slog.i(TAG, "Wired Accessory Manager");
                    // Listen for wired headset changes
                    inputManager.setWiredAccessoryCallbacks(
                            new WiredAccessoryManager(context, inputManager));
                } catch (Throwable e) {
                    reportWtf("starting WiredAccessoryManager", e);
                }
            }

從上面的代碼可以看到<code>WiredAccessoryManager</code>對象直接被注冊為callBack 因此,<code>mWiredAccessoryCallbacks.notifyWiredAccessoryChanged</code>直接將后面的任務交給了<code>WiredAccessoryManager</code>類。

WiredAccessoryManager.java
notifyWiredAccessoryChanged方法:

        @Override
    public void notifyWiredAccessoryChanged(long whenNanos, int switchValues, int switchMask) {
        synchronized (mLock) {
            int headset;
            mSwitchValues = (mSwitchValues & ~switchMask) | switchValues;
            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;
            }
            //上面的switch 語句是看哪種耳機,帶mic否?
            updateLocked(NAME_H2W,
                (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);
        }
    }

接著轉到updateLocked方法,該方法作用是check當前模式是否變化。也就是是否發(fā)生耳機拔出了或者插入了(0->1;1->0的變化)。state無變化則不會繼續(xù)處理。
疑問:這里的BIT_USB_HEADSET_ANLG和BIT_USB_HEADSET_DGTL是什么?

    /**
     * Compare the existing headset state with the new state and pass along accordingly. Note
     * that this only supports a single headset at a time. Inserting both a usb and jacked headset
     * results in support for the last one plugged in. Similarly, unplugging either is seen as
     * unplugging all.
     *
     * @param newName One of the NAME_xxx variables defined above.
     * @param newState 0 or one of the BIT_xxx variables defined above.
     */
    private void updateLocked(String newName, int newState) {
        // Retain only relevant bits
        int headsetState = newState & SUPPORTED_HEADSETS;
        int usb_headset_anlg = headsetState & BIT_USB_HEADSET_ANLG;
        int usb_headset_dgtl = headsetState & BIT_USB_HEADSET_DGTL;
        int h2w_headset = headsetState & (BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT);
        boolean h2wStateChange = true;
        boolean usbStateChange = true;

        //fulairy add: check the state changed or not mHeadsetState is old state and headsetState is new state
        if (mHeadsetState == headsetState) {
            Log.e(TAG, "No state change.");
            return;
        }

        // reject all suspect transitions: only accept state changes from:
        // - a: 0 headset to 1 headset
        // - b: 1 headset to 0 headset
        if (h2w_headset == (BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) {
            Log.e(TAG, "Invalid combination, unsetting h2w flag");
            h2wStateChange = false;
        }
        // - c: 0 usb headset to 1 usb headset
        // - d: 1 usb headset to 0 usb headset
        if (usb_headset_anlg == BIT_USB_HEADSET_ANLG && usb_headset_dgtl == BIT_USB_HEADSET_DGTL) {
            Log.e(TAG, "Invalid combination, unsetting usb flag");
            usbStateChange = false;
        }
        //flairy add : h2wStateChange and usbStateChange all not changed .
        if (!h2wStateChange && !usbStateChange) {
            Log.e(TAG, "invalid transition, returning ...");
            return;
        }

        mWakeLock.acquire(); 
        //flairy add : yeah ,you changed pls mHandler deal with it .
        Message msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE/*what*/, headsetState/*arg0*/, //headsetState 新狀態(tài)
                mHeadsetState/*arg1*/, newName/*obj*/);//mHeadsetState 舊的狀態(tài),newName name of the headset .
        mHandler.sendMessage(msg);

        mHeadsetState = headsetState;// update the status .
    }

接下來mHandler處理:

    private final Handler mHandler = new Handler(Looper.myLooper(), null, true) {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_NEW_DEVICE_STATE:
                    setDevicesState(msg.arg1, msg.arg2, (String)msg.obj);
                    mWakeLock.release();
                    break;
                ...
            }
        }
    };

//這個方法挺有意思...就是沒看明白 :(
private void setDevicesState(
            int headsetState, int prevHeadsetState, String headsetName) {
        synchronized (mLock) {
            int allHeadsets = SUPPORTED_HEADSETS;
            for (int curHeadset = 1; allHeadsets != 0; curHeadset <<= 1) {
                if ((curHeadset & allHeadsets) != 0) {
                    setDeviceStateLocked(curHeadset, headsetState, prevHeadsetState, headsetName);
                    allHeadsets &= ~curHeadset;
                }
            }
        }
    }

    private void setDeviceStateLocked(int headset,
            int headsetState, int prevHeadsetState, String headsetName) {
        if ((headsetState & headset) != (prevHeadsetState & headset)) {
            int outDevice = 0;
            int inDevice = 0;
            int state;
//important:  state set start 
            if ((headsetState & headset) != 0) {
                state = 1;
            } else {
                state = 0;
            }
// important : state set end 

            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;
            } else {
                Slog.e(TAG, "setDeviceState() invalid headset type: "+headset);
                return;
            }
...

//update the out device and in device .
            if (outDevice != 0) {
              mAudioManager.setWiredDeviceConnectionState(outDevice, state, headsetName);
            }
//這里我們僅僅看in device. 
            if (inDevice != 0) {
              mAudioManager.setWiredDeviceConnectionState(inDevice, state, headsetName);
            }
        }
    }

接下來的流程就交給了<code>AudioManager</code>,從代碼看實際上真正的處理這是AudioService

IAudioService service = getService();
...
service.setWiredDeviceConnectionState(device, state, name);

因此我們跳過AudioManager.java直接從AduioService.java看。

setWiredDeviceConnectionState--> queueMsgUnderWakeLock-->sendMsg--> ...  
從源代碼看到這里,你可以發(fā)現(xiàn)AudioService是個非常重要的角色,因此它里面的控制邏輯很多建議多多理解。

接下來我們直接從handler處理消息開始。因為我們只搞清楚headset處理的這條線。

                ...
                case MSG_SET_WIRED_DEVICE_CONNECTION_STATE:
                    onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj);
                    mAudioEventWakeLock.release();
                    break;
...

 private void onSetWiredDeviceConnectionState(int device, int state, String name)
    {
        synchronized (mConnectedDevices) {
            //state==0 ===> the device is disconnected.
            ...  //ignore BluetoothA2dp Device.
            ...
            handleDeviceConnection((state == 1)/*FULAIRY ADD :true if connected , false if disconnected */, device, (isUsb ? name : ""/* FuLaiRy add :that's why we get empty string when we use common headset.*/));
            ... // other conditions we also ignore  
            // FuLAIRY ADD :Send broadcast ...
            if (!isUsb && (device != AudioSystem.DEVICE_IN_WIRED_HEADSET)) {
                sendDeviceConnectionIntent(device, state, name);
            }
        }
    }

<code>onSetWiredDeviceConnectionState</code>方法中我們忽略其他一些情況的處理,看最主要的兩個: 一個是<code>handleDeviceConnection</code>,另外一個是<code>sendDeviceConnectionIntent</code>。因此,下面我們主要看著兩個方法。

  • handleDeviceConnection
    private boolean handleDeviceConnection(boolean connected, int device, String params) {
        synchronized (mConnectedDevices) {
            //Fulairy: mConnectedDevices is a hashMap :
            //private final HashMap <Integer, String> mConnectedDevices = new HashMap <Integer, String>();

            // the if means that if key and values are all equal,indicate the same device has been connected .
            boolean isConnected = (mConnectedDevices.containsKey(device) &&
                    (params.isEmpty() || mConnectedDevices.get(device).equals(params)));

            if (isConnected && !connected) {
                //斷開
                AudioSystem.setDeviceConnectionState(device,
                                              AudioSystem.DEVICE_STATE_UNAVAILABLE,
                                              mConnectedDevices.get(device));
                 mConnectedDevices.remove(device);
                 return true;
            } else if (!isConnected && connected) {
                 //連接
                 //接下來的處理在JNI方法
                 AudioSystem.setDeviceConnectionState(device,
                                                      AudioSystem.DEVICE_STATE_AVAILABLE,
                                                      params);
                 mConnectedDevices.put(new Integer(device), params);
                 return true;
            }
        }
        return false;
    }
        ...

JNI部分放到后面補充。

  • sendDeviceConnectionIntent
    從這個方法,可以看到intent耳機插拔的廣播我們可以在app層通過監(jiān)聽<code>ACTION_HEADSET_PLUG</code>。 intent攜帶了,當前最新狀態(tài)state ,name以及microphone(耳機是否帶麥)??梢詮淖詭б魳凡シ牌骺纯础?/li>
private void sendDeviceConnectionIntent(int device, int state, String name)
    {
        Intent intent = new Intent();

        intent.putExtra("state", state); 
        intent.putExtra("name", name);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);

        int connType = 0;

        if (device == AudioSystem.DEVICE_OUT_WIRED_HEADSET) {
            connType = AudioRoutesInfo.MAIN_HEADSET;
            intent.setAction(Intent.ACTION_HEADSET_PLUG);
            intent.putExtra("microphone", 1);
        } else if (device == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE ||
                   device == AudioSystem.DEVICE_OUT_LINE) {
            /*do apps care about line-out vs headphones?*/
            connType = AudioRoutesInfo.MAIN_HEADPHONES;
            intent.setAction(Intent.ACTION_HEADSET_PLUG);
            intent.putExtra("microphone", 0);
        } ...
        ... 割舍了也很重要的一些其他邏輯處理。
        try {
            ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL); // 后面的流程就不繼續(xù)跟了,這個是通用,單獨分出一條線比較好,check里面是如何運作的。
        } finally ...
    }

說到這里我們不得不提一下,插入與拔出耳機通知欄耳機圖標的顯示與消失。

第三節(jié),插拔耳機,通知欄圖標的顯示與消失。

在原生應用中,搜索<code>ACTION_HEADSET_PLUG</code>我們是搜索不到的。因為沒有做處理。下面我們自己添加。
涉及到的文件

  • PhoneStatusBarPolicy.java
    frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone
添加廣播注冊
        //FuLairy add: HeadSet start
        filter.addAction(Intent.ACTION_HEADSET_PLUG) ;
        //FuLairy add: HeadSet start
        mContext.registerReceiver(mIntentReceiver, filter, null, mHandler);

        // FuLaiRy add Headset start
        mService.setIcon(SLOT_HEADSET,  R.drawable.stat_sys_tty_mode, 0, null);
        mService.setIconVisibility(SLOT_HEADSET, false);
// FuLaiRy add Headset end
新增update方法
    //FuLairy add: HeadSet start

public final void updateHeadSet(intent){

    final int state = intent.getIntExtra("state",0/*default*/) ;
    final String name = intent.getStringExtra("name",""/*this always be empty string*/) ;
    final int microPhone =intent.getIntExtra("microphone",0/*default*/) ;

    switch(state){

        case 0:
            //disconnected
            String contentDescription = mContext.getString(R.string.headset_disconnected);
            mService.setIcon(SLOT_HEADSET, R.drawable.head_set_disconnected_icon, 0, contentDescription);
            mService.setIconVisibility(SLOT_HEADSET, false);
            break;
            case 1:
                //connected
                String contentDescription = mContext.getString(R.string.headset_connected);
                if(microPhone==1){

                    mService.setIcon(SLOT_HEADSET, R.drawable.head_set_connected_icon_microphone, 0, contentDescription);
                    mService.setIconVisibility(SLOT_HEADSET, true);
                }else if(microPhone==0){

                    mService.setIcon(SLOT_HEADSET, R.drawable.head_set_connected_icon_nomicrophone, 0, contentDescription);
                    mService.setIconVisibility(SLOT_HEADSET, true);
                }
                break;
                default:
                    mService.setIconVisibility(SLOT_HEADSET, false);
    }}//FuLairy add: HeadSet end
core/res/res/values/config.xml中添加:
<item><xliff:g id="id">headset</xliff:g></item>

大概就這個思路,由于條件限制了,不然可以完整提供一下patch。流程清楚后,修改就快的多。

結尾:

親身實踐才是王道... 文章只是個拋磚引玉。
歡迎批評指正。
Thank you all .
Congratulations!!Happy new year!!!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容