解析蓝牙原理

1.前言

市面上关于Android的技术书籍很多,几乎每本书也都会涉及到蓝牙开发,但均是上层应用级别的,而且篇幅也普遍短小。对于手机行业的开发者,要进行蓝牙模块的维护,就必须从Android系统底层,至少框架层开始,了解蓝牙的结构和代码实现原理。这方面的文档、网上的各个论坛的相关资料却少之又少。分析原因,大概因为虽然蓝牙协议是完整的,但是并没有具体的实现。蓝牙芯片公司只负责提供最底层的API,与上层的适配和其他元件的兼容,需要各个厂家自己去实现,因此并未出现适用非常广泛的标准API供各个领域的公司使用。而实现了自己适配的公司,出于技术的保护又很少公开相关技术代码或者资料。

作为android手机系统应用维护工程师,初学蓝牙模块也深感资料匮乏。阅读MTK的PPT,总是过分简略不够深入。阅读代码当然是好办法,但是没有指导,容易因理解不到位而出错和绕弯路,难免费时费力。基于这种现状,我将自己的蓝牙学习、代码分析总结出来,形成此文,一来梳理自身的蓝牙技术知识,而来贡献力量将本Team的知识积累建设得更加到位。希望后来者有文档可依,学习上手能更加便捷。由于作者水平有限,文字和理解的勘误难免,如果能互相指教提高,便是最大的荣幸了!

阅读本文后面详细分析,推荐的方法是打开一套工程源码,一边利用本文粘贴出来的代码和对应的说明文字,一边利用工程源码,对照阅读。这样遇到跳转的时候可以直接操作,不至于跟丢致使茫然无从。文字总无法一一俱到,遇到部分没有讲到的但或许对于特定读者却有疑惑的地方,请使用手边的源码认真分析。这样在作者看来学习提高是比较快的。

2.技术起源和名称来历

蓝牙(Bluetooth)是一种短距离的无线通信技术标准。它最初由瑞典的爱立信公司创制,技术始于公司的1994方案,后来由蓝牙技术联盟订定技术标准,1999年7月26日正式公布1.0版。蓝牙名字来源于10世纪丹麦国王Harald Blatand,英文名字是Harold Bluetooth。无线行业协会组织人员进行讨论,有人认为用Blatand国王的名字命名这种无线技术再好不过了,因为Blatand国王将挪威、瑞典和丹麦统一起来,就如同这项技术将统一无线通信领域一样。因此,蓝牙的名字就这样定了下来。它的标志由H和B两个字母合成,如下图:
蓝牙标志示意图
图1 蓝牙标志示意图<喎�"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMSBpZD0="3技术规格">3.技术规格

蓝牙采用了分散式网络结构以及快跳频和短包技术,支持点对点及点对多点的通信。

3.1射频与基带部分

Bluetooth设备工作在全球通用的2.4GHz的ISM(Industrial,Science and Medicine)频段,在北美和欧洲为2400~2483.5MHz,使用79个频道,载频为2402+kMHz(k=0,1…,22)。无论是79个频道还是23个频道,频道间隔均为1MHz,采用时分双工(TDD,TimeDivision Duplex)方式。调制方式为BT=0.5的GFSK,调制指数为0.28~0.35,最大发射功率分为三个等级,分别是:100mW(20dBm),2.5mW(4dBm)和1mW(0dBm),在4~20dBm范围内要求采用功率控制,因此,Bluetooth设备间的有效通信距离大约为10~100米。

Bluetooth的基带符号速率为1Mb/s,采用数据包的形式按时隙传送,每时隙长0.625ūs,不排除将来采用更高的符号速率。Bluetooth系统支持实时的同步面向连接传输和非实时的异步面向非连接传输,分别成为SCO链路(Synchronous Ccnnection-Oriented Link)和ACL链路(Asynchronous Connection-Less Link),前者只要传送语音等实时性强的信息,在规定的时隙传输,后者则以数据为主,可在任意时隙传输。但当ACL传输占用SCO的预留时隙,一旦系统需要SCO传输,ACL则自动让出这些时隙以保证SCO的实时性。数据包被分成3大类:链路控制包、SCO包和ACL包。已定义了4钟链路控制数据包,后两者最多可分别定义12种,目前已定义了4种和7种,即共定义了15种。大多数数据包只占用1个时隙,但有些包占用3个或5个时隙。

Bluetooth支持64kb/s的实时语音传输和各种速率的数据传输,语音编码采用对数PCM或连续可变斜率增量调制(CVSD,Continuous Variable Slope Delta Modulation)。语音和数据可单独或者同时传输。当仅传输语音时,Bluetooth设备最多可同时支持3路全双工的语音通信;当语音和数据同时传输或仅传输数据时,Bluetooth设备支持433.9kb/s的对称全双工通信或723.2/57.6kb/s的非对称双工通信,另外,采用CRC(Cyclic Redundancy Check)、FEC(Forward Error Correction)及ARQ(Automatic Repeat Request),保证了通信的可靠性。

跳频是Bluetooth使用的关键技术之一,对应于单时隙包,Bluetooth的跳频速率为1600跳每秒;对应于多时隙包,跳频速率有所降低;但在建链时(包括寻呼和查询)则提高为3200跳每秒。使用这样高的跳频速率,Bluetooth系统具有足够高的抗干扰能力。

跳频序列受控于Bluetooth 48-bit设备地址码(BD——ADDR)中的28-bit和28-bit的时钟,采用以多级碟形运算为核心的映射方案。该跳频方案和其他方案相比,具有硬件设备简单、性能优越、便于79/23频段两种系统的兼容以及各种状态的跳频序列使用统一的电路来实现等特点。

<h2 id="32组网技术">3.2组网技术

Bluetooth根据网络的概念提供点对点和点对多点的无线链接,在任意一个有效通信范围内,所有设备的地位都是平等的。首先提出通信要求的设备称为主设备(Master),被动进行通信的设备称为从设备(Slave)。利用TDMA,一个Master最多可同时与7个Slave进行通信并和多个Slave(最多可超过200个)保持同步但不通信。一个Master和一个以上的Slave构成的网络称为Bluetooth的主从网络(Piconet)。若两个以上的Piconet之间存在着设备间的通信,则构成了Bluetooth的分散网络(Scatternet)。基于TDMA原理和Bluetooth设备的平等性,任意Bluetooth设备在Piconet和Scatternet中,既可作Master,又可作Slave,还可同时既是Master又是Slave。因此,在Bluetooth中没有基站的概念。另外,所有设备都是可移动的。

Bluetooth的基本出发点是可使其设备能够在全球范围内应用于任意的小范围通信。任一Bluetooth设备,都可根据IEEE 802标准得到一个唯一的48-bit的BD_ADDR,它是一个公开的地址码,可以通过人工或自动进行查询。在BD_ADDR基础上,使用一些性能良好的算法可获得各种保密和安全码,从而保证了设备识别码(ID,Identification)在全球的唯一性,以及通信过程中设备的鉴权和通信的安全保密。

4.蓝牙规范

蓝牙规范(Specification of the Bluetooth System)就是蓝牙无线通信协议标准,它规定了蓝牙应用产品应遵循的标准和需要达到的要求,由SIG颁布。

蓝牙规范包括核心协议(Core)与应用框架(Profiles)两个文件。协议规范部分定义了蓝牙的各层通信协议,应用框架指出了如何采用这些协议实现具体的应用产品。蓝牙协议规范遵循开放系统互连参考模型(Open System Interconnetion/Referenced Model, OSI/RM),从低到高地定义了蓝牙协议堆栈的各个层次。

5.协议堆栈

