1. 程式人生 > >Android APP通過藍芽耳機錄音可行性分析

Android APP通過藍芽耳機錄音可行性分析

1 藍芽的兩種型別

部署最為普遍的兩種規格為藍芽基礎率/增強資料率 (BR/EDR)(採用版本為 2.0/2.1)和低耗能 (LE) 藍芽(採用版本為 4.0/4.1/4.2)。

存在哪些差異?

藍芽 BR/EDR—可建立相對較短距離的持續無線連線,因此非常適用於流式音訊等應用

藍芽 LE—可建立短時間的長距離無線電連線,非常適用於無需持續連線但依賴電池具有較長壽命的的物聯網 (IoT) 應用

雙模—雙模晶片可支援需要連線 BR/EDR 裝置(例如音訊耳機)以及 LE 裝置(例如穿戴裝置或零售信標)的單一裝置(例如智慧手機或平板電腦)

2 核心系統結構

該系統包含射頻收發器、基帶和協議棧,支援裝置連線和交換各類資料。

藍芽裝置交換根據藍芽規格協議訊號。核心系統協議包括射頻 (RF) 協議、鏈路控制 (LC) 協議、鏈路管理器 (LM) 協議以及邏輯鏈路控制和適配協議 (L2CAP),藍芽規格詳細定義了這些協議。

最低的三個系統層—射頻、鏈路控制和鏈路管理器協議—通常被歸屬於稱為藍芽控制器的子系統。這是一種採用可選標準介面—主機控制器介面 (HCI)—的通用部署,支援與藍芽系統的其他裝置(即藍芽主機)進行雙向通訊。

主控制器可能是以下配置之一,具體取決於用例:

BR/EDR 控制器,包括射頻、基帶、鏈路管理器和可選 HCI

LE 控制器,包括 LE PHY、鏈路層和可選 HCI

BR/EDR 組合控制器和 LE 控制器,組合控制器共享一個藍芽裝置地址

藍芽規格通過定義等效層之間交換的協議資訊來實現系統之間的互操作性。它還通過定義藍芽控制器和藍芽主機之間的公用介面來實現獨立藍芽子系統之間的互操作性。

image

物理 (PHY) 層

通過藍芽通訊通道控制 2.4Ghz 射頻的傳輸/接收。BR/EDR 提供的通道較多但頻寬較窄,而 LE 使用的通道較少但頻寬較寬。

鏈路層

定義資料包結構/通道、發現/連線程式以及傳送/接收資料。

直接測試模式

允許測試人員向 PHY 層發出指令以傳輸或接收給定資料包序列,通過 HCI 或 2 線 UART 介面提交命令。

主機控制器介面 (HCI)

藍芽控制器子系統(底部三層)和藍芽主機之間的可選標準介面。

邏輯鏈路控制和適配協議 (L2CAP) 層

基於資料包的協議,可將資料包傳輸至 HCI 或直接傳輸到無主機系統中的鏈路管理器。支援更高級別的協議多路複用、資料包分割和重組,以及將服務質量資訊傳輸到更高層。

屬性協議 (ATT)

在建立連線之後定義資料交換客戶端/伺服器協議。使用通用屬性配置檔案 (GATT) 將屬性分類為有意義的服務。ATT 主要用於 LE 部署,偶爾也會用於 BR/EDR 部署。

安全管理器

定義管理藍芽裝置之間配對完整性、身份驗證以及加密的協議和操作,提供安全功能工具箱,其他元件可利用該工具箱支援不同應用所需的各種安全級別。

通用屬性配置檔案 (GATT)

使用屬性協議,GATT 對封裝裝置元件效能的服務進行分組,並描述基於 GATT 功能的用例、角色和一般效能。其服務框架定義服務規程和格式及其特性,其中包括髮現、讀取、寫入、通知以及指示特性以及配置特性廣播。GATT 僅用於藍芽 LE 部署。

通用訪問配置檔案(GAP)

可與藍芽 LE 部署中的 GATT 配合使用,以定義與發現藍芽裝置和共享資訊相關的規程和角色,以及連線藍芽裝置的鏈路管理內容。

3 Android抓取hci藍芽log

