分类目录归档:蓝牙聊天

蓝牙聊天 后记

第7节 后记

现在,我们还可以为聊天应用加上多国语言的支持和关于界面,把使用到的颜色和尺寸定义到资源文件当中,这样一来,安豆的蓝牙聊天应用就算是比较完整的完成了。

这两部分在以前“计算器”章节中,已经介绍过了,大家就自己动手吧。

经过这一章的实践,我们就接触到了初步的网络编程、多线程的配合。因为使用了蓝牙技术,也为将来进行与蓝牙相关的智能硬件开发打下了基础。

不过,也正如在开篇中我们提到的那样,这个应用留下了不少的遗憾,

  1. 没有让蓝牙连接作为Service在后台运行,收到消息后能用通知提醒用户;
  2. 不能记录下上次启动应用时聊天的内容;
  3. 没有查看彼此个人信息的功能;

在应用的使用过程中,还会发现很多诸如上面那样觉得值得添加的功能和改进的地方。所以这个版本的蓝牙聊天应用还只是一个粗糙的版本,我们应该在后面,添加上那些实用的功能。

蓝牙聊天 第6节 应用的美化与完善

第6节 应用的美化与完善

现在,我们还可以为聊天应用加上多国语言的支持和关于界面,把使用到的颜色和尺寸定义到资源文件当中,这样一来,安豆的蓝牙聊天应用就算是比较完整的完成了。

这两部分在以前“计算器”章节中,已经介绍过了,大家就自己动手吧。

这一节,我们将重点介绍聊天文字的背景图片是如何制作的。

6.1 9Patch图片的原理

观察一下安卓系统中需要经常用到的图片,可以发现:

  1. 很多要使用透明效果的地方在转角处;
  2. 很多图片不同的地方只在靠近边缘的地方,内部区域几乎都是一样的;

为此安卓系统在png格式图片的基础上,发展出了9patch图片,让图片占用更少的空间。它的格式是xxx.9.png。聊天文字的背景我们就要使用9patch图片,它放在res\drawable目录当中。9patch图片和普通png图片相比,只是在四周多了条1个像素宽的黑色线条,这四条黑线相互交叉,正好把图片分成了9宫格的样子,所以叫做9patch。

6.1.1 左线条和上线条

左线条覆盖的区域(深黄色、蓝色、桃红色)可以被上下拉伸,上线条覆盖的区域(绿色、蓝色、黑色)可以被左右拉伸;左线条和上线的交叉区表示可以被上下左右方向拉伸(下图中蓝色的区域);

当9patch图片需要根据控件的大小缩放时,四个角上的区域(褐色、青色、黄色、紫色)不会被缩放,只有可以被拉伸的区域发生缩放,填满整个控件。

6.1.2 右线条和下线条,

右线条和下线条交叉的区域表示内容区域。例如按钮的文字区域。
显示区域就是:在被拉伸后、可以用来显示文字内容的区域。

需要注意的是左线条和上线条必须有,右线条和下线条可以没有。当右线条和下线条都没有的时候,左线条和上线条的交叉区不仅仅表示该区域可扩展,也表示该区域是文字显示区域。

6.2 9Patch图片的制作

Android SDK提供了9Patch图片的制作工具,它放在sdk目录\tools\这个位置,名字叫做draw9patch。通过这个工具可以在png图片等四条边添加上黑色线,将它变成真正的9.png图片。

不过这里,我们将使用Android Asset Studio提供的在线生成工具。因为它能一次生成对应多种像素密度的图片。

  1. 打开Android Asset Studio选择Simple nine-patch generator

  2. 选择要处理的图片,

  3. 设置允许缩放的区域,

  4. 设置可以用来显示文字的区域

  5. 预览效果,下载图片,

6.3 9Patch图片的使用

图片下载后,将压缩包解压,可以看到自动生成了对应不同像素密度的图片。将这些图片放到项目工程的res目录下即可,

蓝牙聊天 第5节 界面使用ConnectionManager

第5节 界面使用ConnectionManager

ConnectionManager已经设计完成了,它的价值需要在ChatActivity中体现出来。

5.1 监听ConnectionManager

实现对ConnectionManager各个状态的监听,当ConnectionManager的状态有变化、收到发送的数据时,需要让ChatActivity知道,它才能将各种变化反应到用户界面上。

5.1.1 创建监听器

ConnectionManager定义了ConnectionListener接口,状态变化、数据的接收可以通过这个接口获得。创建一个ConnectionListener监听器,

5.1.2 创建Handler