5.1概述

蓝牙栈的目的是什么呢?栈是控制蓝牙设备的软件(和固件)。蓝牙协议堆栈低层为各类应用所通用,高层则视具体应用而有所不同,大体上分为计算机背景和非计算机背景两种方式,前者通过主机控制接口(HCI,Host Controller Interface)实现高、低层的联接,后者则不需用HCI。层次结构使其设备具有最大可能的通用性和灵活性。根据通信协议,各种Bluetooth设备无论在任何地方,都可以通过人工或自动查询来发现其他Bluetooth设备,从而构成Piconet或Scatternet,实现系统提供的各种功能,使用起来十分方便。

5.2协议体系结构

蓝牙协议堆栈按照功能分为4层:

核心协议层(BaseBand、LMP、L2AP、SDP)
线缆替换协议层(RFCOMM)
电话控制协议层(TCS-BIN、AT命令集)
选用协议层(PPP、TCP、IP、UDP、OBEX、IrMC、WAP、WAE)

这里写图片描述
除上述协议层外,规范还定义了主机控制器接口(HCI),它为基带控制器、连接管理器、硬件状态和控制寄存器提供命令接口。在图1中,HCI位于L2CAP的下层,但HCI也可位于L2CAP上层。

蓝牙核心协议由SIG制定的蓝牙专用协议组成。绝大部分蓝牙设备都需要核心协议(加上无线部分),而其他协议则根据应用的需要而定。总之,电缆替代协议、电话控制协议和被采用的协议在核心协议基础上构成了面向应用的协议。

5.2.1核心协议

核心协议包括基带、链路管理、逻辑链路控制和适应协议四部分。

(1)基带(BaseBand)协议

描述了完成底层链路建立维护和执行基带协议的链路控制器的规范;基带和链路控制层确保微微网内各蓝牙设备单元之间由射频构成的物理连接。蓝牙的射频系统是一个跳频系统,其任一分组在指定时隙、指定频率上发送。它使用查询和分页进程同步不同设备间的发送频率和时钟,为基带数据分组提供了两种物理连接方式,即面向连接(SCO)和无连接(ACL),而且,在同一射频上可实现多路数据传送。ACL适用于数据分组,SCO适用于话音以及话音与数据的组合,所有的话音和数据分组都附有不同级别的前向纠错(FEC)或循环冗余校验(CRC),而且可进行加密。此外,对于不同数据类型(包括连接管理信息和控制信息)都分配一个特殊通道。

可使用各种用户模式在蓝牙设备间传送话音,面向连接的话音分组只需经过基带传输,而不到达L2CAP。话音模式在蓝牙系统内相对简单,只需开通话音连接就可传送话音。

(2)链路管理协议(Link Manager Protocol)

负责蓝牙组件间连接的建立,定义了链路的建立与控制的规范,在接收层信号由解释及过滤;该协议负责各蓝牙设备间连接的建立。它通过连接的发起、交换、核实,进行身份认证和加密,通过协商确定基带数据分组大小。它还控制无线设备的电源模式和工作周期,以及微微网内设备单元的连接状态。

(3)逻辑链路控制与适配协议(Logical Link Control and Adaptation Protocol)

位于基带(BaseBand)协议层上,属于数据链路层,是一个为高层传输和应用层协议屏蔽基带协议的适配协议,支持高层协议复用、数据包分段重组、QoS信息服务并获得相应的信息;该协议是基带的上层协议,可以认为它与LMP并行工作,它们的区别在于,当业务数据不经过LMP时,L2CAP为上层提供服务。L2CAP向上层提供面向连接的和无连接的数据服务,它采用了多路技术、分割和重组技术、群提取技术。L2CAP允许高层协议以64k字节长度收发数据分组。虽然基带协议提供了SCO和ACL两种连接类型,但L2CAP只支持ACL。

(4) 服务发现协议(SDP)

在蓝牙技术框架中起着至关紧要的作用,它是所有用户模式的基础。使用SDP可以查询到设备信息和服务类型,从而在蓝牙设备间建立相应的连接。

5.2.2电缆替代协议

电缆替代协议(RFCOMM)是ETSITS07.10的子集,提供L2CAP之上的串口防真。它在蓝牙基带协议上仿真RS-232控制和数据信号,为使用串行线传送机制的上层协议(如OBEX)提供服务。

5.2.3电话控制协议层

(1) 二元电话控制协议(TCS-Binary或TCSBIN)是面向比特的协议,它定义了蓝牙设备间建立语音和数据呼叫的控制信令,定义了处理蓝牙TCS设备群的移动管理进程。基于ITU TQ.931建立的TCSBinary被指定为蓝牙的二元电话控制协议规范。

(2)AT命令集电话控制协议,SIG定义了控制多用户模式下移动电话和调制解调器的AT命令集,该AT命令集基于ITU TV.250建议和GSM07.07,它还可以用于传真业务。

5.2.4选用协议

选用协议都是已有的其他组织的协议。

6.应用模型

Profiles部分规定不同蓝牙应用所需的协议,整个Profiles部分涉及了从耳机到局域网接入点等多种应用。对于每一个应用,应用模型给出其协议栈结构,并针对每一层具体规定一些必须实现的内容,诸如消息序列、功能集以及空中接口。依据应用模型实现应用,有利于不同厂家设备之间的互通性。

例如应用模型的第一个是一般接入应用模型,这个模型定义了用于发现蓝牙设备的过程、用于链接管理的过程、使用不同安全级别的过程,并描述了用户界面层次上参数的表示格式。模型首先给出了协议栈结构,而后分别描述了蓝牙地址、蓝牙设备类型、蓝牙PIN码在用户层面的表示格式,同时就认证等安全方面的内容给出流程图,最后描述设备发现及链路维护的消息序列,依照这个模型实现的设备互相可以发现对方并根据用户需求建立链路。

Bluetooth的四种应用模式

(1)通用访问应用(GAP)模式:定义了两个蓝牙单元如何互发现和建立连接,它是用来处理连接设备之间的相互发现和建立连接的。它保证两个蓝牙设备,不管是哪一家厂商的产品,都能够发现设备支持何种应用,并能够交换信息。

(2)服务发现应用(SDAP)模式:定义了发现注册在其他蓝牙设备中的服务的过程,并且可以获得与这些服务相关的信息。

(3)串口应用(SPP)模式:定义了在两个蓝牙设备间基于RFCOMM建立虚拟的串口连接的过程和要求。

(4)通用对象交换应用(GOEP)模式:定义了处理对象交换的协议和步骤,文件传输应用和同步应用都是基于这一应用的,笔记本电脑、PDA、移动电话是这一应用模式的典型应用。

7.蓝牙版本

根据不同的蓝牙版本,传输速度会差很多。例如蓝牙3.0的传输速度为3Mb/s,而蓝牙4.0技术从理论上可以达到60Mb/s。到目前为止,蓝牙最新版本为4.0,它的版本历史如下表所示:

版本 规范发布日期 增强功能
0.7 1998年10月19日 Baseband、LMP
0.8 1999年1月21日 HCI、L2CAP、RFCOMM
0.9 1999年4月30日 OBEX与IrDA的互通性
1.0Draft 1999年7月5日 SDP、TCS
1.0A 1999年7月26日 /
1.0B 2000年10月1日 WAP应用上更具互通性
1.1 2001年2月22日 IEEE 802.15.1
1.2 2003年11月5日 列入IEEE 802.15.1a
2.0+EDR 2004年11月9日 EDR传输率提升至2-3Mbps
2.1+EDR 2007年7月26日 简易安全配对、暂停与继续加密、Sniff省电
3.0+HS 2009年4月21日 交替射频技术、取消了UMB的应用
4.0+HS 2010年6月30日 传统蓝牙技术、高速蓝牙和新的蓝牙低功耗技术

