[摘要]: 本文主要论述基于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个类

车机Android 蓝牙电话 车载蓝牙电话系统_android

 

BluetoothHeadsetClient.java主要负责蓝牙通话的相关动作,比如接听等等

BluetoothHeadsetClientCall.java主要负责蓝牙通话的状态,比如是来电还是去电等等。

 

2,蓝牙服务端的相关代码,路径5:

packages\apps\Bluetooth\src\com\android\bluetooth\hfpclient\

有3个类

车机Android 蓝牙电话 车载蓝牙电话系统_android_02

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个文件: 

车机Android 蓝牙电话 车载蓝牙电话系统_java_03

 

com_android_bluetooth_hfpclient  蓝牙通话动作,拨号/接听/挂断/拒接 实际的执行者。

 

DialerBTHfpService服务是开机之后启动的。

 

 

 

2.2类图

车机Android 蓝牙电话 车载蓝牙电话系统_java_04

图一 类图

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流程图

车机Android 蓝牙电话 车载蓝牙电话系统_车机Android 蓝牙电话_05

图二 接听电话流程图

除了dial 方法最后调用从C/C++ dialNative之外,其它的3个方法最后都是调用handleCallActionNative,只是参数不同而已。

拨号/接听/拒接/挂断都是主动完成的,那么如果有来电,对方接通电话或者对方挂断电话,我们怎么知道呢?这些都是com_android_bluetooth_hfpclient.cpp通过JNI机制调用通话状态机的方法sendCallChangedIntent,将电话的状态(包含在

BluetoothHeadsetClientCall.java中)通过广播发送出来,第三方apk监听状态就可以进行相应的操作了。具体的来电流程图如下:

车机Android 蓝牙电话 车载蓝牙电话系统_车机Android 蓝牙电话_06

 图三 来电流程图

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;
	}