摘要概述

在蓝牙opp时既然是发送文件,client为发送方,那么还需要明确一个接收方作为server,待发送方和接受方确定后就要在两个设备之间点对点的打通一条光明大道作为传输通道。

 

当然还有你要运输的信息,有了这四要素,你就可以进行完美的运输了。

 

在运输结束之后需要把通道给拆了,因为每个设备的通道是有限的。所以运输的前提是保证server端存在,并且通道可以正确建立。

 

那么在建立之后开始传输,当信息送到server端时server是有自主选择权的,可以选择是否接受。如果被server拒绝的话,那么信息就会被丢弃了。

通道是否建立以及传输的具体情况都需要通知给client和server,所以应运而生的就是必不可少的notification。

 

所以,作为client端首先从功能上考虑总共需要处理5件事

 

  1. 选择接收方:在保证蓝牙开启的情况下,开启选择附近可用蓝牙设备界面,供用户选择目标设备进行分享

  2. 将要发送的文件存入db:以便随时更新状态,随时通知client。将需要分享的file插入到btopp.db中,由BluetoothOppProvider提供对数据库的增删改查。用于记录file的状态,协助传输和notification

  3. 建立传输通道:通过sdp搜索简历l2cap连接,若sdp搜索失败或者l2cap连接建立失败,则会去建立rfcomm连接。obex层connect操作,会在connect之后进行put发送。

  4. 拆除通道:在发送完毕后断开obex和l2cap或者是Rfcomm,即断开所建立的通道。

  5. notification:对于文件的等待状态,正在发送状态,以及发送结束(包括发送成功或者失败或者被server拒绝)进行更新通notification,是直接和用户交互的窗口,贯穿整个文件传输过程。

 

如下图所示,client和server端进行opp传输时的具体协议栈情况

 

跟我一起学OPP(一)_java

 

源码真算是博大精深,在实现opp时没有多余的废话,每个功能都缺一不可,在功能设计上让人佩服。

 

至于实现过程中源码是不是有不恰当的地方,就需要好好研究一下了。接下来分析各个功能项的具体实现了。

 

选择接收方

如上所示,选择接收方也就是选择目标方server。那么功能是如何实现的呢?

 

在选择蓝牙分享时会发送action_send(分享单个对象)或者是

action_send_multiple(分享多个对象),负责监听处理该广播的为BluetoothOppLauncherActivity。那么接下来看看接收到send之后都做了哪些处理。

 

这里就不贴代码了,直接总结出他所作的事情

  1. 判断蓝牙是否可用

  2. 保存要分享的文件

  3. 开启蓝牙设备选择界面

 

接下来就是各个击破了

 

第一:判断蓝牙是否可用isBluetoothAllowed():注意此处判断蓝牙是否可用并非是指判断isEnable而是判断canEnable。那么蓝牙是否可用和什么相关呢?代码如下

 

 

跟我一起学OPP(一)_java_02

 

首先确认,该方法返回为true时表示此时蓝牙可用。可以看到蓝牙是否可用有三个判断条件

 

  1. isAirplaneModeOn:是否处于飞行模式

  2. isAirplaneSensitive:从字面上看是说是否对飞行模式敏感,也就是说在a条件满足即飞行模式开启时蓝牙是否被disable

  3. isAirplaneToggleable:在a和b条件都满足的情况下即飞行模式开启且在开启飞行模式时bluetooth会被disable的情况下,是否支持用户重新开启reenable蓝牙。

 

第二:保存要分享的对象saveSendingFileInfo:注意此时要分享的对象并不是保存在db,而是会被保存至BluetoothOppManager和BluetoothOppUtility供确认目标方后开启分享时使用。

 

saveSendingFileInfo是BluetoothOppManager的实例方法,以保存单个分享对象为例进行说明

 

 

跟我一起学OPP(一)_java_03

 