表X 蓝牙规范版本历史

8.Android对蓝牙的支持状况

8.1支持版本

蓝牙技术作为目前比较常用的无线通信技术,早已成为手机的标配之一,可以实现蓝牙手机互连、传输文件、蓝牙耳机、蓝牙键盘等等功能。Android1.5对蓝牙的支持非常不完善,只支持像蓝牙耳机一样的设备,并不支持蓝牙数据传输等高级特性。在Android2.0终于加入了完善的蓝牙支持。

8.2Android蓝牙应用开发

8.2.1应用开发基本功能

在Android开发者网站http://developer.android.com/guide/topics/connectivity/bluetooth.html,介绍Android平台包括支持蓝牙的协议栈,允许设备之间通过无线交换数据。应用框架层提供了实现蓝牙功能的API,这些API使得应用可以无线连接到其他蓝牙设备,提供点对点和多点无线互连功能。通过蓝牙API,Android应用能够实现下面的功能:

? 扫描其他蓝牙设备
? 检索配对的蓝牙设备
? 建立RFCOMM连接
? 与其他设备传递数据
? 管理多路连接

8.2.2 应用开发API

使用Android API进行蓝牙通信,需要完成四个主要工作:建立蓝牙,寻找附近配对的或者可以使用的蓝牙设备,连接设备,在设备间传递数据。
所有的API都在android.bluetooth包中,在建立蓝牙连接中所需要使用的类和接口如下:

BluetoothAdapter:代表本地蓝牙适配器(Bluetooth Radio)。该BluetoothAdapter是入口点的所有蓝牙互动。利用这一点,你会发现其他蓝牙设备,查询保税(配对)的设备列表,使用已知的MAC地址实例化一个BluetoothDevice类,并创建一个BluetoothServerSocket来监听来自其他设备的通信。

BluetoothDevice:代表一个远程蓝牙设备。使用它可以请求与远程设备的连接,通过一个BluetoothSocket或者关于设备的名称,地址,类别和键合状态的查询信息。

BluetoothSocket:代表了一个蓝牙套接字(类似于TCP套接字)的接口。这是一个连接点,允许应用程序通过InputStream和OutputStream与其他蓝牙设备交换数据。

BluetoothServerSocket:代表一个侦听传入的请求的开放服务器套接字(类似于TCP的ServerSocket)。为了连接两个Android设备,一台设备必须打开这个类服务器套接字。一个远程蓝牙设备发送连接请求到该设备,当连接被接受后BluetoothServerSocket会返回一个连接的BluetoothSocket。

BluetoothClass:描述蓝牙设备的一般特点和能力。这是一组只读的定义了设备主要和次要的类以及服务的属性。然而,这并不能可靠地描述所有的蓝牙协议和设备支持的服务,但作为一个提示的设备类型是有用的。

BluetoothProfile:一个表示蓝牙协议的接口。蓝牙协议是一个设备之间基于蓝牙通信的无线接口规范。比如免提协议(Hands-Free)。
BluetoothHandset:对蓝牙耳机可被手机使用提供了支持。包括蓝牙耳机(Bluetooth Headset)协议和免提(Hands-Free V1.5)协议。

BluetoothA2dp:定义了高品质的音频怎样通过蓝牙连接以流的方式从一个设备传输到另一个设备。“A2DP”代表高级音频传输模式(Advanced Audio Distribution Profile)。

BluetoothHealth:代表控制蓝牙服务的健康设备协议代理。

BluetoothHealthCallback:用来实现BluetoothHealth回调的抽象类。你必须继承这个类并实现回调方法来接收有关更改应用程序的注册状态和蓝牙信道状态的更新。

BluetoothHealthAppConfiguration:表示蓝牙健康第三方应用程序注册与远程蓝牙健康设备进行通信的应用程序配置。

BluetoothProfile. ServiceListener:当BluetoothProfile IPC客户端已经连接到或从服务(一个运行特定协议的内部服务)断开连接时,通知该客户端的接口。

至于具体如何实现各种基本功能,本文暂不赘述。

8.3Android蓝牙源码开发

8.3.1Android蓝牙架构

在Android源码开发网站https://source.android.com/devices/bluetooth.html,介绍蓝牙如下:

Android提供了一个默认的蓝牙协议栈:BlueDroid,它被分为两层:蓝牙嵌入式系统(BTE),它实现了核心的蓝牙功能;和蓝牙应用层(BTA),它与蓝牙的应用框架进行通信。一个蓝牙系统服务通过JNI与蓝牙协议栈进行通信,通过Binder IPC与应用程序进行通信。蓝牙系统服务向开发者提供了多种访问蓝牙协议的途径。下图显示了蓝牙协议栈的整体结构:
这里写图片描述
Application framework
在应用程序框架层是应用程序的代码,它利用android.bluetooth API与蓝牙硬件交互。在内部,这个代码通过Binder IPC机制调用蓝牙进程。

Bluetooth system service
蓝牙系统服务,位于packages/apps/Bluetooth,被打包为一个Android应用程序,并在Android框架层实现了蓝牙服务和协议。这个应用程序通过JNI调用到HAL层。

JNI
与android.bluetooth相关的JNI代码位于packages/apps/Bluetooth/jni。JNI代码调用到HAL层,并且当某些蓝牙操作发生,比如当设备被发现的时候,接收来自HAL层的回调。

HAL
硬件抽象层定义了android.bluetooth API和蓝牙进程需要调用到,而且必须实现以确保蓝牙硬件能正确运行的标准接口。对于蓝牙HAL的头文件位于hardware/libhardware/include/hardware/bluetooth.h和hardware/libhardware/include/hardware/bt_*.h文件中。

Bluetooth stack
默认的蓝牙协议栈是提供给开发者的,位于external/bluetooth/bluedroid。该协议栈实现了通用的蓝牙HAL,当然,通过扩展和更改配置也可以自定义它。

Vendor extensions
用来添加用于跟踪的自定义扩展和HCI层,你可以创建一个libbt-vendor模块,并指定这些组件。

8.3.2实现HAL

蓝牙HAL位于hardware/libhardware/include/hardware/,它包含以下头文件:

bluetooth.h 包含设备上的蓝牙硬件HAL
bt_av.h 包含高级音频协议HAL
bt_hf.h 包含免提协议HAL
bt_hh.h 包含HID主机协议HAL
bt_hl.h 包含健康协议HAL
bt_pan.h 包含pan协议HAL
bt_sock.h 包含套接字协议HAL

请记住,你的蓝牙实现并不限制在HAL暴露的功能和协议。你可以找到位于external/bluetooth/bluedroid目录的默认实现,它实现了默认的HAL,也实现了额外的和自定义的功能。

8.3.3定制BlueDroid协议栈

如果你在使用默认的BlueDroid协议栈,但是想做一小部分定制,那么可以通过以下方式:

(1)定制蓝牙协议 - 如果你想补充的是, Android的HAL接口所没有提供的蓝牙协议,你必须提供一个SDK插件下载,使协议可以被程序开发人员使用,使蓝牙系统进程应用程序可以使用该API(packages/apps/Bluetooth),并把它们添加到BlueDroid堆栈(external/bluetooth/bluedroid)。

(2) 定制厂商扩展和配置更改 - 你可以通过创建一个libbt-vendor模块来添加东西,例如额外的AT命令或特定于设备的配置更改。可以去vendor/broadcom/libbt-vendor目录查看示例。

