[摘要]: 本文主要论述基于android 6.0的蓝牙上层(Java层)通话机制;总结了蓝牙通话框架,并且给出了接听电话的详细的流程图;最后说明了apk的实现以及总结了蓝牙/android 相关的知识点。
1, 蓝牙框架
主要代码路径:
路径1: frameworks\base\core\java\android\bluetooth\
蓝牙相关接口,蓝牙各种功能的发起点。
路径2:packages\apps\Bluetooth\src\com\android\bluetooth\
独立的Bluetooth.apk,里面包含蓝牙相关的各种服务,是java层和C/C++层的桥梁。
路径3: packages\apps\Bluetooth\jni\
调用底层C/C++实现各种蓝牙功能,并且反馈给java层。
在路径2里面还有各种相互独立的java代码包,每一个包都包含一个协议,实现一个具体的功能:
btservice: 统一管理,控制其他服务。
a2dp: 和蓝牙耳机,音频有关,比如听歌等。
avrcp: 音频/视频通过连接的蓝牙控制,比如放歌时控制暂停等。
gatt:低功耗BLE有关,比如蓝牙按键。
hdp: 蓝牙医疗有关
hfp和hfpclient : 蓝牙通话有关,比如蓝牙通话的相关操作
hid: 蓝牙键盘键盘/鼠标
map: 同步蓝牙短信相关
opp: 蓝牙传输,比如传输文件等
pan: 个人局域网
pbap: 同步电话本,比如联系人/通话记录等
sap : 蓝牙通话,主要和SIM卡相关
sdp: 蓝牙服务发现/获取相关
这12个包分别实现了12中蓝牙功能,大多数以服务的形式存在,运行在Bluetooth.apk中。不仅如此,还具有以下特点:
1,每一个服务相互独立,互相毫无任何影响, 继承自 ProfileService,由
AdapterService服务统一管理。
2,每一个服务在路径1中都存在对应的客户端类,通过Binder进行跨进程通信。
3,每一个服务在路径3中都存在对应的C/C++类,通过JNI机制互相调用。
4,每一个服务的启动,对应的Binder以及JNI机制的调用原理,方法,流程几乎都是一样的。
下面以蓝牙通话功能为例来解析相关接口以及代码实现框架图。
2 蓝牙通话框架
2.1 相关类的说明
蓝牙通话上层代码主要分为3个部分:
1,蓝牙api相关代码, 路径4: frameworks\base\core\java\android\bluetooth\
主要有2个类
BluetoothHeadsetClient.java主要负责蓝牙通话的相关动作,比如接听等等
BluetoothHeadsetClientCall.java主要负责蓝牙通话的状态,比如是来电还是去电等等。
2,蓝牙服务端的相关代码,路径5:
packages\apps\Bluetooth\src\com\android\bluetooth\hfpclient\
有3个类
HeadsetClientHalConstants.java类里面只是定义了一些int/boolean 类型的值。
HeadsetClientService.java从名字就知道它是一个服务,它的设计很有意思,里面还有一个BluetoothHeadsetClientBinder内部类,该内部类主要负责和
BluetoothHeadsetClient进行跨进程通信。另外,HeadsetClientService也是
BluetoothHeadsetClientBinder和HeadsetClientStateMachine之间的桥梁。
HeadsetClientStateMachine是一个状态机,即管理连接的状态也是通话时java和C/C++之间的桥梁,通过JNI机制和com_android_bluetooth_hfpclient 里面的方法互相调用。
3,JNI相关代码,路径6: packages\apps\Bluetooth\jni\
有1个文件:
com_android_bluetooth_hfpclient 蓝牙通话动作,拨号/接听/挂断/拒接 实际的执行者。
DialerBTHfpService服务是开机之后启动的。
2.2类图
图一 类图
BluetoothHeadsetClient是一个api,由第三方apk直接调用,可以进行拨号/接听/拒接/挂断操作,对应的方法依次为dial()/acceptCall()/rejectCall()/terminateCall().
HeadsetClientService是一个服务, BluetoothHeadsetClientBinder是它的内部类, BluetoothHeadsetClientBinder是BluetoothHeadsetClient在HeadsetClientService中的代理,通过aidl进行方法的调用。
Connected(已连接状态)是HeadsetClientStateMachine 的其中一种状态,通话的相关操作都建立在已连接状态之上,其它三种状态为Disconnected, Connecting,
AudioOn状态。HeadsetClientService 根据不同的方法给状态机发送不同的消息,最后通过HeadsetClientStateMachine根据JNI机制调用
com_android_bluetooth_hfpclient.cpp对应的方法,最后调用底层C/C++ 来真正的实现拨号/接听/拒接/挂断操作。
拨号/接听/拒接/挂断方法调用的流程完全是一模一样的,下小节给出接听方法具体调用的完整流程图。
2.3流程图
图二 接听电话流程图
除了dial 方法最后调用从C/C++ dialNative之外,其它的3个方法最后都是调用handleCallActionNative,只是参数不同而已。
拨号/接听/拒接/挂断都是主动完成的,那么如果有来电,对方接通电话或者对方挂断电话,我们怎么知道呢?这些都是com_android_bluetooth_hfpclient.cpp通过JNI机制调用通话状态机的方法sendCallChangedIntent,将电话的状态(包含在
BluetoothHeadsetClientCall.java中)通过广播发送出来,第三方apk监听状态就可以进行相应的操作了。具体的来电流程图如下:
图三 来电流程图
3 蓝牙通话apk说明
在自己的apk中,只需要做2件事情就可以完成蓝牙通话的几乎所有动作,
1,根据上一小节的论述,注册BluetoothHeadsetClientCall相关广播,监听来电/接通/对方挂断的状态,获取蓝牙电话的相关信息,比如号码,设备信息等等。
比如,如果收到来电广播,就可以根据BluetoothHeadsetClientCall获取来电的号码等信息,然后显示来电界面。
2,通过BluetoothAdapter 获取并且初始化BluetoothHeadsetClient对象,然后就可以直接调用dial()/acceptCall()/rejectCall()/terminateCall() 方法进行拨号/接听/拒接/挂断的操作了。
4, 小节
利用api实现蓝牙通话不是很难,但是如果弄清楚具体的蓝牙通话机制以及细节甚至蓝牙相关功能,这就得下功夫了,关于蓝牙,还可以进一步研究的android知识点以及蓝牙涉及的上层java要点如下:
1,基本蓝牙功能:打开/关闭/扫描/配对/连接
2,蓝牙功能:通过蓝牙传输文件/蓝牙键盘/蓝牙医疗服务/蓝牙同步联系人等等
3,android知识:跨进程通信机制/JNI机制/反射机制/状态机机制等等。
实际开发过程
公司用的是android8.1的源码,系统api有改动,改动的地方会稍微标明一下。我是在系统源码上开发的,所以有些类或者api@hide了 在开发工具上会报错,但是可以编译通过。如果是纯应用上层需要利用反射,有一部分功能需要移植代码。
车载蓝牙主要是实现蓝牙电话,蓝牙音乐,同步通讯录。这些功能都是用到蓝牙的配置文件协议。下面简单介绍一下这几个协议。
1.HFP(Hands-free Profile),让蓝牙设备可以控制电话,如接听、挂断、拒接、语音拨号等,拒接、语音拨号要视蓝牙耳机及电话是否支持。
2.A2DP(Advanced Audio Distribution Profile)是蓝牙的音频传输协议,典型应用为蓝牙耳机。A2DP协议的音频数据在ACL Link上传输。
3.AVRCP(Audio/Video Remote Control Profile)作用是支持CT控制TG,具体来说如果手机和一个蓝牙音箱设备连接上了,那么音箱可以控制手机播放/暂停/切歌以及获得手机上播放歌曲的信息,如专辑,歌名,歌手,时长等信息。
4.PBAP(Phone Book Access Profile)访问下载通讯录以及通话记录。
以上是简单介绍这些协议的用途,想具体了解可以分别取查资料,这方面资料很多。
一.打开蓝牙,查找蓝牙设备,连接蓝牙协议。
要使用蓝牙必须先在文件清单加权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
检测设备蓝牙是否打开,没有可以自动打开,也可以请求,给用户提示,让用户自己打开。
查找蓝牙之前可以先查看是否有以前绑定过的蓝牙设备
Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
if (devices != null && devices.size() > 0) {
for (Iterator<BluetoothDevice> it = devices.iterator(); it.hasNext();) {
BluetoothDevice device = it.next();
if (device != null) {
addBluetoothDevice(device);
}
}
}
mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = mBluetoothManager.getAdapter();
if (!mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.enable();
}
查找蓝牙可以先查找低功耗蓝牙,一般查找时间为8-10秒,然后再查找经典蓝牙。两种查找用的api不同,先说一下查找低功耗蓝牙设备。
拿到BluetoothLeScanner:mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
调用startScan,参数为ScanCallback对象,它里面有各种回调,根据自己的需求实现。
private final ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
super.onScanResult(callbackType, result);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
BluetoothDevice device = result.getDevice();
Log.i(TAG, "scan succeed device == " +device);
addBluetoothDevice(device);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
for (ScanResult result: results){
Log.i(TAG, "scan succeed device == " +result.getDevice());
addBluetoothDevice(result.getDevice());
}
}
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
Log.i(TAG,"scan fail");
}
};
查看经典蓝牙直接使用BluetoothAdapter的startDiscovery方法,注册BluetoothDevice.ACTION_FOUND广播接收搜索到的蓝牙设备。使用intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)可以得到BluetoothDevice。
接着连接设备,连接设备之前需要绑定设备,就是确认双方设备的存在通过相同秘钥,如果需要访问通讯录,需要勾选权限。
可以先判断BluetoothDevice的状态,如果状态为BluetoothDevice.BOND_NONE,调用bluetoothDevice.createBond()进行绑定。
讲连接之前说一下自己踩过的坑,也不能说是坑,自己能力不足所以花了不少时间。开始我以为连接蓝牙需要socket通讯,查google文档说要 两台设备一台充当客户端一台充当服务端,然后用io流传输数据。我一直在纠结,我只能操作车载应用,用户手机安装不了我开发的应用,那这怎么连接呢。后来看到系统setting有连接蓝牙的效果就果断去setting的Bluetooth模块查看连接蓝牙的代码,终于找到了。在frameworks\base\packages\SettingsLib\src\com\android\settingslib\bluetooth中专门有个类存储管理蓝牙设备CachedBluetoothDevice.java,他连接远程蓝牙设备就是
synchronized void connectInt(LocalBluetoothProfile profile) {
if (!ensurePaired()) {
return;
}
if (profile.connect(mDevice)) {
if (Utils.D) {
Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile));
}
return;
}
Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName);
}
LocalBluetoothProfile是一个接口,各种协议的封装者实现了该接口,例如HfpClientProfile它里面封装的是BluetoothHeadsetClient,就是HFP协议。主要看它的connect方法
@Override
public boolean connect(BluetoothDevice device) {
if (mService == null) return false;
List<BluetoothDevice> srcs = getConnectedDevices();
if (srcs != null) {
for (BluetoothDevice src : srcs) {
if (src.equals(device)) {
// Connect to same device, Ignore it
Log.d(TAG,"Ignoring Connect");
return true;
}
}
}
return mService.connect(device);
}
先获取已连接的蓝牙设备,如果蓝牙设备已连接返回true,不在列表中则调用BluetoothHeadsetClient的connect方法。接着看下这个BluetoothHeadsetClient怎么实例化。
mLocalAdapter.getProfileProxy(context, new HfpClientServiceListener(),
BluetoothProfile.HEADSET_CLIENT);
通过BluetoothAdapter的getProfileProxy方法去请求这个协议对象。
private final class HfpClientServiceListener
implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (V) Log.d(TAG,"Bluetooth service connected");
mService = (BluetoothHeadsetClient) proxy;
// We just bound to the service, so refresh the UI for any connected HFP devices.
List<BluetoothDevice> deviceList = mService.getConnectedDevices();
while (!deviceList.isEmpty()) {
BluetoothDevice nextDevice = deviceList.remove(0);
CachedBluetoothDevice device = mDeviceManager.findDevice(nextDevice);
// we may add a new device here, but generally this should not happen
if (device == null) {
Log.w(TAG, "HfpClient profile found new device: " + nextDevice);
device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice);
}
device.onProfileStateChanged(
HfpClientProfile.this, BluetoothProfile.STATE_CONNECTED);
device.refresh();
}
mIsProfileReady=true;
}
@Override
public void onServiceDisconnected(int profile) {
if (V) Log.d(TAG,"Bluetooth service disconnected");
mIsProfileReady=false;
}
}
监听返回状态,赋值。其实不仅仅是BluetoothHeadsetClient,BluetoothA2dpSink等等请求方式一模一样,参数不同。看到这个之后恍然大悟,车载蓝牙实现的功能不需要进行socket通信,只需要连接这些协议。
好了,整理一下连接过程,先拿到BluetoothAdapter,调用getProfileProxy方法请求协议,第三个参数为协议的种类,都定义在BluetoothProfile中,例如BluetoothProfile.AVRCP_CONTROLLER(蓝牙控制协议),BluetoothProfile.A2DP_SINK(音频协议)。实现BluetoothProfile.ServiceListener接口,监听返回状态。如果返回成功得到对象,就可以调用connect(BluetoothDevice device)连接。
mAdapter = BluetoothAdapter.getDefaultAdapter();
mAdapter.getProfileProxy(mContext, new ProxyServiceListener(), BluetoothProfile.AVRCP_CONTROLLER);
mAdapter.getProfileProxy(mContext, new ProxyServiceListener(), BluetoothProfile.A2DP_SINK);
private final class ProxyServiceListener implements BluetoothProfile.ServiceListener {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
Log.d(TAG,"Bluetooth service connected profile == "+profile);
if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
isAvrcpProfileReady = true;
mAvrcpCt = (BluetoothAvrcpController)proxy;
Log.d(TAG, "AvrcpController Profile Proxy Connected");
}
if (profile == BluetoothProfile.A2DP_SINK) {
isA2dpProfileReady = true;
mA2dpSink = (BluetoothA2dpSink)proxy;
Log.d(TAG, "BluetoothA2dpSink Profile Proxy Connected");
}
}
@Override
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.AVRCP_CONTROLLER) {
isAvrcpProfileReady = false;
mAvrcpCt = null;
Log.d(TAG, "AvrcpController Profile Proxy Disconnected");
}
if (profile == BluetoothProfile.A2DP_SINK) {
isA2dpProfileReady = false;
mA2dpSink = null;
Log.d(TAG, "BluetoothA2dpSink Profile Proxy Disconnected");
}
}
}
public boolean connect(BluetoothDevice device) {
if (null != mA2dpSink) {
return mA2dpSink.connect(device);
}
Log.i(TAG, "mA2dpSink == null");
return false;
}