因为监听器触发的函数不一定是在UI线程被调用的,例如onConnectStateChange(),所以不能在监听器当中对界面做修改,必须把界面更新的任务交给UI线程进行。

安卓系统提供了Handler的机制,让其它非UI线程能通过Handler把界面更新的操作,从工作线程布置给主线程完成。

  1. 创建一个能在主线程当中工作的Handler,

  2. ConnectionManager通知的内容,转交给主线程的Handler处理,

  3. 创建ConnectionManager,添加监听,

5.2 启动与停止监听

当聊天应用运行起来的时候,需要开启对其它蓝牙设备可能接入的监听,

当应用退出的时候,要断开可能存在的连接并停止监听,

5.3 启动连接

5.3.1 通过选择设备主动连接

当用户点击菜单栏的启动连接菜单项时,会启动DeviceListActivity,让用户从刷新的列表中,选取一个希望连接的设备。用户选择后,会把选中设备的地址返回给ChatActivity

这样,就可以利用ConnectionManager发起主动连接的请求了,

之后,ConnectionManager的各种状态变化,就会通过监听器ConnectionListener,传递到ChatActivity当中,据此更新界面就好了。

5.3.2 通过监听被动连接

这种情况,并不需要用户去做任何点击的操作。
之后,ConnectionManager的各种状态变化,就会通过监听器ConnectionListener,传递到ChatActivity当中,据此更新界面就好了。

5.3.3 菜单项的改变

我们之前已经通过ChatActivityonCreateOptionsMenu()方法,把菜单项添加到了菜单栏。现在需要菜单项随着ConnectionManager状态的变化,跟着做变化了。

  1. 当监听器的onConnectStateChange()或者onListenStateChange被触发后,我们将变化通过Handler通知到了UI线程,

    在更新UI到方法updateUI()中,修改菜单项的显示,

  2. 菜单项的响应也需要根据当前的连接状态,做进一步的修改,

5.4 发送与显示数据

聊天文字发送成功或者接收到对方发来的文字时,要显示到列表中。为此,我们需要专门设计一个Adapter来展示它们。

5.4.1 文字信息的数据结构

首先定义一个记录每条信息的数据结构ChatMessage,每一条消息要注明是由谁发来的,是自己还是对方,

5.4.2 信息展示的Adapter

我们采用类似微信聊天的样子来展示聊天内容。每条消息的背景图片是9patch形式的PNG图片,将它们放在res\drawable目录中。针对不同的屏幕像素密度,设计了对应的图片,放到对应的drawable目录下就行了。例如为xxhdip设计的背景图片就放在res\drawable-xxhdip目录中。

这些图片可以在示例代码中获得。

  1. 定义展示对方发来信息的布局-others_list_item.xml

  2. 定义展示自己发送信息的布局-me_list_item.xml

  3. 定义AdapterMessageAdapter,

  4. 使用聊天列表,

5.4.3 文字的发送

当连接建立以后,禁止点击的发送按钮和文字编辑框将被解禁。再文字编辑框中编辑好文字,点击发送按钮,就能将文字发送出去了。

  1. 为按钮创建监听器,当点击后,获取文字编辑框中的数据,再使用ConnectionManager提供的接口,把数据发送出去,

  2. 注册监听函数,

  3. ConnectionListeneronSendData回调方法中,将发送结果传递给UI线程,让UI线程把聊天内容更新到消息列表中,

5.4.4 文字的接收

当接收到对方发来的消息时,ConnectionListeneronReadData回调方法,将发送结果传递给UI线程,让UI线程把聊天内容更新到消息列表中,

至此,蓝牙聊天的整个流程都得以实现了。

蓝牙聊天 第4节 蓝牙连接模块

第4节 蓝牙连接模块

蓝牙连接的管理模块需要为ChatActivity提供于连接相关的所有功能,要设计的方便使用,并尽量隐藏连接的细节。

4.1 对外接口