在保存时有这么几个参数

  1. mMultipleFlag:表示要分享的是否是多个文件,该参数值由所监听到的send的action决定,如果是send_multiple则为true

  2. mMimeTypeOfSendingFile:表示要分享的类型,比如image/png,从监听到action的intent中获取

  3. mUriOfSendingFile:要分享的文件的uri,比如file:///storage/emulated/0/DCIM/Camera/IMG_20180607_142700R.jpg@3348f35,该参数也是在监听到send的action时从intent中获取的。

  4. mIsHandoverInitiated:此次文件传输是否是由NFC发起的,普及一下,nfc传输文件实际上走的也是蓝牙opp传输

 

然后就是具体的保存了,保存分为两大部分:

一部分是storeAppicationData(),将file通过sharedPreference方法保存到文件OPPMGR.xml中,文件所保存的信息如下所示:

 

 

跟我一起学OPP(一)_java_04

 

其中sendingflag表示程序是否在开启蓝牙的过程中为。其他参数解释见上文。

 

而关于保存的另一部分操作就是在BluetoothOppUtility的putSendFileInfo方法了,在putSendFileInfo方法中其实就是保存早BluetoothOppUtility私有的map集合sSendFileMap,key值为uri,value为generateFileInfo所生成的BluetoothOppSendFileInfo对象。

 

每个BluetoothOppSendFileInfo对象包含以下6个字段

 

 

跟我一起学OPP(一)_java_05

 

其中每个字段的取值示例如下所示

 

 

跟我一起学OPP(一)_java_06

 

该对象就是蓝牙opp传输进行时所操作的对象了。也就是说在传输过程中对于文件传输的状态、进度、输入流都跟该对象相关。

 

至此,准备工作做完了,接下来就是去打开蓝牙设备选择界面选择设备了

 

第三:启动蓝牙设备选择界面。通过在BluetoothOppLauncherActivity中调用launchDevicePicker方法来启动蓝牙选择界面加载可用设备。

 

当然在加载该界面之前需要保证蓝牙处于开启状态,所以在这里做了个蓝牙isEnable的判断,如果蓝牙未开启,则会去先开启蓝牙然后再去启动蓝牙选择界面。

 

对于蓝牙选择界面是依靠com.android.example.notificationlistener.LAUNCH来启动DevicePickerActivity的。

 

关于界面的UI加载不在赘述,当用户选择设备后会发送android.bluetooth.devicepicker.action.DEVICE_SELECTED广播,交由BluetoothOppReceiver处理选择设备之后的事情。

 

好了,目前为止第一部分也是最简单的部分选择目标设备的分析完成了,那么整个传输过程完全就可以到此为止了。

 

按照之前的总结再加上对代码的分析可以看出选择目标方这一过程其实做了四件事

 

第一:检测蓝牙的可用性即canEnable,这是必要的,如果目前处于飞行模式并且飞行模式下会关闭蓝牙且不支持用户重新enable蓝牙。

 

第二:将一些file相关的type和length以及uri的信息保存到BluetoothOppManager中,而这个信息就是在接下来保存到db时做铺垫,需要将manager中的信息存入db

 

第三:当然每个file都会对应一个BluetoothSendFileInfo对象,该对象会在文件开始传输时发挥作用,记录file的状态并提供输入流。

 

第四:启动蓝牙选择界面,供用户选择目标设备。

如果对某部分有疑问请往上翻...

 

存入btopp.db

在接收到设备选择后的广播之后调用BluetoothOppManager的实例方法startTransfer开始传输,本文会将传输分成两大部分解释,一是将文件存入DB,另一个就是开启传输。

 

本部分讲述将文件存入DB,其实存入db原本是没什么好说的也不是文章的重点,但是在蓝牙OPP过程中存入DB之时做了一件决定性的操作,所以就看一下db的存入来避免错过重点。

 

好了接下来看一下具体代码,如下

 

 

跟我一起学OPP(一)_java_07

 

可以看到,在该方法几乎算是只做了一件事,那就是开启了插入db的线程InsertShareInfoThread。

 

直接奔到线程的run方法中,代码逻辑很简单,就不再贴出,run方法中处理如下:如果是插入单个文件则调用insertSingleShared,如果是插入多个文件则调用insertMultipleShare。

 

