基于Android P版本分析
蓝牙电话簿
蓝牙电话可以分为两个部分:
- 蓝牙电话簿
- 蓝牙通话
蓝牙电话簿可能不是必须的,但是只要支持蓝牙电话功能,则蓝牙通话则必须支持;
蓝牙电话簿协议PBAP,分为了两个角色:
- PSE:手机端,作为服务端
- PCE:车机端,作为客户端
我们主要关注PCE侧的连接流程;
相关协议
Protocol | Description |
---|---|
PBAP | 电话本访问协议,是一种基于OBEX的上层协议,该协议可以同步手机这些具有电话本功能设备上的通讯录和通话记录等信息 |
SDP | 蓝牙服务发现协议,为应用程序提供了发现服务以及确定哪些服务属性是可以利用的能力 |
RFCOMM | 简单传输协议,其目的为了解决如何在两个不同设备上的应用程序之间保证一条完整的通信路径,并在它们之间保持一通信段的问题。RFCOMM协议提供对基于L2CAP协议的串口仿真 |
OBEX | 对象交换协议,用于在蓝牙设备间传数据对象,使用Request-Response形式实现在不同的设备、不同的平台之间方便、高效的交换信息 |
架构

基于SDP协议用于发现PSE端服务,基于OBEX协议,使用request-response形式传输数据,通过RFCOMM协议创建channel来传递数据;
类图

上述类图中,主要有几个关键类:
- PbapClientService:PbapClient服务,通过Binder机制对外提供服务PbapClient支持的各种功能;
- PbapClientStateMachine:PbapClient的状态机,用于处理在各个状态下的消息处理逻辑;
- PbapClientConnectionHandler:用于处理PBAP连接、断开、同步数据等操作的核心类;
- ClientOperation:客户端操作类,用于提供操作接口,其本质还是通过ClientSession实现的;
- ClientSession:继承自ObexSession,是OBEX协议使用的Session,用于响应Request和返回Response;
- BluetoothPbapRequest:这个就是用于加载电话簿信息的request请求类,是BluetoothPbapRequestPullPhoneBook的基类,提供了execute方法用于发送request;
- BluetoothPbapRequestPullPhoneBook:BluetoothPbapRequest的子类,专门为PhoneBook封装了一个Request请求类,用于请求PhoneBook数据;
- BluetoothPbapVcardList:对应于Request的Response,用于接收Request响应完成后获取到的电话簿数据信息;
- CallLogPullRequest:用于请求通话记录的Request;
- VCardEntry:用于记录联系人/通话记录信息的实体类;
PBAP 连接
PbapClientService的加载(startService)基本上和其他的ProfileService的加载逻辑一致,都是通过startService方式进行启动;

PbapClientService启动之后,创建了PbapClientService的代理,就可以等待车机端应用进程连接该服务了,然后通过代理对象来访问PbapClientService;

在连接过程中,其实主要做的就是创建PbapClient对应的状态机PbapClientStateMachine,然后调用start()方法来开启状态机,PbapClient状态机和其他Service的状态机有一点不同, 该状态机的初始化状态就是Connecting,而不是Disconnected;
RFCOMM & OBEX
PbapClientService启动之后,紧接着就是连接Service,创建和启动对应的状态机,而在状态机启动过程中,进行了创建各种实例以及状态机跳转的处理。
首先,我们先看一下在这个连接过程中,PbapClientStateMachine创建和启动过程中执行了哪些操作;

大致可以分为4个步骤:
- 创建SDPBroadcastReceiver广播监听器监听BluetoothDevice.ACTION_SDP_RECORD广播并执行sdpSearch方法;
- SDPBroadcastReceiver接收到BluetoothDevice.ACTION_SDP_RECORD广播,创建RfcommSocket并连接以及根据创建的Socket的基础上连接OBEX Session,其中OBEX为对象交换协议;
- 将状态机从Connecting跳转到Connected状态;
- 创建HandlerThread以及对应的PbapClientConnectionHandler,用于处理Connect过程中的消息,通过该Handler与真正的逻辑实现进行交互;
完整的PBAP连接流程分为以下三部分:

SDP服务发现
我发起某个协议的连接,必须先进行SDP服务搜索,发现对端设备支持该协议才能继续连接流程;
主要是建立PSM=SDP的l2cap链路,然后在该链路上搜索Phonebook Access-PSE的服务,PSE如果支持该服务会将该服务绑定的channel返回给PCE端。最后将该l2cap链路断开;
RFCOMM连接
在SDP服务中根据获取到的搜索结果选择L2cap还是RFCOMM去建立链路连接,其实本质上还是L2cap的连接,因为蓝牙的所有数据交互都是基于L2cap链路,处理电话语音数据走的是sco链路。我们通过代码分析,知道是通过RFCOMM连接,通过对端设备提供的channel通道信号去连接该协议的RFCOMM通道,为PBAP(OBEX)的连接做准备;
PBAP连接
使用OBEX连接的数据格式构造数据,通过层层的包头封装成不同的协议数据格式最后通过RFCOMM建立起来的链路发送给PSE请求建立连接;
上述3个步骤执行完成之后,PBAP协议就连接成功了;
电话簿下载