我们首先来看看ConnectionManager需要向Chat Activity提供哪些接口。

  1. 监听。当应用运行起来后,聊天应用需要启动对其它蓝牙设备的监听,迎接随时可能到来的连接请求。所以ConnectionManager需要提供启动监听-startListen()停止监听-stopListen()的两个接口;

  2. 主动连接。应用搜索到可连接到设备后,可以主动的连接对方设备。假如正在连接,用户可能会取消连接;假如已经连接上,用户可能会想断开这个连接。所以ConnectionManager需要提供连接-connect()断开连接-disconnect()(包含取消连接的功能)两个接口;

  3. 主动查询当前连接状态。应用在采取某些操作的时候,可能需要知道当前的连接状态。所以需要提供主动查询当前连接状态的接口。

    连接的状态,我们可以把它分成两类,一个是监听的状态,一个是连接的状态。监听的状态表示当前是否在监听;连接的状态应该包括没有连接,正在连接和已经连接。

    所以接口应该有getCurrentListenState()getCurrentConnectState()

  4. 获取连接状态变化通知。应用界面需要根据连接的状态做出相应的变化,需要ConnectionManager能在连接状态发生变化的时候,主动把信息通知给ChatActivity。所以还要提供一个通知状态变化的接口。

  5. 接收数据。ConnectionManager收到数据以后,需要一个机制将数据通知给ChatActivity,这样ChatActivity才能显示接收到的内容。

  6. 发送数据。用户要发送数据给被连接的设备,所以ConnectionManager要提供一个发送数据的接口-sendData()

45都需要ConnectionManager主动向ChatActivity传递信息(收到的数据、状态改变),可以通过设计监听器来实现。因此需要设计安装监听器的方法:定义一个回调的接口-ConnectionListener;把监听器设置到ConnectioManager中。

综上所述,ConnectionManager大概应该是这个样子,

4.2 ConnectionManager的结构

我们已经明确了ConnectionManager对外的接口。接下来就需要来实现功能了。

由Android SDK提供的蓝牙连接接口函数,很多都是阻塞的(wifi连接也是如此),例如accept()connect()等等,所以就需要将这些操作放到单独的工作线程中进行。

因此,我们将会在ConnectionManager中设计两个工作线程,

  1. AcceptThread:负责监听别的蓝牙设备发起的连接;

  2. ConnectThread:用来维持与其它设备的连接。

主线程首先创建一个监听线程,用来接收其它设备可能发出的连接请求,当监听线程监听到了连接请求,就会得到一个Socket;然后我们再创建一个连接线程,将Socket交给连接线程,两个设备就可以通过这个Socket,在连接线程中进行消息的发送和接收了。

4.2.1 ConnectionManager

  • AcceptThread线程的工作状态,反应的就是监听状态。

    它包括LISTEN_STATE_IDLELISTEN_STATE_LISTENING。这个状态保存在mListenState变量中;

    当监听状态发生改变的时候,通过setListenState()设置状态的改变,并利用回调的方式,将改变的状态通知给关注者。

  • ConnectedThread线程的工作状态,反应的就是连接状态。

    它包括CONNECT_STATE_IDLE CONNECT_STATE_CONNECTINGCONNECT_STATE_CONNECTED;
    这个状态保存在mConnectState变量中;

    当连接状态发生改变的时候,通过setConnectState()设置状态的改变,并利用回调的方式,将改变的状态通知给关注者。

  • ConnectionManager对外提供的接口,实际上就是对这两个工作线程的控制;

通过这里,我们可以看出,为了能够取消正在运行的工作线程,在设计AcceptThreadConnectedThread的时候,我们需要给它们添加上取消的方法-cancel();为了在连接后能够发送数据,需要给ConnectedThread添加发送数据的方法-sendData()

4.2.2 监听线程

监听线程进行的工作有,

  1. 等待其它蓝牙设备发起的连接;

  2. 如果接收到连接的请求,就创建出一个Socket

  3. 之后继续等待其它设备可能发起的连接;

  4. 假如已经处于正在连接或者已经连接的状态,就断开最新收到的连接,因为我们假定了每次只能连接一个设备;

  5. 监听线程可以被取消,退出运行;

4.2.3 连接线程

连接线程的工作是,

  1. 假如是主动连接,连接线程要主动调用connect()方法;该方法是一个阻塞调用,在调用前,应该把ConnectionManager的连接状态,设置成CONNECT_STATE_CONNECTING;如果连接成功,就把状态设置成CONNECT_STATE_CONNECTED

  2. 假如是被动连接,那么从监听线程获取的Socket就已经是被连接上了的Socket,不需要进行connect()的操作了;

  3. 从已经连接的Socket当中,获取输入、输出的流接口,以后这个连接上的数据读取和写入,就是通过它们对应的流接口进行的;

  4. 连接线程进入循环,不断的尝试通过InputStreamread()方法,读取数据;

  5. 连接线程可以被取消,退出连接;

至此,ConnectionManager就设计好了。

蓝牙聊天 第3节 获取要连接的设备

第3节 获取要连接的设备

这一节我们开始设计蓝牙聊天应用的界面。根据之前的规划,连接管理将放在单独的ConnectionManager模块当中,所以每当要使用连接功能的时候,我们就暂时把它空着,等到ConnectionManager开发完成之后再加进来。