(3) 主机控制器接口(HCI) - 你可以通过创建一个libbt-HCI模块来提供自己的人机交互,它主要用于调试跟踪。可以去external/bluetooth/hci目录查看示例。

9.Android手机蓝牙代码剖析

下面,将以MTK6572平台的手机进行说明。

9.1手机蓝牙的外在功能点

(可画一幅功能图)

蓝牙开启与关闭
Rename phone
Visibility timeout
show received files
蓝牙配对
蓝牙传输文件

9.2蓝牙源代码位置

蓝牙核心功能的代码主要位于以下路径:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">alps/packages/apps/Settings/src/com.android.settings.bluetoothangel  
  2. alps/frameworks/base/core/java/android/bluetooth  
  3. alps/mediatek/packages/apps/Bluetooth  
  4. alps/mediatek/frameworks-ext/base/core/java/android/bluetooth  
  5. alps/mediatek/frameworks-ext/base/core/java/android/server  
  6. alps/mediatek/frameworks-ext/base/core/jni/  
  7. </code>  

核心代码结构如下表所示:

Module Sourcefile  
Settings alps/packages/apps/Settings/src/com/android/settings/bluetoothangel/ All files
Phone alps/packages/apps/Phone/src/com/mediatek/blueangel/ All files
Media Play alps/packages/apps/Music/src/com/mediatek/bluetooth/avrcp/ All files
Other profile(opp、avrcp) alps/mediatek/packages/apps/Bluetooth/profiles/ All files

蓝牙核心代码结构表
表X 蓝牙核心代码结构表

9.3蓝牙开启与关闭

蓝牙开关
蓝牙开关
蓝牙开关
蓝牙开关
图X 蓝牙开关

手机蓝牙默认是关闭的,要想打开蓝牙,可以通过上图所示的四个地方:

(1)手机设置里的蓝牙选项,后面有个开关,点击可以进行打开/关闭操作;
(2)点进去蓝牙设置界面,右上角也有个开关,点击可以进行打开/关闭操作;
(3)在手机下拉快捷设置界面,长按【BLUETOOTH】按钮,可以进入蓝牙设置界面,后同第(2)条;
(4)在进行文件的分享操作时,可以选择通过蓝牙进行传送,则会弹出是否打开蓝牙的对话框。
蓝牙打开/关闭时序图
图X 蓝牙打开/关闭时序图

在Settings.Java中,如果检测到Bluetooth service不可用,则不会显示蓝牙设置选项。具体代码如下:

[java] view plain copy
  1. <code class="hljs ruby" style="width:auto; overflow:hidden; word-break:break-all">} else if (id == R.id.bluetooth_settings) {  
  2. // Remove Bluetooth Settings if Bluetooth service is not available.  
  3. if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {  
  4. target.remove(i);  
  5. }  
  6. </code>  

简单讲一下Settings界面的实现原理,它继承于PreferenceActivity,每一个设置项被定义为一个Header。但是由于有特殊的项,比如Wi-Fi、Bluetooth右侧有一个开关,Quick start右侧有一个勾选框,以及若干项归为一类,不同分类之间有间隔,因此,这些项用如下四种标签来区分:

[java] view plain copy
  1. <code class="hljs java" style="width:auto; overflow:hidden; word-break:break-all">static final int HEADER_TYPE_CATEGORY = 0;  
  2. static final int HEADER_TYPE_NORMAL = 1;  
  3. static final int HEADER_TYPE_SWITCH = 2;  
  4. static final int HEADER_TYPE_CHECK = 3;  
  5. </code>  

Wi-Fi和Bluetooth右侧开关为Switch组件,实际上是一个CompoundButton,分别使用WifiEnabler和BluetoothEnabler管理;Quick start右侧的勾选框为一个CheckBox。Header的视图,则由Settings.java的内部类HeaderAdapter.java来管理,具体每一个Header的各个组件,在HeaderAdapter.java的内部类HeaderViewHolder.class中进行定义,代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">private static class HeaderViewHolder {  
  2. ImageView icon;  
  3. TextView title;  
  4. TextView summary;  
  5. Switch switch_;  
  6. //FR441047-qixing.chen-001 begin  
  7. CheckBox check;  
  8. //FR441047-qixing.chen-001 end  
  9. }  
  10. </code>  

这样,在getView()方法中,通过getHeaderType()方法获得Header类型,然后使用LayoutInflater装载不同Header视图,即形成我们最终看到的Settings主界面。
在BluetoothEnabler.java中,实现了setSwitch()方法,该方法会读取当前蓝牙的当前开关状态bluetoothState,并根据这个状态来设置Switch组件的选中状态(setChecked)和是否可选(setEnabled)状态。当Switch的选中状态有改变的时候,将调用onCheckedChanged()方法,然后通过setBluetoothEnabled()继续向下调用。代码如下:

[java] view plain copy
  1. <code class="hljs ruby" style="width:auto; overflow:hidden; word-break:break-all">/// M: if receive bt status changed broadcast, do not need enable/disable bt.  
  2. if (mLocalAdapter != null && !mUpdateStatusOnly) {  
  3. mLocalAdapter.setBluetoothEnabled(isChecked);  
  4. }  
  5. </code>  

这里有一个mUpdateStatusOnly的变量十分重要,它是BluetoothEnabler.java的全局变量,用来表示是否只更新Switch的状态。设置它的目的是我们可能通过其他途径(蓝牙开关的第四幅图)打开或者关闭蓝牙,而这种情况下必须同步更改Switch的状态,但此时是不需要再次打开/关闭蓝牙的操作的。

LoacalBluetoothAdapter.java中实现setBluetoothEnabled()方法。这个方法重要之处在于,它清晰地提供了打开/关闭代码逻辑上的分支,看下面代码很容易理解:

[java] view plain copy
  1. <code class="hljs java" style="width:auto; overflow:hidden; word-break:break-all">public void setBluetoothEnabled(boolean enabled) {  
  2. boolean success = enabled  
  3. ? mAdapter.enable()  
  4. : mAdapter.disable();  
  5.   
  6. if (success) {  
  7. setBluetoothStateInt(enabled  
  8. ? BluetoothAdapter.STATE_TURNING_ON  
  9. : BluetoothAdapter.STATE_TURNING_OFF);  
  10. else {  
  11. if (Utils.V) {  
  12. Log.v(TAG, "setBluetoothEnabled call, manager didn&#39;t return " +  
  13. "success for enabled: " + enabled);  
  14. }  
  15.   
  16. syncBluetoothState();  
  17. }  
  18. }  
  19. </code>  

如果传递过来的参数enabled是true,则表示打开蓝牙,就调用mAdapter.enable();如果enabled是false,则表示关闭蓝牙,就调用mAdapter.disable()。流程图只画出了打开蓝牙的流程,而关闭蓝牙的流程从这之后,只要将enable环卫disable就基本可以了。

接下来进入BluetoothAdapter.java,其中分别实现了enable()和disable()方法,两个方法最终分别调用了BluetoothManagerService.java中的enable()和disable()方法。

在BluetoothManagerService.java中,enable()和disable()方法分别调用sendEnableMsg(false)和sendDisableMsg(),而这两个本地方法有异曲同工之妙,就是最终都发送了消息,由BluetoothHandler来处理。代码如下:

[java] view plain copy
  1. <code class="hljs java" style="width:auto; overflow:hidden; word-break:break-all">private void sendDisableMsg() {  
  2. mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_DISABLE));  
  3. }  
  4.   
  5. private void sendEnableMsg(boolean quietMode) {  
  6. mHandler.sendMessage(mHandler.obtainMessage(MESSAGE_ENABLE,  
  7. quietMode ? 1 : 00));  
  8. }  
  9. </code>  