本文是基於AOSP,android 6.0抓取藍芽log方式描述。

在安卓手機設定-開發者選項-啟用藍芽HCI資訊收集日誌。個人理解我們抓取的日誌實際上就是host和controller通過HCI收發資料截獲的資料幀。

3.1 操作描述

1.捷波朗藍芽耳機進入可被發現狀態;

2.開啟手機上的藍芽開關;

3.掃描到藍芽裝置,直到掃描結束;

4.手機上點選捷波朗進行配對;

5.稍等片刻手機和捷波朗耳機配對並連線成功;

6.開啟手機音樂播放器放音樂,耳機中聽到音樂聲,停留一小段時間;

7.手機中關閉藍芽;

8.藍芽耳機斷開連線,並關閉耳機。

重啟手機PC上可以在內建儲存中找到btsnoop_hci.log檔案,或者用其他第三方360助手等匯出到PC,這就是藍芽日誌,PC上使用WireShark進行日誌分析。

3.2 分析藍芽log

對照上面的操作描述。

1.開啟手機上的藍芽開關,host向controller傳送Reset指令,標誌著即將啟動手機上的藍芽模組。

2.手機上的藍芽開啟成功以後,自動進入搜尋模式,直到搜尋結束,這個時候controller向host上報搜尋結果,其中就有捷波朗藍芽耳機,如下:

掃描發現藍芽耳機捷波朗

可以發現捷波朗耳機為可穿戴耳機裝置(Wearable Headset Device),耳機名稱縮寫名稱為Jabra EXTREME2,以及訊號強度RSSI值等。

3.點選手機上的配對和耳機進行配對。

對照log可以知道:

首先會請求被連線裝置的詳細名稱;
連線藍芽耳機捷波朗獲取名字

然後建立連線、獲取時鐘偏移等,具體如下;
連線藍芽耳機匹配之前

接下來才是真正的匹配,進行身份認證;
配對認證成功

4.手機和藍芽耳機連線成功,配置完成後可正常傳輸資料。這裡的主從角色和我們想當然的理解有差異,藍芽耳機是主裝置,手機是從裝置。
藍芽耳機連線成功

5.手機上播放音樂,藍芽耳機接收,並未看到熟悉的A2DP協議,實際上SBC就是A2DP中關於音訊的編碼格式。

SBC即Sub-band coding,子帶編碼,是A2DP(Advanced Audio Distribution Profile,藍芽音訊傳輸協議)協議強制規定的編碼格式。
藍芽耳機聽音樂

這個耳機比較老了,編碼用的是SBC格式,新耳機目前會採用ACC、APTX和LDAC等格式編碼,進一步提升音訊品質。

6.關閉手機藍芽,也會看到Reset命令。

以上就是對照log粗略分析藍芽耳機從配對到連線,再到通過藍芽接收手機音訊的過程。

4. 手機通話過程中藍芽耳機連線分析

4.1 預備知識

在主單元和從單元之間,可以確定不同的型別的藍芽物理鏈路:ACL(Asynchronous Connectionless),和另一種鏈路是SCO(Synchronous Connection Oriented)。SCO主要用於同步話音傳送,ACL主要用於分組資料傳送。A2DP(Advanced Audio Distribution Profile 高階音訊傳輸模型)是跑在ACL鏈路上的高品質音訊協議。

SCO連線對稱連線,利用保留時隙傳送資料包。它主要用於:主單元和從單元之間實現點到點連結。連線建立後,主裝置和從裝置可以不被選中就傳送SCO資料包。

(1) SCO資料包既可以傳送話音,也可以傳送資料,但在傳送資料時,只用於重發被損壞的那部分的資料。

(2) SCO主要用來傳輸對時間要求很高的資料通訊。

(3) SCO 連結由主單元傳送SCO 建立訊息,經連結管理(LM)協議來確立。

ACL鏈路就是定向傳送資料包,它既支援對稱連線,也支援不對稱連線(既可以一對一,也可以一對多)。主要用於:主單元與網中的所有從單元之間實現一點多址的連線方式。

主裝置負責控制鏈路頻寬,並決定微微網中的每個從裝置可以佔用多少頻寬和連線的對稱性。從裝置只有被選中時才能傳送資料。ACL鏈路也支援接收主裝置發給微微網中所有從裝置的廣播訊息。