这里我们将完成下面的界面设计,

3.1 主界面

主界面是一个独立的ActivityChatActivity,它要实现三个主要功能,

  1. 当蓝牙没有开启或者设备不能被发现的时候,请求用户打开对应的功能;
  2. 下方有输入框输入要发送的文字内容,点击按钮后能实现文字的发送;输入框上方的大部分区域用来显示聊天的内容;
  3. 菜单栏根据当前蓝牙连接的状态,显示不同的菜单项。例如,没有连接时启动蓝牙设备选择界面;

3.1.1 打开蓝牙功能

ChatActivity创建的时候,查询当前蓝牙设备是否满足运行的要求,

  1. 提示开启蓝牙功能,

  2. 提示开启被其它蓝牙设备发现的功能,

3.1.2 界面布局

界面布局比较简单,使用垂直的线性布局LinearLayout将界面分成两个区域,上面的大区域显示聊天的内容,用ListView的显示;下面文字输入和发送用TextEditorImageButton组合起来。

在代码中,获取将来要操作的控件,

3.1.3 菜单项显示

菜单栏根据当前蓝牙连接的状态,显示不同的菜单项,

  1. 没有连接时,显示启动连接。点击该菜单,将启动显示可连接设备的ActivityDeviceListActivity

  2. 正在连接时,显示取消。点击该菜单,将取消正在进行的连接;

  3. 已经连接时,显示断开连接。点击该菜单,将断开与其它设备已经建立好的连接;

由于这里要根据蓝牙设备连接的状况设计不同的逻辑,所以接下来设计的ConnectionManager要为其它模块提供获取当前连接状态的接口。

目前,我们就暂时将它设计成满足条件1的状况,

  1. 定义一个菜单main_menu.xml

  2. 将菜单添加到菜单栏中,

  3. 响应菜单栏,启动DeviceListActivity获取可以连接到设备名称

    我们从ChatActivity启动DeviceListActivity,目的是要获取DeviceListActivity返回的内容-蓝牙设备的连接地址。所以不能简单的使用startActivity()方法了。

    1. 两个Activity之间传递数据,可以使用startActivityForResult()方法,这里面要设置一个ResultCode,用来主返回结果的时候使用辨别结果对应的是哪个请求,

    2. 返回的结果将在onActivityResult()函数中被通知到。这里参数的requestCode就是我们在startActivityForResult()中填入的那个数值;而resultCode代表另一个Activity是否如我们所愿返回了结果,

      得到蓝牙设备的地址后,就可以通过ConnectionManager模块去连接设备了。

在蓝牙设备连接之前,是不需要编辑文字和发送内容的。所以,可以使用ViewsetEnabled()函数,将TextEditorImageButton给禁用掉(点击它们不会有任何响应)。等到设备连接上之后,在把它们开启。

3.2 设备列表界面开发

为设备列表界面创建一个DeviceListActivity

3.2.1 主界面布局

  1. 界面布局很简单,就是一个ListView

    在代码中,设置上返回按钮,并获取这个ListView,以备将来使用,

  2. 为了展示可连接的蓝牙设备,我们会把收集到的可连接设备保存起来,通过ListView进行显示。

    这里将自定义一个AdapterDeviceItemAdapter,让它显示设备的名字和地址,

    1. 数据项的界面布局,

    2. 自定义的DeviceItemAdapter将继承自ArrayAdapter

    3. 使用ListView

3.2.2 展现可连接的设备

可连接的设备包括两种,

  1. 曾经连接过的,已经被系统记录在案,连接这种设备时,系统不会提示用户有设备需要配对;
  2. 完全新发现的设备,连接这种设备时,系统会提示用户有设备需要配对。

3.2.2.1 获取已绑定过的设备

获取第一种设备很简单,使用BluetoothAdaptergetBondedDevices()方法就可以了。找到后,添加到ListView中显示,

3.2.2.2 获取新发现的设备

获取第二种设备,就采用技术验证时使用的mBluetoothAdapter.startDiscovery()方法;

  1. 首先要注册一个BroadcastReceiver,然后startDiscovery(),之后系统会发出BluetoothAdapter.ACTION_DISCOVERY_STARTED的广播,告知搜索开始;发出BluetoothAdapter.ACTION_DISCOVERY_FINISHED的广播,告知搜索结束,

  2. 根据收到的广播,更新显示列表。假如搜索到的设备是曾经绑定过的,说明之前已经加到设备列表里面了,这里不需要重复添加,

    注意,这里能够在BroadcastReceiveronReceive()方法中直接修改界面元素,是因为onReceive()是运行在UI线程-主线程当中的。

  3. DeviceListActivity销毁的时候,注销BroadcastReceiver,同时也别忘了取消可能正在进行的搜索,