BluetoothHandler是BluetoothManagerService.java的内部类,handleMessage()方法中即实现了对蓝牙各个不同状态的消息处理。找到对应的分支,接下来调用本地方法handleEnable()和handleDisable()两个方法。

在handleEnable()中,通过mBluetooth.enable()或mBluetooth.enableNoAutoConnect()继续向下调用;在handleDisable()中,通过mBluetooth.disable(false)继续向下调用。

上面三个方法都调用自BluetoothService.java,分别实现了enable()和disable()的带参数重载。并在其中分别发送了消息进行打开关闭的操作,代码如下:
enable:

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">mBluetoothState.sendMessage(BluetoothAdapterStateMachine.USER_TURN_ON, saveSetting);</code>  

disable:

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">mBluetoothState.sendMessage(BluetoothAdapterStateMachine.USER_TURN_OFF, persistSetting);</code>  

mBluetoothState为BluetoothAdapterStateMachine.java的实例,在它的内部类PowerOff中,processMessage()方法对消息进行处理。在消息处理的对应代码分支,将调用prepareBluetooth()本地方法。在该方法中,通过mBluetoothService调用enableNative()和disableNative()方法。mbluetoothService为BluetoothService的实例,而enableNative()和disableNative()则是JNI层的方法。

在蓝牙开关图第二幅中,由于都使用的是同一个BluetoothEnabler作为入口,所以所有流程都一样。

在蓝牙开关图第三幅中,只需要搞懂手机下拉快捷设置中关于蓝牙的界面部分。

它的代码在/mtk6572-v1.0-dint/frameworks/base/packages/SystemUI/src下面。

作用
packages.SystemUI.src.com.android.systemui.statusbar.phone QuickSettings.java  
packages.SystemUI.src.com.android.systemui.statusbar.policy BluetoothController.java  
packages.SystemUI.src.com.android.systemui.statusbar.toolbar QuickSettingsConnectionModel.java  

表X 蓝牙快捷设置代码结构表

这部分涉及SystemUI模块,由于时间因素暂时先不细讲。

在蓝牙开关图第四幅中,也待补充。

9.4蓝牙设置主界面

蓝牙各种功能的配置,主要集中在蓝牙设置主界面。它包括了打开/关闭,重命名,可见时间设置,共享历史查询,搜索周围设备,配对周围设备,配对后进行文件传输等功能。也就是说,以上功能都能在这里找到入口。
蓝牙设置主界面(关闭状态)
图X 蓝牙设置主界面(关闭状态)

蓝牙设置,对应的代码位置在Settings模块下的com.android.settings.bluetoothangel包内。上图的蓝牙设置界面,主要由BluetoothSettings.java负责。接下来的很多功能,都将从这个类开始。

当打开蓝牙后,菜单栏的【Rename phone】和【Visibility timeout】就会变为可选状态。在最顶端会显示本机蓝牙设备名称和可见状态。默认对所有其他蓝牙设备不可见。
蓝牙设置主界面(打开状态)
图X 蓝牙设置主界面(打开状态)

9.5蓝牙设备重命名

蓝牙设备都会拥有自己的名字,便于用户识别。对于手机这类可进行输入的蓝牙设备,还可以更改它的名字。当周围设备搜索到该设备后,所看到的名字即这个修改后的名字。

点击【Rename phone】,将出现一个对话框,如下图所示:
蓝牙rename功能图
图X 蓝牙rename功能图

由BluetoothNameDialogFragment.java负责rename界面,是一个Dialog。

其中,需要特别注意两点:

(1)修改确认的【Rename】按钮,在没有进行修改的时候不可按;在有了修改操作后变为可按。为了区分有没有修改,使用 mDeviceNameEdited作为区分标志。还有一个标志为mDeviceNameUpdated,则是用来标记是否有过确认操作。

(2)在更改名字的这个界面,有可能发生转屏,转屏是需要销毁Activity再重新建立Activity的。因此,必须处理这种特殊情况下的名字保存问题,使用了KEY_NAME和KEY_NAME_EDITED两个静态关键字,来保存修改中的名字和修改的状态,从而在发生转屏的时候能够恢复该名字信息。

更改的名字,最终保存在手机data/@mtBluetooth路径下面。
蓝牙rename时序图
图X 蓝牙rename时序图

9.6蓝牙可见时间设置

蓝牙在开启后,默认是不可见状态,也就是说就算开启蓝牙,周围的其他设备也无法搜索到你的设备。要想能够被其他设备搜索得到,必须设置“可见时间”。一般可以设置可见2分钟到数分钟不等,在这种情况下,设备在设置的时间范围内对外可见,超过时间后自动变为不可见;当然,也可以设置为一直可见,这样就不会有时间限制;但是,每次重新关闭又打开蓝牙后,都必须重新手动点击“可见时间”选项,才能重新生效(这种行为方式作者认为可以被定制)。

这部分内容,将功能截图与代码分析放在一起,便于对照说明。整个流程的时序图如下:
蓝牙可见时间设置时序图
图X 蓝牙可见时间设置时序图

点击【Visibility timeout】后,显示如下界面:
蓝牙Visibility timeout设置界面
图X 蓝牙Visibility timeout设置界面

(1) 该界面由BluetoothVisibilityTimeoutFragment.java负责。这是一个填充了列表的dialog,几个数据选项来自资源com.android.settings.R.array.bluetooth_visibility_timeout_entries。核心代码如下:

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">public Dialog onCreateDialog(Bundle savedInstanceState) {  
  2. return new AlertDialog.Builder(getActivity())  
  3. .setTitle(R.string.bluetooth_visibility_timeout)  
  4. .setSingleChoiceItems(R.array.bluetooth_visibility_timeout_entries,  
  5. mDiscoverableEnabler.getDiscoverableTimeoutIndex(), this)  
  6. .setNegativeButton(android.R.string.cancel, null)  
  7. .create();  
  8. }  
  9. </code>  

(2)array默认选项为2minutes。在初始化该dialog时的mDiscoverableEnabler.getDiscoverableTimeoutIndex()方法中进行控制,该方法在BluetoothDiscoverableEnabler.java中实现,具体代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">int getDiscoverableTimeoutIndex() {  
  2. int timeout = getDiscoverableTimeout();  
  3. switch (timeout) {  
  4. case DISCOVERABLE_TIMEOUT_TWO_MINUTES:  
  5. default:  
  6. return 0;  
  7.   
  8. case DISCOVERABLE_TIMEOUT_FIVE_MINUTES:  
  9. return 1;  
  10.   
  11. case DISCOVERABLE_TIMEOUT_ONE_HOUR:  
  12. return 2;  
  13.   
  14. case DISCOVERABLE_TIMEOUT_NEVER:  
  15. return 3;  
  16. }  
  17. }  
  18. </code>  

(3)具体设置可见时间的方法,则是在item被点击后进行,代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">public void onClick(DialogInterface dialog, int which) {  
  2. mDiscoverableEnabler.setDiscoverableTimeout(which);  
  3. dismiss();  
  4. }  
  5. }  
  6. </code>  

该方法在BluetoothDiscoverableEnabler.java中实现。在setDiscoverableTimeout()方法中,通过对应的list item ID来确定具体可见的时间,然后写入SharedPreference中,并通过setEnable(true)开启可见。核心代码如下:

[java] view plain copy
  1. <code class="hljs ruby" style="width:auto; overflow:hidden; word-break:break-all">mSharedPreferences.edit().putString(KEY_DISCOVERABLE_TIMEOUT, timeoutValue).apply();  
  2. setEnabled(true); // enable discovery and reset timer  
  3. </code>  