4.2 藍芽耳機通話分析

(1) 模式設定

為指定連線控制代碼寫鏈路策略設定。鏈路策略設定允許主機控制器指定用於連線控制代碼的LM連線模式

host->controlller

Frame 321: 8 bytes on wire (64 bits), 8 bytes captured (64 bits)
Bluetooth
    [Source: host]
    [Destination: controller]
Bluetooth HCI H4
    [Direction: Sent (0x00)]
    HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Write Link Policy Settings
    Command Opcode: Write Link Policy Settings (0x080d)
    Parameter Total Length: 4
    Connection Handle: 0x0003
    .... .... .... ...1 = Enable Master Slave Switch: true (1)  //允許角色切換
    .... .... .... ..1. = Enable Hold Mode: true (1)    //改變LM狀態和本地及遠端裝置為主模式
    .... .... .... .1.. = Enable Sniff Mode: true (1)   //改變LM狀態和本地及遠端裝置為呼吸模式
    .... .... .... 0... = Enable Park Mode: false (0)   //改變LM狀態和本地及遠端裝置為休眠模式
    [Response in frame: 322]
    [Command-Response Delta: 1.3ms]

controlller->host

Frame 322: 9 bytes on wire (72 bits), 9 bytes captured (72 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command Complete
    Event Code: Command Complete (0x0e)//命令執行狀態-完成
    Parameter Total Length: 6
    Number of Allowed Command Packets: 1
    Command Opcode: Write Link Policy Settings (0x080d)
        0000 10.. .... .... = Opcode Group Field: Link Policy Commands (0x02)
        .... ..00 0000 1101 = Opcode Command Field: Write Link Policy Settings (0x00d)
    Status: Success (0x00)
    Connection Handle: 0x0003 //建立連線控制代碼
    [Command in frame: 321]
    [Command-Response Delta: 1.3ms]

(2) 語音設定

host->controlller

Frame 1471: 6 bytes on wire (48 bits), 6 bytes captured (48 bits)
Bluetooth
    [Source: host]
    [Destination: controller]
Bluetooth HCI H4
    [Direction: Sent (0x00)]
    HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Write Voice Setting
    Command Opcode: Write Voice Setting (0x0c26)
    Parameter Total Length: 2
    0000 00.. .... .... = Unused bits: 0x00
    .... ..00 .... .... = Input Coding: Linear (0)
    .... .... 01.. .... = Input Data Format: 2's complement (1)
    .... .... ..1. .... = Input Sample Size: 16 bit (only for Linear PCM) (1)
    .... .... ...0 00.. = Linear PCM Bit Position: 0
    .... .... .... ..11 = Air Coding Format: Transparent (3)
    [Response in frame: 1472]
    [Command-Response Delta: 0.36ms]

controlller->host

Frame 1472: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command Complete
    Event Code: Command Complete (0x0e)
    Parameter Total Length: 4
    Number of Allowed Command Packets: 1
    Command Opcode: Write Voice Setting (0x0c26)
        0000 11.. .... .... = Opcode Group Field: Host Controller & Baseband Commands (0x03)
        .... ..00 0010 0110 = Opcode Command Field: Write Voice Setting (0x026)
    Status: Success (0x00)
    [Command in frame: 1471]
    [Command-Response Delta: 0.36ms]

(3) 建立SCO連線

host->controlller

Frame 1473: 21 bytes on wire (168 bits), 21 bytes captured (168 bits)
Bluetooth
    [Source: host]
    [Destination: controller]
Bluetooth HCI H4
    [Direction: Sent (0x00)]
    HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Setup Synchronous Connection
    Command Opcode: Setup Synchronous Connection (0x0428)
    Parameter Total Length: 17
    Connection Handle: 0x0003
    Tx Bandwidth (bytes/s): 8000
    Rx Bandwidth (bytes/s): 8000
    Max. Latency (ms): 13
    //語音設定引數,可以理解為SCO通道的屬性
    0000 00.. .... .... = Unused bits: 0x00
    .... ..00 .... .... = Input Coding: Linear (0)
    .... .... 01.. .... = Input Data Format: 2's complement (1)
    .... .... ..1. .... = Input Sample Size: 16 bit (only for Linear PCM) (1)
    .... .... ...0 00.. = Linear PCM Bit Position: 0
    .... .... .... ..11 = Air Coding Format: Transparent (3)
    Retransmission Effort: At least 1 retransmission, optimize for link quality (2)
    Packet Type: 0x0388, 3-EV5, 2-EV5, 3-EV3, EV3
        0000 00.. .... .... = Reserved: 0x00
        .... ..1. .... .... = 3-EV5: True
        .... ...1 .... .... = 2-EV5: True
        .... .... 1... .... = 3-EV3: True
        .... .... .0.. .... = 2-EV3: False
        .... .... ..0. .... = EV5: False
        .... .... ...0 .... = EV4: False
        .... .... .... 1... = EV3: True
        .... .... .... .0.. = HV3: False
        .... .... .... ..0. = HV2: False
        .... .... .... ...0 = HV1: False
    [Pending in frame: 1474]
    [Command-Pending Delta: 1.161ms]
    [Response in frame: 1476]
    [Command-Response Delta: 201.225ms]

controlller->host

Frame 1474: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command Status
    Event Code: Command Status (0x0f)
    Parameter Total Length: 4
    Status: Pending (0x00)
    Number of Allowed Command Packets: 1
    Command Opcode: Setup Synchronous Connection (0x0428)
        0000 01.. .... .... = Opcode Group Field: Link Control Commands (0x01)
        .... ..00 0010 1000 = Opcode Command Field: Setup Synchronous Connection (0x028)
    [Command in frame: 1473]
    [Response in frame: 1476]
    [Command-Pending Delta: 1.161ms]
    [Pending-Response Delta: 200.064ms]
Frame 1476: 20 bytes on wire (160 bits), 20 bytes captured (160 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Synchronous Connection Complete
    Event Code: Synchronous Connection Complete (0x2c)
    Parameter Total Length: 17
    Status: Success (0x00)//成功
    Connection Handle: 0x0004 //建立連線控制代碼
    BD_ADDR: GnNetcom_83:4e:ce (50:c9:71:83:4e:ce)
    Link Type: eSCO connection (0x02)
    Transmit Interval: 12 slots (7.5 msec)
    Retransmit Window: 4 slots (2.5 msec)
    Rx Packet Length: 60
    Tx Packet Length: 60
    Air Mode: Transparent Data (3)
    [Command in frame: 1473]
    [Pending in frame: 1474]
    [Pending-Response Delta: 200.064ms]
    [Command-Response Delta: 201.225ms]

(4) 打電話,低功耗監聽模式(Sniff Subrating)、呼吸模式(Sniff Mode)設定
低功耗模式和呼吸模式設定

(5) 結束電話斷開SCO連線

host->controller

Frame 1490: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth
    [Source: host][Destination: controller]
Bluetooth HCI H4
    [Direction: Sent (0x00)]
    HCI Packet Type: HCI Command (0x01)
Bluetooth HCI Command - Disconnect
    Command Opcode: Disconnect (0x0406)
    Parameter Total Length: 3
    Connection Handle: 0x0004 //注意連線控制代碼和建立sco通道時候一致
    Reason: Remote User Terminated Connection (0x13)
    [Pending in frame: 1491][Command-Pending Delta: 4.262ms][Response in frame: 1492][Command-Response Delta: 97.828ms]

controlller->host

Frame 1491: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Command Status
    Event Code: Command Status (0x0f)
    Parameter Total Length: 4
    Status: Pending (0x00)
    Number of Allowed Command Packets: 1
    Command Opcode: Disconnect (0x0406)
        0000 01.. .... .... = Opcode Group Field: Link Control Commands (0x01)
        .... ..00 0000 0110 = Opcode Command Field: Disconnect (0x006)
    [Command in frame: 1490]
    [Response in frame: 1492]
    [Command-Pending Delta: 4.262ms]
    [Pending-Response Delta: 93.566ms]
Frame 1492: 7 bytes on wire (56 bits), 7 bytes captured (56 bits)
Bluetooth
    [Source: controller]
    [Destination: host]
Bluetooth HCI H4
    [Direction: Rcvd (0x01)]
    HCI Packet Type: HCI Event (0x04)
Bluetooth HCI Event - Disconnect Complete
    Event Code: Disconnect Complete (0x05) //斷開連線成功
    Parameter Total Length: 4
    Status: Success (0x00)
    Connection Handle: 0x0004
    Reason: Connection Terminated by Local Host (0x16)
    [Command in frame: 1490]
    [Pending in frame: 1491]
    [Pending-Response Delta: 93.566ms]
    [Command-Response Delta: 97.828ms]

5.手機通過藍芽耳機錄音解決辦法

經過查閱資料可以知道大部分手機並不支援藍芽耳機錄音功能,但我們在通話過程中使用藍芽耳機的確可以正常輸入語音的,那麼可以推斷想要通過藍芽耳機錄音是不是需要在錄音APP中開啟相應的服務支援。

其實經過上面的分析已經知道打電話之所以可以通過藍芽輸入和輸出聲音,實際上是通過建立SCO連線完成的,所以需要在APP程式碼內控制SCO連線。也就是說想要使用APP藍芽錄音,也要建立SCO連線。是不是確實建立成功可以通過藍芽HCI log分析確定。

先來看Android 6.0藍芽架構

image

藍芽系統服務通過JNI與藍芽協議棧,通過Binder IPC和應用進行通訊。系統服務為開發人員提供了訪問各種藍芽配置檔案的機會。

電話中是如何建立SCO連線的?

通過電話接通後,選擇藍芽音訊輸出入口

image

packages/apps/InCallUI/src/com/android/incallui/CallButtonFragment.java

    @Override
    public boolean onMenuItemClick(MenuItem item) {
        Log.d(this, "- onMenuItemClick: " + item);
        Log.d(this, "  id: " + item.getItemId());
        Log.d(this, "  title: '" + item.getTitle() + "'");

        int mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;

        switch (item.getItemId()) {
            case R.id.audio_mode_speaker:
                mode = CallAudioState.ROUTE_SPEAKER;
                break;
            case R.id.audio_mode_earpiece:
            case R.id.audio_mode_wired_headset:
                // InCallCallAudioState.ROUTE_EARPIECE means either the handset earpiece,
                // or the wired headset (if connected.)
                mode = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
                break;
            case R.id.audio_mode_bluetooth:
                mode = CallAudioState.ROUTE_BLUETOOTH;
                break;
            default:
                Log.e(this, "onMenuItemClick:  unexpected View ID " + item.getItemId()
                        + " (MenuItem = '" + item + "')");
                break;
        }

        getPresenter().setAudioMode(mode);

        return true;
    }

看到了MVP模式中的P,通過點選選單上不同的選項呼叫setAudioMode(mode),設定音訊模式

packages/apps/InCallUI/src/com/android/incallui/CallButtonPresenter.java

public void setAudioMode(int mode) {

        // TODO: Set a intermediate state in this presenter until we get
        // an update for onAudioMode().  This will make UI response immediate
        // if it turns out to be slow

        Log.d(this, "Sending new Audio Mode: " + CallAudioState.audioRouteToString(mode));
        TelecomAdapter.getInstance().setAudioRoute(mode);
    }

進入TelecomAdapter單例,設定音訊路由,發現實際是呼叫InCallService的setAudioRoute(route)方法

packages/apps/InCallUI/src/com/android/incallui/TelecomAdapter.java

void setAudioRoute(int route) {
        if (mInCallService != null) {
            mInCallService.setAudioRoute(route);
        } else {
            Log.e(this, "error setAudioRoute, mInCallService is null");
        }
    }

實際是呼叫Phone的setAudioRoute(route)方法

frameworks/base/telecomm/java/android/telecom/InCallService.java

/**
 * This service is implemented by any app that wishes to provide the user-interface for managing
 * phone calls. Telecom binds to this service while there exists a live (active or incoming) call,
 * and uses it to notify the in-call app of any live and recently disconnected calls. An app must
 * first be set as the default phone app (See {@link TelecomManager#getDefaultDialerPackage()})
 * before the telecom service will bind to its {@code InCallService} implementation.
 * <p>
 * Below is an example manifest registration for an {@code InCallService}. The meta-data
 * ({@link TelecomManager#METADATA_IN_CALL_SERVICE_UI}) indicates that this particular
 * {@code InCallService} implementation intends to replace the built-in in-call UI.
 * <pre>
 * {@code
 * &lt;service android:name="your.package.YourInCallServiceImplementation"
 *          android:permission="android.permission.BIND_IN_CALL_SERVICE"&gt;
 *      &lt;meta-data android:name="android.telecom.IN_CALL_SERVICE_UI" android:value="true" /&gt;
 *      &lt;intent-filter&gt;
 *          &lt;action android:name="android.telecom.InCallService"/&gt;
 *      &lt;/intent-filter&gt;
 * &lt;/service&gt;
 * }
 * </pre>
 */
public abstract class InCallService extends Service {
    ...
    /**
     * Sets the audio route (speaker, bluetooth, etc...).  When this request is honored, there will
     * be change to the {@link #getCallAudioState()}.
     *
     * @param route The audio route to use.
     */
    public final void setAudioRoute(int route) {
        if (mPhone != null) {
            mPhone.setAudioRoute(route);
        }
    }
    ...
}

frameworks/base/telecomm/java/android/telecom/Phone.java

    /**
     * Sets the audio route (speaker, bluetooth, etc...).  When this request is honored, there will
     * be change to the {@link #getAudioState()}.
     *
     * @param route The audio route to use.
     */
    public final void setAudioRoute(int route) {
        mInCallAdapter.setAudioRoute(route);
    }

實際是使用InCallAdapter的setAudioRoute(route)方法

frameworks/base/telecomm/java/android/telecom/InCallAdapter.java

    /**
     * Sets the audio route (speaker, bluetooth, etc...). See {@link CallAudioState}.
     *
     * @param route The audio route to use.
     */
    public void setAudioRoute(int route) {
        try {
            mAdapter.setAudioRoute(route);
        } catch (RemoteException e) {
        }
    }

android.telecom.InCallAdapter實際使用了com.android.server.telecom.InCallAdapter的setAudioRoute(route)

frameworks/base/telecomm/java/com/android/internal/telecom/IInCallAdapter.aidl

/**
 * Internal remote callback interface for in-call services.
 *
 * @see android.telecom.InCallAdapter
 *
 * {@hide}
 */
oneway interface IInCallAdapter {
    ...

    void setAudioRoute(int route);

    ...
}

packages/services/Telecomm/src/com/android/server/telecom/InCallAdapter.java

/**
 * Receives call commands and updates from in-call app and passes them through to CallsManager.
 * {@link InCallController} creates an instance of this class and passes it to the in-call app after
 * binding to it. This adapter can receive commands and updates until the in-call app is unbound.
 */
class InCallAdapter extends IInCallAdapter.Stub {
    ...
    @Override
    public void setAudioRoute(int route) {
        long token = Binder.clearCallingIdentity();
        try {
            synchronized (mLock) {
                mCallsManager.setAudioRoute(route);
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
    ...
}

packages/services/Telecomm/src/com/android/server/telecom/CallsManager.java

    /**
      * Called by the in-call UI to change the audio route, for example to change from earpiece to
      * speaker phone.
      */
    void setAudioRoute(int route) {
        mCallAudioManager.setAudioRoute(route);
    }

packages/services/Telecomm/src/com/android/server/telecom/CallAudioManager.java

    /**
     * Changed the audio route, for example from earpiece to speaker phone.
     *
     * @param route The new audio route to use. See {@link CallAudioState}.
     */
    void setAudioRoute(int route) {
        // This can happen even when there are no calls and we don't have focus.
        if (!hasFocus()) {
            return;
        }

        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));

        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
        int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());

        // If route is unsupported, do nothing.
        if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
            Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
            return;
        }

        if (mCallAudioState.getRoute() != newRoute) {
            // Remember the new speaker state so it can be restored when the user plugs and unplugs
            // a headset.
            mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
            setSystemAudioState(mCallAudioState.isMuted(), newRoute,
                    mCallAudioState.getSupportedRouteMask());
        }
    }

    private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
        setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
    }

    private void setSystemAudioState(
            boolean force, boolean isMuted, int route, int supportedRouteMask) {
        if (!hasFocus()) {
            return;
        }

        CallAudioState oldAudioState = mCallAudioState;
        saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
        if (!force && Objects.equals(oldAudioState, mCallAudioState)) {
            return;
        }

        Log.i(this, "setSystemAudioState: changing from %s to %s", oldAudioState, mCallAudioState);
        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
                CallAudioState.audioRouteToString(mCallAudioState.getRoute()));

        mAudioManagerHandler.obtainMessage(
                MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE,
                mCallAudioState.isMuted() ? 1 : 0,
                0)
                .sendToTarget();

        // Audio route.
        if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
            turnOnSpeaker(false);
            turnOnBluetooth(true);
        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
            turnOnBluetooth(false);
            turnOnSpeaker(true);
        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
                mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
            turnOnBluetooth(false);
            turnOnSpeaker(false);
        }

        if (!oldAudioState.equals(mCallAudioState)) {
            mCallsManager.onCallAudioStateChanged(oldAudioState, mCallAudioState);
            updateAudioForForegroundCall();
        }
    }