我们关注一下Download和Browsing两种功能;
- Download:用于下载电话簿对象的全部内容;
- Browsing:用于需要滚动浏览电话簿的应用程序;

由于Download功能可以将电话簿的全部内容同步到PCE,从而PCE端获取到数据后完全可以通过蓝牙电话等应用程序将数据显示到界面,一样可以达到滚动浏览电话簿信息的目的;
Download这个功能特别适用于PSE端存储的电话簿容量相对较大,PCEdaunt设备通常从PSE端下载这些大容量数据并在其本地存储整个电话簿的场景下,PullPhonebook函数就是用来下载自己感兴趣的电话簿对象;

由于PBAP协议是基于OBEX的,所以PUllPhonebook函数顾名思义也是采用request-response形式传输数据;

PBAP的连接分为连接请求和连接响应,在PBAP协议中一般是PCE主动发起连接请求,PSE响应该连接请求,这也符合Client-Server设计原则。因为电话簿源数据都是存储在PSE中的,只有PCE需要通过PBAP协议来将这些数据同步过来,所以PSE等待PCE的请求并做出响应;
在这个电话簿下载过程中,使用到了request-response形式;
- request:BluetoothPbapRequestPullPhoneBook,该类继承自BluetoothPbapRequest,其中包含了execute方法用于发送请求并接收response;
- response:BluetoothPbapVcardList,在BluetoothPbapRequestPullPhoneBook类中通过readResponse的方法获取到request请求响应后的结果,其中包含的就是电话簿信息;
PATH 确认
同时,在加载电话簿的时候,需要确定加载的path,而这些path定义在PbapClientConnectionHandler中;
Phone Path
path | value | desc |
---|---|---|
PB_PATH | telecom/pb.vcf | 手机联系人 |
MCH_PATH | telecom/mch.vcf | 手机未接电话记录 |
ICH_PATH | telecom/ich.vcf | 手机来电记录 |
OCH_PATH | telecom/och.vcf | 手机去电记录 |
CCH_PATH | telecom/cch.vcf | 手机所有通话记录 |
Phone SIM Path
path | value | desc |
---|---|---|
SIM1_PB_PATH | SIM1/telecom/pb.vcf | SIM卡联系人 |
SIM1_MCH_PATH | SIM1/telecom/mch.vcf | SIM卡未接电话记录 |
SIM1_ICH_PATH | SIM1/telecom/ich.vcf | SIM卡来电记录 |
SIM1_OCH_PATH | SIM1/telecom/och.vcf | SIM卡去电记录 |
SIM1_CCH_PATH | SIM1/telecom/cch.vcf | SIM卡所有通话记录 |
在加载信息过程中,会通过传入的vcf path来决定加载哪一部分信息;
电话簿下载

这个过程,主要涉及到了请求的发送和请求的响应处理,其中涉及到了两个类:VCardEntryConstructor和VCardEntryCounter,其中涉及到了一个关键类:VCardEntry,用于描述所有的联系人/通话记录的各种信息,其中包含了一个枚举:EntryLabel;
arduino
public enum EntryLabel {
NAME, // 联系人方式
PHONE, // 联系方式
EMAIL, // 邮件
POSTAL_ADDRESS, // 通讯地址
ORGANIZATION, // 组织机构
IM, //
PHOTO, // 头像
WEBSITE, // 网址
SIP, //
NICKNAME, // 昵称
NOTE, // 记录
BIRTHDAY, // 生日
ANNIVERSARY, // 周年纪念
ANDROID_CUSTOM // Android 定制
}
上述枚举列举了所有的联系人/通话记录的各种信息属性,其中每一个元素对应一个EntryElement,EntryElement描述了具体每一个枚举包含的一些信息,例如NAME,其中包含了姓氏、名称和全称;
EntryLabel & EntryElement的映射关系
EntryLabel | EntryElement |
---|---|
NAME | NameData |
PHONE | PhoneData |
EmailData | |
POSTAL_ADDRESS | PostalData |
ORGANIZATION | OrganizationData |
IM | ImData |
PHOTO | PhotoData |
WEBSITE | WebsiteData |
SIP | SipData |
NICKNAME | NicknameData |
NOTE | NoteData |
BIRTHDAY | BirthdayData |
ANNIVERSARY | AnniversaryData |
ANDROID_CUSTOM | AndroidCustomData |
上述的时序图和Android原生的有点不同的是,在B16的PbapClientStateMachine中,通过发送MSG_PULL_PHONEBOOK消息来触发,Android P原生是通过MSG_DOWNLOAD消息来触发的,而在处理MSG_DOWNLOAD消息的时候,直接调用了BluetoothPbapRequestPullPhoneBook的execute,B16中则是通过handler的方式处理的;