至此,DeviceListActivity已经可以列出可被发现和连接到设备了。

3.2.3 设置菜单栏

设置菜单栏的菜单项device_menu.xml,让菜单项一直显示,

我们将根据搜索设备的状态更改该菜单项的名称。所以,这里要定义当前搜索的状态,

  1. 当正在搜索的时候,显示取消,此时状态是BT_SEARCH_STATE_SEARCHING
  2. 当没有搜索的时候,显示搜索,此时对应的状态是BT_SEARCH_STATE_IDLE

这两种状态,都要记录下来,

在代码中添加菜单项,

响应菜单项,

为了更新菜单项,还需要BroadcastReceiver的配合,

3.3 得到要连接的设备

当用户点击要连接的设备后,将把该设备的地址返回给ChatActivity,由ChatActivity去连接设备。

  1. 为设备列表设置点击响应;
  2. 假如点击的时候还在进行搜索,取消搜索;
  3. 获取设备的地址,将它存储到Intent当中,最后通过setResult()方法,将结果传递给启动DeviceListActivityActivityChatActivity

点击之后,选中的设备地址会传递到ChatActivityonActivityResult()方法中,


***************************************

本系列课程使用到的Arduino开发板、扩展板以及其他相关的传感器,各位可以根据我们文章中介绍的硬件在淘宝网选购。
您也可以在我们的网店跟我学Arduino编程中购买,这些相关硬件都由我们为您把关购买,为大家节省选择的精力与时间。同时也感谢大家对我们这些码农的支持。

最后再次感谢各位读者对安豆的支持,谢谢:)


QQ交流群

571747694

蓝牙聊天 第2节 设计方案

第2节 设计方案

功能确定后,就要开始围绕功能进行功能的验证、界面设计的规划、以及程序结构的规划了。

2.1 技术验证

选定了现阶段要完成的核心功能后,我们首先需要对它们做技术上的验证,看看用什么样的方法能实现它们。在进行技术验证的同时,也能让我们发现很多我们在头脑风暴阶段没有意识到的现实问题。

现在的手机和移动设备已经把蓝牙作为了标准配置,它常常用到与周边小设备的数据连接上,例如蓝牙自拍杆,蓝牙音箱,蓝牙键盘,运动手环等等,可以看出这都是一些不需要太大数据量传输而需要保持长时间数据通信的设备。

蓝牙技术从出现到现在经过了多个版本,现在手机上最普遍的就是蓝牙4.0了。

作为应用程序的开发者,其实并不需要知道很多与蓝牙技术相关的硬件知识。Google将软件开发者会用到的所有蓝牙功能,都封装了起来,通过Android SDK提供给开发者使用。所以,只要我们知道如何使用Android SDK中提供的蓝牙接口函数就可以了。至于硬件是如何实现的无线电波互相通信,开发者完全不需要去关心。

要操作蓝牙设备,我们首先要获取操作蓝牙设备的对象BluetoothAdapter

同时,还需要使用到三项系统权限,

之后我们就可以通过BluetoothAdapter对真实的蓝牙设备以及软件功能进行操作使用了。

2.1.1 设备发现

一句简单的“让设备互相连通”,实际上却包含了很多隐藏的任务。

2.1.1.1 开启蓝牙

用户并不一定总是把蓝牙设备开启的,因为我们的应用要使用蓝牙,假如用户没有打开蓝牙设备,我们的程序得打开它或者提示让用户主动打开它。

有三种方式开启蓝牙功能,

  1. 使用安卓系统提供的开关打开。例如,在设置->蓝牙界面中打开,

  2. 在应用中使用Intent启动确认窗口,让用户选择是否允许打开,

  3. 在应用中直接打开蓝牙功能,不需要询问用户,

2.1.1.2 允许被发现

蓝牙打开后,设备并不一定能够别的蓝牙设备找到,出于安全角度考虑,很多设备的蓝牙开关即使是打开的,也有可能禁止让别的设备发现自己。

为了保险起见,我们需要设置设备可以被别的蓝牙设备发现,

  1. 使用BluetoothAdaptergetScanMode()方法,获取当前的蓝牙是否能被发现;
  2. 通过BluetoothAdapter.ACTION_REQUEST_DISCOVERABLEIntent启动询问窗口;
  3. BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION用来指定允许被发现的时间,例如,120就表示未来120秒内该蓝牙设备允许被别人发现;如果将该值设置成0,表示可以一直被发现;