分析到這裡看到了關於切換藍芽輸出的程式碼:

if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
    turnOnSpeaker(false);
    turnOnBluetooth(true);
}

趕緊看看turnOnBluetooth(true)方法,這一定是開啟藍芽語音的程式碼

    private void turnOnBluetooth(boolean on) {
        if (mBluetoothManager.isBluetoothAvailable()) {
            boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
            if (on != isAlreadyOn) {
                Log.i(this, "connecting bluetooth %s", on);
                if (on) {
                    mBluetoothManager.connectBluetoothAudio();
                } else {
                    mBluetoothManager.disconnectBluetoothAudio();
                }
            }
        }
    }

先判斷藍芽裝置是否存在,是否已經使用藍芽裝置作為輸出,如果不是才進行切換。在此進入了BluetoothManager管轄範圍

packages/services/Telecomm/src/com/android/server/telecom/BluetoothManager.java

    void connectBluetoothAudio() {
        Log.v(this, "connectBluetoothAudio()...");
        if (mBluetoothHeadset != null) {
            mBluetoothHeadset.connectAudio();
        }

        // Watch out: The bluetooth connection doesn't happen instantly;
        // the connectAudio() call returns instantly but does its real
        // work in another thread.  The mBluetoothConnectionPending flag
        // is just a little trickery to ensure that the onscreen UI updates
        // instantly. (See isBluetoothAudioConnectedOrPending() above.)
        mBluetoothConnectionPending = true;
        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
    }