本次分析以插入单个文件为例。所以直接看插入单个文件的代码。

 

 

跟我一起学OPP(一)_java_08

 

根据这几行代码,你要确认3个信息

 

  1. 所出入的对象values所包含的信息有:可以看到会包含file的uri、file的文件类型、以及目标设备。当然如果是通过nfc启动的文件传输需要传入一个确认方式即为nfc确认。是不是觉得少?还记得文章第一部分说的文件保存吗?保存方式是以map的形式,key为uri,value为BluetoothOppSendFileInfo对象,所以一旦有了uri,那么对应的value也就不难获取了

  2. 所插入的数据库为:这个可以从contentUri中得到,为btopp.db

  3. 执行插入操作的contentprovider为:根据contenturi同样也可以得出执行insert操作的为BluetoothOppProvider(根据contenturi推出provider相信大家都很明白,不再赘述)

 

看到这儿有没有一脸懵的感觉...说好的文件传输呢?怎么insert到DB就完事儿了?

 

难道你以为insert就是单纯的将数据写入DB?那你就太单纯了...目前到这儿只有insert的实现不明,所以直接奔到BluetoothOppProvider的insert方法中看看到底是怎么做的...

 

insert方法基本上是分为两部分,一部分实现insert一条数据到db,而另一部分就是开启BluetoothOppService。

 

第一,insert数据

 

insert数据其实流程都一样:首先构造values对象,然后insert到db,代码就不贴了,想看的可以通过androidxref看谷歌源码。

 

这里贴一下btopp.db数据库示例,该数据库一般位于data/data/com.android.bluetooth/databases/目录下,root之后可以导出来通过sqlite工具查看,先来看一下数据库都有哪些字段

 

 

跟我一起学OPP(一)_java_09

 

共包含14个字段,先来看一个db数据示例

 

跟我一起学OPP(一)_java_10

 

其中id属于插入db的序列号,uri是文章第一部分所保存的uri,hint文件名,direction表示是发送文件还是接受文件,destination表示目标方设备地址,comfirm表示文件传输时的用户确认方式,status表示文件的状态等等。

 

stauts是个很重要的字段,在文件传输过程中就是通过更新status状态来让其他人可以通过查询db来查看当前的文件状态:是否正在等待发送,是否正在发送,是否发送结束等等,也是更新notification的依据。

 

status的更新贯穿整个opp过程,会把status和notification放在一起进行分析。current_bytes和total_bytes用于更新文件传输过程中的进度条。

 

timeStamp字段在这里不起眼很容易被忽略掉,在后续的开启transfer时需要创建batch,而至于是新建batch还是插入已有的batch就是由该timestamp决定的。

 

如果分享单个文件那么毫无疑问timestamp是在BluetoothOppProvider的insert赋值的,但如果是分享多个文件那么timestamp实在BluetoothOppManager的插入db之前完成的。

 

也就是说一次分享多个文件时这些文件拥有同一个timestamp值那么在后续的transfer中也就会拥有同一个batch

 

第二,开启BluetoothOppService

insert里通过startService来开启服务。在开启蓝牙时就会去检测是否支持Opp并开启服务,那为什么这里又要开启呢?而且还是连续两次?,如下

 

跟我一起学OPP(一)_java_11

 

明明在蓝牙开启之后service就会启动了,那为什么在真正insert到db之前会启动一次service,insert之后再次start一次,什么目的?

 

首先明确一点调用startService启动服务时如果service未启动则走create-->start流程,如果service已经存在则会去触发service的start,而在opp的service的start方法会根据db数据库的更新来开启传输。

 

所以此处猜测之所以启动两次一是避免service已经被杀死,二是及时根据db的更新来更新发送。

 

基本上往db存数据就到这儿了,主要就是存入db,顺便重启service保证oppservice不死。接下来就是看看当把数据插入到db之后是如何影响service来开启transfer的

https://mp.weixin.qq.com/s/P0BgNKxQxlCrP8Y1Zm3AIw