2.1.1.3 搜索可连接设备

搜索可连接的蓝牙设备,

  1. 使用BluetoothAdapterstartDiscovery()方法开始搜素设备

  2. 当开始搜索的时候,系统会发出一个BluetoothAdapter.ACTION_DISCOVERY_STARTED广播;
    当结束搜索的时候,系统会发出一个BluetoothAdapter.ACTION_DISCOVERY_FINISHED广播;
    当找到某个可连接设备的时候,系统会发出一个BluetoothAdapter.ACTION_FOUND广播;并把可连设备的信息放在广播对应的Intent当中;

    因此,需要自定义一个BroadcastReceiver来接收这些广播,

  3. 接收机采用动态的方式,在应用启动的时候被注册,不使用的时候要注销,

2.1.1.4 搜索结果展示

应用需要一个搜索附近可连设备的功能,它能把附近的设备搜索出来,形成列表供用户选择,如果用户选择了某个设备,那么它就要去主动连接这个设备。

设置->蓝牙界面就有这样的展示,

但是,我们自身的聊天应用也要有一个类似的界面,方便用户选择设备,在同一个应用内就完成展示和连接的功能。

2.1.2 设备连接

蓝牙设备的连接(wifi设备也是如此)可以分成主动连接和被动连接。

设备的连接是通过Socket-套结字实现的。所谓Socket就像它的英文意思“插座”一样,将两个不同的实体通过插口插座连接起来。

两个实体连接起来后,就可以通过套结字来传送数据了。

2.1.2.1 被动连接

应用如果不去主动发现周围设备,不主动去连接其它设备,那它可以作为一个被动的接受者,等待别人的连接。

  1. 创建一个BluetoothServerSocket,需要给他指定一个名字和UUID。名字可以任意指定,UUID就有一定的学问了。使用相同UUID的蓝牙应用,可以被彼此发现。

    蓝牙设备的UUID可以分成两个大类,

    1. 由蓝牙标准统一规定的UUID。例如00001101-0000-1000-8000-00805F9B34FB这个UUID代表的就是蓝牙串口服务

    2. 自定义UUID,蓝牙应用的开发者可以自己“设计”一个UUID。这个UUID不能与已有规定的UUID相同,只是为了将自己的蓝牙应用与别的蓝牙应用区分开。

    蓝牙的UUID概念有点像网络编程中的端口号。
    例如,按照规定,端口号23是分配给FTP服务使用的,其它FTP应用想要彼此通信,不用去猜对方的端口号是什么,只要知道协议中已经规定了23,大家都会遵守就行,设计出来的应用绝不会有问题。
    开发者也可以为自己的应用指定一个不知名的端口号,不需要和别的应用互通。

    这里我们就使用蓝牙串口服务的UUID

  2. 启动一个线程-叫它监听线程,在它当中,使用BluetoothServerSocketaccept()方法,等待其它设备的连接;

    这是一个阻塞的调用。就是说一旦执行accept()函数,它就一直等在那里,不会返回了。这也是要将等待连接单独放到监听线程执行的原因。

    如果accept()函数返回了,有两种可能,

    1. 有别的设备向自己发出了连接的请求。accept()返回后,socket就是被连接设备与连接设备建立的Socket,这也正是程序所一直等待的;

    2. mServerSocketclose()函数被调用,导致accept()抛出了异常,

经过上面的流程,监听线程在收到其它蓝牙设备的连接后,就得到了Socket

2.1.2.2 主动连接

蓝牙设备搜索到周围的可连接设备后,可以主动向它们发起连接请求。

  1. 通过对方的地址,获取蓝牙设备,

  2. 创建可发起连接请求的Socket,创建时要使用到UUID,发起连接的设备的UUID要与被动接受连接的设备的UUID要相同,就像是要把发送和接受调整到同样的频道上一样,

  3. connect()函数主动发起连接请求,该函数是一个阻塞调用,不确定返回的时间,所以需要为它开启一个单独的线程执行,

    一旦执行成功后,我们就得到了可以与其它蓝牙设备通信的Socket

2.1.3 数据交换

