Android AOSP 6.0.1 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 控制器,组合控制器共享一个蓝牙设备地址

蓝牙规格通过定义等效层之间交换的协议信息来实现系统之间的互操作性。它还通过定义蓝牙控制器和蓝牙主机之间的公用接口来实现独立蓝牙子系统之间的互操作性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTOd7s0b-1583930527112)(https://www.bluetooth.com/~/media/images/page-content/bluetooth%20stack.ashx?h=501&w=388&la=zh-cn&hash=9756388D40595EB444E4B110F3587A323604714A)]

物理 (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蓝牙架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uPOKhP6G-1583930527117)(https://source.android.com/devices/bluetooth/images/ape_fwk_bluetooth.png)]

蓝牙系统服务通过JNI与蓝牙协议栈,通过Binder IPC和应用进行通信。系统服务为开发人员提供了访问各种蓝牙配置文件的机会。

电话中是如何建立SCO连接的?

通过电话接通后,选择蓝牙音频输出入口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XrRDnrQI-1583930527118)(http://7sbojw.com1.z0.glb.clouddn.com/callButton_audio_more.png?imageMogr2/thumbnail/600)]

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
**
*******************************************************************************/
void BTA_AgAudioOpen(UINT16 handle)
{
    BT_HDR  *p_buf;

    if ((p_buf = (BT_HDR *) GKI_getbuf(sizeof(BT_HDR))) != NULL)
    {
        p_buf->event = BTA_AG_API_AUDIO_OPEN_EVT;
        p_buf->layer_specific = handle;
        bta_sys_sendmsg(p_buf);
    }
}

system/bt/stack/include/bt_types.h

/* Define the header of each buffer used in the Bluetooth stack.*/
typedef struct
{
    uint16_t          event;
    uint16_t          len;
    uint16_t          offset;
    uint16_t          layer_specific;
    uint8_t           data[];
} BT_HDR;

system/bt/bta/sys/bta_sys_main.c

/*******************************************************************************
**
** Function         bta_sys_sendmsg
**
** Description      Send a GKI message to BTA.  This function is designed to
**                  optimize sending of messages to BTA.  It is called by BTA
**                  API functions and call-in functions.
**
**
** Returns          void
**
*******************************************************************************/
void bta_sys_sendmsg(void *p_msg)
{
    // There is a race condition that occurs if the stack is shut down while
    // there is a procedure in progress that can schedule a task via this
    // message queue. This causes |btu_bta_msg_queue| to get cleaned up before
    // it gets used here; hence we check for NULL before using it.
    if (btu_bta_msg_queue)
        fixed_queue_enqueue(btu_bta_msg_queue, p_msg);
}

system/bt/osi/src/fixed_queue.c

void fixed_queue_enqueue(fixed_queue_t *queue, void *data) {
  assert(queue != NULL);
  assert(data != NULL);

  semaphore_wait(queue->enqueue_sem);

  pthread_mutex_lock(&queue->lock);
  list_append(queue->list, data);
  pthread_mutex_unlock(&queue->lock);

  semaphore_post(queue->dequeue_sem);
}

后续进入到了linux内核,后续步骤可以猜得到,经过蓝牙驱动让蓝牙模块Host下发指令:Setup Synchronous Connection

APP建立SCO信道控制代码

实现蓝牙耳机录音功能

打开sco,关键代码:

AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
mAudioManager.startBluetoothSco();

关闭sco:

mAudioManager.stopBluetoothSco();

参考资料:

1.https://zhuanlan.zhihu.com/p/21943377

2.http://blog.csdn.net/xubin341719/article/details/38305331

3.http://blog.csdn.net/vnanyesheshou/article/details/71374935

4.http://blog.csdn.net/aaa111/article/details/50364061

5.http://androidxref.com

tyyj90 CSDN认证博客专家 安卓全栈开发
Android高级开发工程师,熟悉App开发,熟悉Framework定制开发,略了解Linux内核。开发使用的语言涉及Java、Kotlin、C/C++、Python和汇编NEON指令集。
©️2020 CSDN 皮肤主题: 像素格子 设计师:CSDN官方博客 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值