(4)不可见的信息,由setSummary方法提供。具体代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">private void setSummaryNotDiscoverable() {  
  2. if (mNumberOfPairedDevices != 0) {  
  3. mDiscoveryPreference.setSummary(R.string.bluetooth_only_visible_to_paired_devices);  
  4. else {  
  5. mDiscoveryPreference.setSummary(R.string.bluetooth_not_visible_to_other_devices);  
  6. }  
  7. }  
  8. </code>  

而当设置了可见时间后,如何动态地更新时间信息?由updateCountdownSummary()提供减数计算,算出剩余时间timeLeft,然后传递给updateTimerDisplay()进行界面的更新。而动态性,则是另起一个线程,由mUpdateCountdownSummaryRunnable来完成,它的run方法中调用updateCountdownSummary()。

(5)以上4点将外在的功能实现,在可见时间期间,蓝牙的工作是怎样开启?从第3点的setEnable(ture)调用开始。代码如下:

[java] view plain copy
  1. <code class="hljs java" style="width:auto; overflow:hidden; word-break:break-all">private void setEnabled(boolean enable) {  
  2. if (enable) {  
  3. int timeout = getDiscoverableTimeout();  
  4. long endTimestamp = System.currentTimeMillis() + timeout * 1000L;  
  5. LocalBluetoothPreferences.persistDiscoverableEndTimestamp(mContext, endTimestamp);  
  6.   
  7. mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);  
  8. updateCountdownSummary();  
  9.   
  10. Log.d(TAG, "setEnabled(): enabled = " + enable + "timeout = " + timeout);  
  11.   
  12. if (timeout > 0) {  
  13. BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(mContext, endTimestamp);  
  14. else if(timeout == 0) {  
  15. //M: ALPS00580026 when set the discoverable timeout is 0, cacel the previous alarm  
  16. BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext);  
  17. }  
  18. else {  
  19. mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);  
  20. BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext);  
  21. }  
  22. }  
  23. </code>  

首先通过getDiscoverableTimeout()方法得到所设置的可见时间timeout。

然后,通过System.currentTimeMillis()获得当前系统时间,与timeout相加,得到可见的结束时间endTimestamp。

然后,调用LocalBluetoothPreferences.java类的persistDiscoverableEndTimestamp()方法,将endTimestamp写入SharedPreference中。这个时间最终在第4点updateCountdownSummary()方法中调用,用以更新Summary。
然后,用setScanMode()方法,将可见的搜寻模式DiscoveryMode和可见时间timeout通过BluetoothLocalApapter.java、BluetoothAdapter.java、BluetoothService.java一层层传入。最终,BluetoothService.java中实现了该方法,通过setPropertyInteger()方法将时间传入JNI层,通过setPropertyBoolean()将控制搜寻模式的两个boolean值discoverable和pairable传入JNI层。这两个布尔值分别控制是否可以被其他蓝牙设备搜寻到和是否可以配对,两者组合在一起即是几种搜寻模式。

(6)在上面的代码中,最后还调用了BluetoothDiscoverableTimeoutReceiver.java的两个方法setDiscoverableAlarm()和cancelDiscoverableAlarm()用来显示当手机挂起后的notification。这个类没有在最前面的流程图里画出,这点请知悉。
时间设置成功效果
图X 时间设置成功效果

9.7蓝牙文件共享历史

当点击【Show received files】后,会进入下图界面。只要是通过蓝牙上传(传送给其他蓝牙设备)或者下载(从其他蓝牙设备接收)的文件,不论成功与否,没打开之前均会在这里有显示。
蓝牙文件传输历史主界面
图X 蓝牙文件传输历史主界面

(1)当点击show received files选项后,通过发送一个广播android.btopp.intent.action.OPEN_RECEIVED_FILES跳转到MTK实现的广播接收器BluetoothShareGatewayReceiver.java中,该类在/mediatek/packages/apps/Bluetooth/common/bt40/src/com/mediatek/bluetooth/下面。通过该广播接收器,跳转到BluetoothShareMgmtActivity.java,也就是负责上图所示的Activity。
它继承自TabActivity,从图中可以看到由两个页面组成,分别使用inComingTabActivity和outGoingTabActivity进行管理。

(2)右上角有个菜单按钮,提供Clear all操作。当没有内容时,为不可点击状态;当有内容时,变为可点击状态。如下图所示:
蓝牙文件传输历史功能示意图
蓝牙文件传输历史功能示意图
蓝牙文件传输历史功能示意图
蓝牙文件传输历史功能示意图
图X 蓝牙文件传输历史功能示意图

代码如下:

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">if( CurrentTab.equals("Outgoing") ){  
  2. menu.findItem(R.id.bt_share_mgmt_tab_menu_clear).setEnabled((outGoingTabActivity.getCursor().getCount() > 0));  
  3. }  
  4. else if( CurrentTab.equals("Incoming") ){  
  5. menu.findItem(R.id.bt_share_mgmt_tab_menu_clear).setEnabled((inComingTabActivity.getCursor().getCount() > 0));  
  6. }  
  7. </code>  

而清除内容的工作,则是通过调用BluetoothShareTabActivity.java的clearAllTasks()方法实现。

(3)Download和Upload两个Tab界面,均通过getIntent()方法跳转至BluetoothShareTabActivity.java实现。代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">BluetoothShareTabActivity.getIntent(thisfalse));  
  2. BluetoothShareTabActivity.getIntent(thistrue));  
  3. </code>  

该方法的第二个参数是布尔型的isOutgoing。如果是false,表示不是Outgoing,则处理Download界面;如果是ture,表示是Outgoing,则处理Upload界面。