连接建立以后,双方开始交换数据。无论是主动连接还是被动连接,数据的交换都是通过之前得到的Socket进行的。

  1. 获取读数据和发送数据的能力。Socket交换的数据都是以二进制的形式进行的。因此可以使用流的方式对数据进行操作,

    InputStream负责读取从Socket传入的数据;
    OutputStream负责将数据从Socket发送出去;

  2. 采用InputStreamread()方法读取数据。这是一个阻塞的方法,只有连接的对方有数据传送过来的时候才会返回,并把传来的数据放在buffer参数当中,返回值代表读取的数据字节长度。

    读完一次以后,需要重复调用read()继续读取接下来的数据,这是一个循环的过程,

    当别当地方关闭了Socket后,mInStream.read()会抛出异常,

    因为mInStream是通过mSocket创建的,两者之间已经建立了联系。所以当关闭mSocket的时候mInStream也会收到影响,从而抛出异常。

  3. 采用OutputStreamwrite()方法发送数据

在使用Socket的过程中,很多地方都可能抛出异常,所以在后面设计程序结构的时候,我们要好好考虑如何对应这些异常情况。

2.2 界面设计

确定了功能,进行了技术调查之后,我们就可以开始根据目前掌握的信息,设计应用的使用流程了。

应用流程分为三个部分,

2.2.1 应用启动

  1. 如果蓝牙功能没有打开,启动应用后提示打开蓝牙功能,

  2. 如果不能被其它蓝牙设备发现,启动应用后提示允许被其它蓝牙设备发现,

  3. 如果蓝牙功能已经打开,并且可以被其它设备发现,进入主界面,

2.2.2 设备主动连接及信息发送流程

2.2.3 设备被动连接及信息发送流程,

2.2.4 关于界面

2.3 程序结构

根据应用的功能和界面逻辑,程序结构将分成三个模块,

  1. 聊天记录的展示和发送。使用一个ActivityChatActivity来完成这部分工作;
  2. 发现可连接的蓝牙设备,让用户选择要连接的设备。使用一个ActivityDeviceListActivity来实现。当用户选中了某个设备后,将结果它告诉ChatActivity,统一由ChatActivity来进行连接的逻辑控制;
  3. 连接管理模块。连接蓝牙设备、设备之间发送消息、断开连接等等和通信密切相关的工作都交给ConnectionManager模块。该模块提供简单易用的接口給ChatActivity调用。

蓝牙聊天 第1节 功能规划

第1节 功能规划

规划产品的时候,我们先要做加法,尽可能的把它可以拥有的功能挖掘出来;然后再做减法,把不实用、或者投入性价比不高的功能放一放,作出第一版产品;最后,再根据用户的反馈、加上上一版产品留下的遗憾,进行产品的升级。

这是一个循环发展、螺旋上升的过程。这样做能把有限的开发资源放到最重要的地方去,得到用户的反馈,尽可能设计出用户真正需要的产品。

1.1 可能的功能点

作为一个即时通讯的小应用,我们完全可以把它设计成“微信”或者“QQ”的样子,唯一的不同是,这个聊天应用是通过蓝牙技术进行的数据传输,而不是wifi。因此,我们可以暂时列出它可能具备的功能:

  1. 两台设备能互相连接;
  2. 连接后的设备可以传输数据,比如文字、图片、语音、音乐、甚至是实时视频;
  3. 双方发送的内容,能够展示出来;
  4. 可以编辑任何一方的个人信息,例如头像、昵称、个性描述等等,建立通信等双方可以查看相互的资料;
  5. 可以查看双方聊天的历史纪录;
  6. 可以搜索聊天的历史记录;
  7. 可以单独保存对方发送的图片、语音或者视频
    ……

可以添加的功能实在是太多了。

1.2 功能的筛选

从上面列出的明细可以看出,能够赋予这个聊天应用的功能实在是太多了,因此我们必须根据我们的能力和精力来进行筛选,做功能的减法。

  1. 设备的互联是必须的,实现聊天的基础就是建立在这个基础上的。
  2. 数据在设备之间的传输都是以二进制的形式进行的,能够传送文字、图片、语音、音乐、视频等等信息。
  3. 需要展示出双方聊天的内容,这样方便用户判断上下文。

以上3条是就是蓝牙聊天应用的核心功能,其它功能都是这2条基础功能的扩展,

  1. 个人资料,有的话当然很好,但是并不会影响两人通信功能的使用;
  2. 查看历史聊天记录,查看双方上一次建立连接的聊天记录当然是有一个有必要的功能,不过为了简化问题,我们还是暂时把这个功能放一放,留到下一版再加入吧;

至于其它功能,那就离的更远了。我们就暂时不去实现了。

1.3 蓝牙技术的性能考虑