這個方法實際是呼叫BluetoothHeadset物件的connectAudio()方法,啟動一個藍芽耳機連線,使用SCO通道,到此已經知道電話切換藍芽音訊輸出確實是用藍芽SCO通道實現的。

frameworks/base/core/java/android/bluetooth/BluetoothHeadset.java

    /**
     * Initiates a connection of headset audio.
     * It setup SCO channel with remote connected headset device.
     *
     * @return true if successful
     *         false if there was some error such as
     *               there is no connected headset
     * @hide
     */
    public boolean connectAudio() {
        if (mService != null && isEnabled()) {
            try {
                return mService.connectAudio();
            } catch (RemoteException e) {
                Log.e(TAG, e.toString());
            }
        } else {
            Log.w(TAG, "Proxy not attached to service");
            if (DBG) Log.d(TAG, Log.getStackTraceString(new Throwable()));
        }
        return false;
    }

frameworks/base/core/java/android/bluetooth/IBluetoothHeadset.aidl

/**
 * API for Bluetooth Headset service
 *
 * {@hide}
 */
interface IBluetoothHeadset {
    // Public API
    ...
    boolean connectAudio();
    ...
}

BluetoothManager中呼叫connectBluetoothAudio(),實際上是遠端呼叫HeadsetService服務中的connectAudio()

packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetService.java

public boolean connectAudio() {  
    HeadsetService service = getService();  
    if (service == null) return false;  
    return service.connectAudio();  
}
    boolean connectAudio() {  
         // TODO(BT) BLUETOOTH or BLUETOOTH_ADMIN permission  
         enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission");  
         if (!mStateMachine.isConnected()) {  
             return false;  
         }  
         if (mStateMachine.isAudioOn()) {  
             return false;  
         }  
         mStateMachine.sendMessage(HeadsetStateMachine.CONNECT_AUDIO);  
         return true;  
     }

先檢查許可權BLUETOOTH或者BLUETOOTH_ADMIN,然後判斷連線狀態,未連線和音訊已經通過藍芽輸出都返回false,什麼都沒做。只有在已連線藍芽耳機但還沒使用藍芽耳機作為輸出的時候才去發出Message,從HeadsetStateMachine命名也可以知道它是狀態機,當接收到CONNECT_AUDIO命令時就會將藍芽狀態切換為AudioOn

packages/apps/Bluetooth/src/com/android/bluetooth/hfp/HeadsetStateMachine.java

/**
 * Bluetooth Handset StateMachine
 *                      (Disconnected)
 *                           |    ^
 *                   CONNECT |    | DISCONNECTED
 *                           V    |
 *                         (Pending)
 *                           |    ^
 *                 CONNECTED |    | CONNECT
 *                           V    |
 *                        (Connected)
 *                           |    ^
 *             CONNECT_AUDIO |    | DISCONNECT_AUDIO
 *                           V    |
 *                         (AudioOn)
 */