(4) BluetoothShareTabActivity.java的onCreate()方法负责绘制界面,界面是一个ListView,该ListView的数据从BluetoothShareTaskMetaData.java中query()得到后装入Cursor,然后交由BluetoothShareTabAdapter负责显示和管理。核心代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">// query all tasks  
  2. this.mCursor = getContentResolver().query(BluetoothShareTaskMetaData.CONTENT_URI, null,   
  3. isOutgoing ? OUTGOING_SELECTION : INCOMING_SELECTION, null, BluetoothShareTaskMetaData._ID + " DESC");  
  4.   
  5. // create list adapter  
  6. if (this.mCursor != null) {  
  7.   
  8. BluetoothShareTabAdapter listAdapter = new BluetoothShareTabAdapter(this, R.layout.bt_share_mgmt_item,  
  9. this.mCursor);  
  10. listView.setAdapter(listAdapter);  
  11. </code>  

最后,使用registerTabActivity()方法回调至BluetoothShareMgmtActivity.java,这样就能保持ListView的内容与菜单一致了,界面显示Download的时候菜单操作的即是清除Dowload列表,界面显示Upload的时候菜单操作的即是清除Upload列表。代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">BluetoothShareMgmtActivity.registerTabActivity(isOutgoing, this);</code>  

(5)第2点已经提到,删除所有内容的方法是clearAllTasks()。代码如下:

[java] view plain copy
  1. <code class="hljs cs" style="width:auto; overflow:hidden; word-break:break-all">public void clearAllTasks() {  
  2.   
  3. int columnIndex = this.mCursor.getColumnIndexOrThrow(BluetoothShareTaskMetaData._ID);  
  4. ArrayList uris = new ArrayList();  
  5. for (this.mCursor.moveToFirst(); !this.mCursor.isAfterLast(); this.mCursor.moveToNext()) {  
  6. // compose Uri for the task and clear it  
  7. int id = this.mCursor.getInt(columnIndex);  
  8. Uri uri = Uri.withAppendedPath(BluetoothShareTaskMetaData.CONTENT_URI, Integer.toString(id));  
  9. uris.add(uri);  
  10. }  
  11.   
  12. Object[] param = new Object[] {  
  13. this.mCursor, uris  
  14. };  
  15. sHandler.sendMessage(sHandler.obtainMessage(CLEAR_ALL_TASK, param));  
  16. }  
  17. </code>  

首先获得存储ListView数据的mCursor中每一项的id,然后使用得到的id组合成每一项的uri,全部装入数组元素为uri的数组列表uris中,然后将该数组列表uris和代表清除所有数据的标志作为参数包装为消息交给Handler进行处理。

其实,删除单个内容的方法也是交给Hanlder进行处理的,这个后面遇到再讲。

(6)这里涉及到一个类BluetoothShareTaskMetaData,它是定义在BluetoothShareTask.java中的继承BaseColumns的数据接口。而BluetoothShareTask.java是负责蓝牙共享文件相关的数据辅助类。通过该类中的Uri定义,我们很容易发现蓝牙的共享文件均存储于ContentProvider中,具体实现类为BluetoothShareProvider.java。

(7)现在来具体看一下消息处理的方法handleMessage()。通过标志消息,判断出是清除所有内容还是清除单项内容。通过new一个新线程BtShareClearHistoryThread来进行清除所有内容的操作。在该线程的run()方法中,执行clearAllItems()进行清除工作。核心代码如下:

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">int columnIndex = this.mCursor.getColumnIndexOrThrow(BluetoothShareTaskMetaData._ID);  
  2.   
  3. for (Uri uri : this.mUris) {  
  4. ContentValues updateValues = new ContentValues();  
  5. updateValues.put(BluetoothShareTaskMetaData.TASK_STATE, BluetoothShareTask.STATE_CLEARED);  
  6. BluetoothShareTabActivity.this.getContentResolver().update(uri, updateValues, nullnull);  
  7. }  
  8. </code>  

目前来看,通过获取ContentResolver,然后使用update()方法。但是还未想清楚这到底是怎么联系到清除功能上的。

(8)在列表界面,每个item是支持单击操作的。

对于Download来说,代码中使用BluetoothShareTask.Direction.in来表示,而且分下载成功和失败两种情况。如果下载成功,单击后可以打开该文件,打开成功后,会自动从列表中删除该项记录;如果下载失败,单击后会通过MessageActivity的createIntent()方法,启动MessageActivity。该Activity主要用来显示一个Dialog对话框,用以询问是否需要重新下载。当然,当该对话框关闭后,会回调startActivityForResult()方法,进行记录的删除操作。

对于Upload来说,代码中使用BluetoothShareTask.Direction.out来表示,分为三种情况:上传失败且文件路径OK,上传失败的其他情况,上传成功。三种情况均要调用MessageActivity的createIntent()方法,在第一种情况中弹出的对话框会询问是否需要重新传送,如果点击确认将重新传送。最终,当对话框关闭后,也会回调startActivityForResult()方法,进行记录的删除操作。重新传送的Intent的核心代码如下,至于其他部分逻辑已经说清除,就不粘贴出来了。

[java] view plain copy
  1. <code class="hljs avrasm" style="width:auto; overflow:hidden; word-break:break-all">// check to re-send the failed outgoing file  
  2. Intent resendIntent = new Intent(Intent.ACTION_SEND);  
  3. resendIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);  
  4. resendIntent.setType(mTask.getMimeType());  
  5. resendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(mTask.getObjectUri()));  
  6. resendIntent.putExtra(BluetoothShareGatewayActivity.EXTRA_DEVICE_ADDRESS, BluetoothAdapter  
  7. .getDefaultAdapter().getRemoteDevice(mTask.getPeerAddr()));  
  8. </code>  

(9)在消除内容后,会进行Notification的取消,将调用NotificationFactory.java。它是一个Notification处理类,只有一个返回NotificationId的方法,很简单。该方法中又调用BluetoothProfile.java,该类对各蓝牙协议进行了编号定义。

(10)在第8点中讲到,单击Download列表项,如果该项是下载成功的,则可以打开该文件。这里调用了SystemUtils.java的getOpenFileIntent()方法。SystemUtils.java是蓝牙文件存储的辅助类,在蓝牙接收文件的过程中还将讲到。

9.8蓝牙扫描

触发蓝牙开启扫描的方式有四个:

(1)在蓝牙设置主界面,点击蓝牙打开按钮,这时即可自动开启扫描,在扫描过程中【SEARCH FOR DEVICES】按钮为不可点击状态,直到扫描完毕,按钮变为可点击状态。
(2)在上面状态下,点击【SEARCH FOR DEVICES】按钮可以重新进行扫描。
(3)在蓝牙打开的状态下,每次进入蓝牙设置主界面,都会重新进行扫描。
(4)在共享文件的时候,点击【Allow】打开蓝牙后,也会自动进行扫描。
(*5)在上面扫描完毕后,点击【Scan for devices】,可以重新进行扫描。由于这个操作都是在共享文件的时候发生的,因此可以与第(4)并归为一个方式。

第4个方式比较特殊,截图如下(在文件传输的时候还会讲到):
蓝牙传送文件时的扫描界面
图X 蓝牙传送文件时的扫描界面

从逻辑上讲,蓝牙扫描的具体流程又分两步:扫描设备信息,返回设备信息并显示。

9.8.1逻辑上的设备信息扫描

逻辑上的扫描设备信息的时序图如下:
蓝牙扫描流程时序图
图X 蓝牙扫描流程时序图

(1)配对之前首先要进行扫描工作,在BluetoothSettings.java中调用startScanning(),在该方法中调用LocalBluetoothAdapter.java中的startScanning(Boolean force)方法,在其中又调用重写的方法startScanning(boolean force, int type)方法,将BluetoothAdapter. TYPE_BR_EDR_ONLY参数传进去。这个参数是在MTK自己实现的BluetoothAdapter中进行的定义,路径如下:/mediatek/frameworks-ext/base/core.java.android.bluetooth,注意与Android源码自身的BluetoothAdapter进行区分。

(2)在Scanning之前,需要进行一些状态的判断。比如如果手机是否已经处于搜索状态等,方法为isDiscovering()。比如是否是强制扫描,这里的强制扫描我暂时还不清楚具体是什么功能,但是代码中即是由布尔值参数force在控制。代码如下:

[java] view plain copy
  1. <code class="hljs fsharp" style="width:auto; overflow:hidden; word-break:break-all">/// M: new API for startScanning with the type passing to BluetoothAdapter @{  
  2. void startScanning(boolean force, int type) {  
  3. // Only start if we&#39;re not already scanning  
  4. if (!mAdapter.isDiscovering()) {  
  5. if (!force) {  
  6. // Don&#39;t scan more than frequently than SCAN_EXPIRATION_MS,  
  7. // unless forced  
  8. if (mLastScan + SCAN_EXPIRATION_MS > System.currentTimeMillis()) {  
  9. return;  
  10. }  
  11.   
  12. // If we are playing music, don&#39;t scan unless forced.  
  13. A2dpProfile a2dp = mProfileManager.getA2dpProfile();  
  14. if (a2dp != null && a2dp.isA2dpPlaying()) {  
  15. return;  
  16. }  
  17. }  
  18.   
  19. if (mAdapter.startDiscovery(type)) {  
  20. mLastScan = System.currentTimeMillis();  
  21. }  
  22. }  
  23. }  
  24. </code>  