用蓝牙(或者wifi)传输数据,都是传输的二进制数据。文字、图片、语音、音乐、实时视频等内容本质上都是二进制数据,只是它们的格式不太一样而已。所以理论上讲,传输这几类信息都是可以的。

不过任何无线传输技术,在单位时间内传输的数据量和传输的距离都是有限制的。对传输要求比较高的应该算实时视频内容了,每一秒要达到24帧,所以对传输速度的要求可想而知了。那么蓝牙的传输速度能达到吗?

不同的数据类型,对传输的要求是不同的,传输什么样的数据比较合适呢?现阶段,我们把问题简化一下,就让它传送简单的字符串吧。

二进制数据流作为任何数据的载体,是最为基础的,而字符串可以看成是一种具有特殊结构的二进制数据。所以,为了开发简单,第一步先假设只传输文字内容。

到目前为止,实现整个功能都不会有技术上的障碍了。

1.4 现阶段的功能

根据上面的讨论,我们确定蓝牙聊天应用的具体功能:

  1. 能够所搜周围打开了蓝牙功能的设备,把搜索的结果展示出来;
  2. 发现可连接设备后,点击某个搜索的结果,能够主动连接对方;
  3. 能够被周围的其它蓝牙设备搜索到;
  4. 当收到其它设备发出的连接请求以后,能够接受对方的请求,建立连接;
  5. 连接的过程中,可以取消连接,连接成功后,能够断开连接;
  6. 当两个设备建立连接以后,就可以以二进制数据的形式传输数据,数据的内容就是简单的文字;
  7. 双方发送的内容,能够分别展示出来,但是不保存以前曾有过的聊天记录;
  8. 聊天的记录列表中,不能删除展示的信息;
  9. 当设备之间已经相连,就不能和别的设备建立连接了,彼此之间,一次只能连接一个设备;

1.5 功能条件的假设

我们确定了蓝牙聊天应用的功能,还需要给出一些功能设计的基本假设。

  1. 当两台蓝牙设备已经相连的时候,如果其中一台设备的蓝牙功能被用户手动关闭了,我们的应用需要做怎样的处理?对于此,为了简化第一版应用的开发,就简单的假设蓝牙功能一旦打开后,应用运行的过程中,蓝牙功能就不会被关闭了。

  2. 程序的主界面退出后,是否可以继续在后台接收对方发送过来的数据?就像微信那样,只要应用没有退出而且有网络,就能一直接收朋友发来的信息?现阶段还是从简化问题的角度出发,我们就假设一旦程序的主界面退出,就不需要在后台继续接收数据了。

1.6 关于遗憾

对于那些没有在这个阶段加入的功能,期待以后加入吧。

对于那些为了简化开发难度、减少开发时间而采用的简单设计,期待在下一版程序中优化和完善吧。

蓝牙聊天 前言

前言

通过“计算器”和“视频播放器”我们已经能够开始开发一些比较像样的应用了。

今天,我们将开始制作一个“蓝牙聊天”应用。这个应用其实很简单,没有炫酷的界面,就是一对一、通过蓝牙连接两台设备,让两个人互相发送信息。

可别觉得它太无聊、没有什么实用性,其实我们正是想通过它让你开始接触网络编程(蓝牙和wifi都是无线连接技术,它们的程序设计方法和思路非常的相似)。

另外,学会了使用蓝牙,就为大家打开了技术开发的另一扇大门-物联网,现在很多物联网硬件都需要使用蓝牙技术进行连接,真是一箭双雕。将这个技术掌握以后,就可以结合我们的另外一篇介绍蓝牙小车的文档,开发蓝牙小车的遥控器了。

这个应用是根据Google在Android SDK中提供的Bluetooth Chat应用改编的。Google的示例只有代码,没有解释说明,所以对很多初学者来说会有很多的疑问。这里将Google的示例程序进行了大范围的改造,并美化了界面,让它便于初学者学习,从原理知识到设计方案都会详细的讨论。希望大家通过这一章节的学习,对不同设备间的通信开发具有整体的认识,并将这里面使用到的技术,作为我们下一步开发的垫脚石。

本文针对的读者是:

  1. 对安卓开发有初步认识同学;
  2. 准备做物联网开发,但是对安卓(控制端)开发感到迷茫的小白;

在开始以前,假设各位已经做好了如下准备:

  • 一台开发用笔记本电脑,并搭建好了开发环境;
  • 两部安卓系统设备(手机或平板电脑);
  • 一根连接电脑和安卓设备的数据线(通常是micro usb数据线);
  • 一到两天时间;
  • 耐心与求知欲;

本文的代码,可以从安豆网示例代码中下载。