第2节 设计方案
功能确定后,就要开始围绕功能进行功能的验证、界面设计的规划、以及程序结构的规划了。
2.1 技术验证
选定了现阶段要完成的核心功能后,我们首先需要对它们做技术上的验证,看看用什么样的方法能实现它们。在进行技术验证的同时,也能让我们发现很多我们在头脑风暴阶段没有意识到的现实问题。
现在的手机和移动设备已经把蓝牙作为了标准配置,它常常用到与周边小设备的数据连接上,例如蓝牙自拍杆,蓝牙音箱,蓝牙键盘,运动手环等等,可以看出这都是一些不需要太大数据量传输而需要保持长时间数据通信的设备。
蓝牙技术从出现到现在经过了多个版本,现在手机上最普遍的就是蓝牙4.0了。
作为应用程序的开发者,其实并不需要知道很多与蓝牙技术相关的硬件知识。Google将软件开发者会用到的所有蓝牙功能,都封装了起来,通过Android SDK提供给开发者使用。所以,只要我们知道如何使用Android SDK中提供的蓝牙接口函数就可以了。至于硬件是如何实现的无线电波互相通信,开发者完全不需要去关心。
要操作蓝牙设备,我们首先要获取操作蓝牙设备的对象BluetoothAdapter
,
BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter();
同时,还需要使用到三项系统权限,
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.anddle.anddlechat">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
......
</manifest>
之后我们就可以通过BluetoothAdapter
对真实的蓝牙设备以及软件功能进行操作使用了。
2.1.1 设备发现
一句简单的“让设备互相连通”,实际上却包含了很多隐藏的任务。
2.1.1.1 开启蓝牙
用户并不一定总是把蓝牙设备开启的,因为我们的应用要使用蓝牙,假如用户没有打开蓝牙设备,我们的程序得打开它或者提示让用户主动打开它。
有三种方式开启蓝牙功能,
- 使用安卓系统提供的开关打开。例如,在
设置->蓝牙
界面中打开, - 在应用中使用
Intent
启动确认窗口,让用户选择是否允许打开,BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter(); //判断蓝牙功能是否打开 if (!BTAdapter.isEnabled()) { //没有打开,就启动确认窗口询问用户是否打开 Intent i = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivity(i); }
- 在应用中直接打开蓝牙功能,不需要询问用户,
BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter(); //判断蓝牙功能是否打开 if (!BTAdapter.isEnabled()) { ////没有打开,就直接打开 BTAdapter.enable(); }
2.1.1.2 允许被发现
蓝牙打开后,设备并不一定能够别的蓝牙设备找到,出于安全角度考虑,很多设备的蓝牙开关即使是打开的,也有可能禁止让别的设备发现自己。
为了保险起见,我们需要设置设备可以被别的蓝牙设备发现,
- 使用
BluetoothAdapter
的getScanMode()
方法,获取当前的蓝牙是否能被发现; - 通过
BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE
的Intent
启动询问窗口; BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION
用来指定允许被发现的时间,例如,120
就表示未来120
秒内该蓝牙设备允许被别人发现;如果将该值设置成0,表示可以一直被发现;
BluetoothAdapter BTAdapter = BluetoothAdapter.getDefaultAdapter();
//判断是非可以被发现
if(BTAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
//启动窗口,询问用户是否可以设置成允许被发现
Intent i = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//可以一直被别的蓝牙设备发现
i.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
startActivity(i);
}
2.1.1.3 搜索可连接设备
搜索可连接的蓝牙设备,
- 使用
BluetoothAdapter
的startDiscovery()
方法开始搜素设备BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); //如果正在搜索蓝牙设备,则停止搜索 if (mBluetoothAdapter.isDiscovering()) { mBluetoothAdapter.cancelDiscovery(); } //开始搜索蓝牙设备 mBluetoothAdapter.startDiscovery();
- 当开始搜索的时候,系统会发出一个
BluetoothAdapter.ACTION_DISCOVERY_STARTED
广播;当结束搜索的时候,系统会发出一个
BluetoothAdapter.ACTION_DISCOVERY_FINISHED
广播;当找到某个可连接设备的时候,系统会发出一个
BluetoothAdapter.ACTION_FOUND
广播;并把可连设备的信息放在广播对应的Intent当中;因此,需要自定义一个
BroadcastReceiver
来接收这些广播,private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) { //从Intent中获取可连接的设备 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) { //开始搜索了 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) { //结束搜索了 } } };
- 接收机采用动态的方式,在应用启动的时候被注册,不使用的时候要注销,
//注册接收机 IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(mReceiver, filter); ------------------------------------------ //注销接收机 unregisterReceiver(mReceiver);
2.1.1.4 搜索结果展示
应用需要一个搜索附近可连设备的功能,它能把附近的设备搜索出来,形成列表供用户选择,如果用户选择了某个设备,那么它就要去主动连接这个设备。
在设置->蓝牙
界面就有这样的展示,
但是,我们自身的聊天应用也要有一个类似的界面,方便用户选择设备,在同一个应用内就完成展示和连接的功能。
2.1.2 设备连接
蓝牙设备的连接(wifi设备也是如此)可以分成主动连接和被动连接。
设备的连接是通过Socket-套结字
实现的。所谓Socket
就像它的英文意思“插座”一样,将两个不同的实体通过插口插座连接起来。
两个实体连接起来后,就可以通过套结字来传送数据了。
/*******************************************************************/
* 版权声明
* 本教程只在CSDN和安豆网发布,其他网站出现本教程均属侵权。
/*******************************************************************/
2.1.2.1 被动连接
应用如果不去主动发现周围设备,不主动去连接其它设备,那它可以作为一个被动的接受者,等待别人的连接。
- 创建一个
BluetoothServerSocket
,需要给他指定一个名字和UUID。名字可以任意指定,UUID就有一定的学问了。使用相同UUID的蓝牙应用,可以被彼此发现。蓝牙设备的UUID可以分成两个大类,
- 由蓝牙标准统一规定的UUID。例如
00001101-0000-1000-8000-00805F9B34FB
这个UUID代表的就是蓝牙串口服务
- 自定义UUID,蓝牙应用的开发者可以自己“设计”一个UUID。这个UUID不能与已有规定的UUID相同,只是为了将自己的蓝牙应用与别的蓝牙应用区分开。
蓝牙的UUID概念有点像网络编程中的端口号。
例如,按照规定,端口号23是分配给FTP服务使用的,其它FTP应用想要彼此通信,不用去猜对方的端口号是什么,只要知道协议中已经规定了23,大家都会遵守就行,设计出来的应用绝不会有问题。
开发者也可以为自己的应用指定一个不知名的端口号,不需要和别的应用互通。
这里我们就使用
蓝牙串口服务
的UUIDBluetoothAdapter BluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); final String BT_NAME = "AnddleChat"; final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); BluetoothServerSocket mServerSocket = mBluetoothAdapter.listenUsingRfcommWithServiceRecord( BT_NAME, BT_UUID);
- 由蓝牙标准统一规定的UUID。例如
- 启动一个线程-叫它
监听线程
,在它当中,使用BluetoothServerSocket
的accept()
方法,等待其它设备的连接;BluetoothSocket socket = mServerSocket.accept();
这是一个阻塞的调用。就是说一旦执行
accept()
函数,它就一直等在那里,不会返回了。这也是要将等待连接单独放到监听线程执行的原因。如果
accept()
函数返回了,有两种可能,- 有别的设备向自己发出了连接的请求。
accept()
返回后,socket
就是被连接设备与连接设备建立的Socket
,这也正是程序所一直等待的; mServerSocket
的close()
函数被调用,导致accept()
抛出了异常,//别的地方关闭了BluetoothServerSocket mServerSocket.close() ---------------------------------- //监听线程 try { socket = mServerSocket.accept(); } catch (IOException e) { //正在监听会抛出异常 }
- 有别的设备向自己发出了连接的请求。
经过上面的流程,监听线程在收到其它蓝牙设备的连接后,就得到了Socket
。
2.1.2.2 主动连接
蓝牙设备搜索到周围的可连接设备后,可以主动向它们发起连接请求。
- 通过对方的地址,获取蓝牙设备,
BluetoothAdapter BluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddr);
- 创建可发起连接请求的
Socket
,创建时要使用到UUID,发起连接的设备的UUID要与被动接受连接的设备的UUID要相同,就像是要把发送和接受调整到同样的频道上一样,final UUID BT_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); BluetoothSocket mSocket = device.createRfcommSocketToServiceRecord(BT_UUID);
- 用
connect()
函数主动发起连接请求,该函数是一个阻塞调用,不确定返回的时间,所以需要为它开启一个单独的线程执行,//工作线程中执行 mSocket.connect();
一旦执行成功后,我们就得到了可以与其它蓝牙设备通信的
Socket
。
2.1.3 数据交换
连接建立以后,双方开始交换数据。无论是主动连接还是被动连接,数据的交换都是通过之前得到的Socket
进行的。
- 获取读数据和发送数据的能力。
Socket
交换的数据都是以二进制的形式进行的。因此可以使用流的方式对数据进行操作,InputStream mInStream = mSocket.getInputStream(); OutputStream mOutStream = mSocket.getOutputStream();
InputStream
负责读取从Socket
传入的数据;OutputStream
负责将数据从Socket
发送出去; - 采用
InputStream
的read()
方法读取数据。这是一个阻塞的方法,只有连接的对方有数据传送过来的时候才会返回,并把传来的数据放在buffer
参数当中,返回值代表读取的数据字节长度。读完一次以后,需要重复调用
read()
继续读取接下来的数据,这是一个循环的过程,//存放传入数据的数据区 byte[] buffer = new byte[MAX_BUFFER_SIZE]; //存放读取数据的字节长度 int bytes; while (true) { try { //等待读取数据 bytes = mInStream.read(buffer); } catch (IOException e) { //异常情况,例如Socket被关闭,对方断开连接 break; } }
当别当地方关闭了
Socket
后,mInStream.read()
会抛出异常,“`java
//别的地方关闭了Socket
mSocket.close()
//监听线程
try {
//等待读取数据
bytes = mInStream.read(buffer);
} catch (IOException e) {
//异常情况,例如Socket被关闭
break;
}
“`
因为mInStream
是通过mSocket
创建的,两者之间已经建立了联系。所以当关闭mSocket
的时候mInStream
也会收到影响,从而抛出异常。
- 采用
OutputStream
的write()
方法发送数据mOutStream.write(data);
在使用Socket
的过程中,很多地方都可能抛出异常,所以在后面设计程序结构的时候,我们要好好考虑如何对应这些异常情况。
2.2 界面设计
确定了功能,进行了技术调查之后,我们就可以开始根据目前掌握的信息,设计应用的使用流程了。
应用流程分为三个部分,
2.2.1 应用启动
- 如果蓝牙功能没有打开,启动应用后提示打开蓝牙功能,
- 如果不能被其它蓝牙设备发现,启动应用后提示允许被其它蓝牙设备发现,
- 如果蓝牙功能已经打开,并且可以被其它设备发现,进入主界面,
2.2.2 设备主动连接及信息发送流程
2.2.3 设备被动连接及信息发送流程,
2.2.4 关于界面
2.3 程序结构
根据应用的功能和界面逻辑,程序结构将分成三个模块,
- 聊天记录的展示和发送。使用一个
Activity
-ChatActivity
来完成这部分工作; - 发现可连接的蓝牙设备,让用户选择要连接的设备。使用一个
Activity
-DeviceListActivity
来实现。当用户选中了某个设备后,将结果它告诉ChatActivity
,统一由ChatActivity
来进行连接的逻辑控制; - 连接管理模块。连接蓝牙设备、设备之间发送消息、断开连接等等和通信密切相关的工作都交给
ConnectionManager
模块。该模块提供简单易用的接口給ChatActivity
调用。