可以看出,首先判断手机是否在扫描状态,则判断是否为强制扫描。如果最大扫描时间大于当前时间?或者正在播放音乐,都将返回,除非是强制扫描。

如果手机既不在扫描状态,也没有播放音乐,将会进行startDiscovery()操作,即开启扫描功能。

(3)此时,首先判断蓝牙状态是否开启,然后调用BluetoothService.java的startDiscovery(type)方法。这里需要强调一个参数type,它是int类型,用来指明是以哪种模式进行扫描。在此方法中将调用startDiscoveryNative(mode)进行扫描,这个方法在JNI层,在这里先不继续追究了。

9.8.2返回设备信息并显示

进行到上面的最后一步,我们在手机上最直观的感受是:附近的设备作为一个列表依次显示在蓝牙设置主界面,如下图所示:
蓝牙搜索完毕
图X 蓝牙搜索完毕

而这些设备的信息是得到的?又是怎么显示出来的?本节就是来讲述这个容易被想当然忽视的部分。我们可以推断出,它一定是经过了从底层硬件设备给出自身信息,然后一层层传上来,直至传至最上层蓝牙设置界面的过程。逻辑上的返回设备信息并显示的流程时序图如下:

9.9蓝牙配对

(1)首先还是从BluetoothSettings.java开始,它是继承于DeviceListPreferenceFragment.java,在扫描完毕后,扫描得到的附近的蓝牙设备信息将通过Preference显示。当单击任何一个设备后,将调用onDevicePreferenceClick()方法。代码如下:

[java] view plain copy
  1. <code class="hljs css" style="width:auto; overflow:hidden; word-break:break-all">@Override  
  2. void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {  
  3. mLocalAdapter.stopScanning();  
  4. super.onDevicePreferenceClick(btPreference);  
  5. }  
  6. </code>  

(2)在onDevicePreferenceClick()中调用BluetoothDevicePreference.java的onClicked()方法。

[java] view plain copy
  1. <code class="hljs lasso" style="width:auto; overflow:hidden; word-break:break-all">void onClicked() {  
  2. int bondState = mCachedDevice.getBondState();  
  3.   
  4. if (mCachedDevice.isConnected()) {  
  5. askDisconnect();  
  6. else if (bondState == BluetoothDevice.BOND_BONDED) {  
  7. Log.d(TAG, "connect");  
  8. mCachedDevice.connect(true);  
  9. else if (bondState == BluetoothDevice.BOND_NONE) {  
  10. pair();  
  11. }  
  12. }  
  13. </code>  

先获取当前绑定状态,如果当前已经连接,则请求断开连接;如果已经绑定,则连接;如果还未绑定,则调用pair()方法进行配对。

(3)在流程图中,我将如何获取绑定状态的过程也画了出来,可以看到,最终会走到BluetoothBondState.java中,这个类用来存储设备的绑定状态。根据类注释,bluez并不会跟踪设备的即时绑定状态,所以我们通过这个类来进行保存并跟踪。它的状态保存在HashMap中,因此最终通过哈希表的get()方法获得。

(4) askDisconnect()方法会显示一个Dialog对话框,在其中将调用CachedBluetoothDevice.java的disconnect()方法。这个类代表了一个远程的蓝牙设备,它包含了该设备的各种属性,诸如地址(address)、名称(name)、RSSI等等,以及会在该设备上进行的一些操作功能,诸如连接(connect)、配对(pair)、取消连接(disconnect)等等。代码如下:

[java] view plain copy
  1. <code class="hljs coffeescript" style="width:auto; overflow:hidden; word-break:break-all">void disconnect() {  
  2. for (LocalBluetoothProfile profile : mProfiles) {  
  3. disconnect(profile);  
  4. }  
  5. // Disconnect PBAP server in case its connected  
  6. // This is to ensure all the profiles are disconnected as some CK/Hs do not  
  7. // disconnect PBAP connection when HF connection is brought down  
  8. PbapServerProfile PbapProfile = mProfileManager.getPbapProfile();  
  9. if (PbapProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED)  
  10. {  
  11. PbapProfile.disconnect(mDevice);  
  12. }  
  13. }  
  14. </code>  

(5)可以看到,在该方法中将遍历LocalBluetoothProfile中的所有协议,并依次调用disconnect(profile)进行断开连接。这个方法对参数进行了重载,最终调用LocalBluetoothProfile.java的disconnect()方法。而LocalBluetoothProfile.java是一个接口文件,定义了和蓝牙协议相关的各种基础功能,具体实现由各个不同协议分别去做。

代码中的注释部分讲了当HF连接还没有关闭时一些CK/Hs不会断开PBAP的连接,因此在disconnect()方法中调用了disconnect(profile)方法后还会进行再次的Pbap协议的关闭。

(6)回到第2点的代码中,如果已经绑定,则调用connect()方法。首先调用ensurePaired()方法确认是否已经配对,如果还未绑定,则调用startPairing()方法开始配对,返回false表示还未配对;否则,直接返回true,表示已经配对。这块的代码如下:

[java] view plain copy
  1. <code class="hljs java" style="width:auto; overflow:hidden; word-break:break-all">private boolean ensurePaired() {  
  2. if (getBondState() == BluetoothDevice.BOND_NONE) {  
  3. startPairing();  
  4. return false;  
  5. else {  
  6. return true;  
  7. }  
  8. }  
  9. </code>  

(7)startPairing()方法的代码如下:

[java] view plain copy
  1. <code class="hljs ruby" style="width:auto; overflow:hidden; word-break:break-all">boolean startPairing() {  
  2. // Pairing is unreliable while scanning, so cancel discovery  
  3. if (mLocalAdapter.isDiscovering()) {  
  4. mLocalAdapter.cancelDiscovery();  
  5. }  
  6.   
  7. if (!mDevice.createBond()) {  
  8. return false;  
  9. }  
  10.   
  11. mConnectAfterPairing = true// auto-connect after pairing  
  12. return true;  
  13. }  
  14. </code>  

注释中说的比较清楚,当正在进行扫描的时候,配对会非常不稳定,因此在该方法中首先使用isDiscovering()判断当前设备是否正处于扫描状态,如果返回true,则调用cancelDiscovery()取消扫描。

蓝牙配对的过程:
这里写图片描述
这里写图片描述
这里写图片描述

Share file:
这里写图片描述
这里写图片描述
这里写图片描述

Share history:
这里写图片描述

附录

蓝牙各协议
Hands-free Profile(HFP)
手机音频服务,用于连接单声道蓝牙耳机或车载蓝牙,传输语音和数据信息。
实现AG角色

Advanced Audio Distribution Profile(A2DP)
媒体音频服务,用于传输立体声语音
实现Source角色

Audio/Video Remote Control Profile(AVRCP)
蓝牙耳机控制手机上的音乐播放、暂停等。
实现Target角色

Object Push Profile(OPP)
对象传输协议,用于传输普通文件(如mp3、图片等)。
实现Client&Server角色

Human Device Interface Profile(HID)
广泛用于蓝牙键盘、鼠标等人机交互设备
实现Host角色

Personal Area Networking Profile(PAN)
实现NAP Service角色

Phone Book Access Profile(PBAP) 电话本访问

Basic Image Profile(BIP) 图片传输

Basic Printing Profile(BPP) 控制打印

File Transfer Profile(FTP) 文件传输

SIM Access Profile(SAM,SIM) 芯片访问

Proximity Profile(PRX) 距离感应

Message Access Profile(MAP) 信息访问

Serial Port Profile(SPP) 虚拟串口

阅读更多
文章标签: java 蓝牙
个人分类: Java
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