從分發的訊息分支很容易找到下面的方法

connectAudioNative(getByteAddress(device));

進入了Native層

private native boolean connectAudioNative(byte[] address);

packages/apps/Bluetooth/jni/com_android_bluetooth_hfp.cpp

static jboolean connectAudioNative(JNIEnv *env, jobject object, jbyteArray address) {
    jbyte *addr;
    bt_status_t status;

    if (!sBluetoothHfpInterface) return JNI_FALSE;

    addr = env->GetByteArrayElements(address, NULL);
    if (!addr) {
        jniThrowIOException(env, EINVAL);
        return JNI_FALSE;
    }

    if ( (status = sBluetoothHfpInterface->connect_audio((bt_bdaddr_t *)addr)) !=
         BT_STATUS_SUCCESS) {
        ALOGE("Failed HF audio connection, status: %d", status);
    }
    env->ReleaseByteArrayElements(address, addr, 0);
    return (status == BT_STATUS_SUCCESS) ? JNI_TRUE : JNI_FALSE;
}

分析到這裡關鍵在sBluetoothHfpInterface->connect_audio((bt_bdaddr_t *)addr))這個方法呼叫,進入到了藍芽協議棧

system/bt/btif/src/btif_hf.c

/*******************************************************************************
**
** Function         connect_audio
**
** Description     create an audio connection
**
** Returns         bt_status_t
**
*******************************************************************************/
static bt_status_t connect_audio( bt_bdaddr_t *bd_addr )
{
    CHECK_BTHF_INIT();

    int idx = btif_hf_idx_by_bdaddr(bd_addr);

    if ((idx < 0) || (idx >= BTIF_HF_NUM_CB))
    {
        BTIF_TRACE_ERROR("%s: Invalid index %d", __FUNCTION__, idx);
        return BT_STATUS_FAIL;
    }

    /* Check if SLC is connected */
    if (btif_hf_check_if_slc_connected() != BT_STATUS_SUCCESS)
        return BT_STATUS_NOT_READY;

    if (is_connected(bd_addr) && (idx != BTIF_HF_INVALID_IDX))
    {
        BTA_AgAudioOpen(btif_hf_cb[idx].handle);

        /* Inform the application that the audio connection has been initiated successfully */
        btif_transfer_context(btif_in_hf_generic_evt, BTIF_HFP_CB_AUDIO_CONNECTING,
                              (char *)bd_addr, sizeof(bt_bdaddr_t), NULL);
        return BT_STATUS_SUCCESS;
    }

    return BT_STATUS_FAIL;
}

system/bt/bta/ag/bta_ag_api.c

/*******************************************************************************
**
** Function         BTA_AgAudioOpen
**
** Description      Opens an audio connection to the currently connected
**                  headset or hnadsfree.
**
**
** Returns          void
**
************************************************************