第4节 蓝牙连接模块
蓝牙连接的管理模块需要为ChatActivity
提供于连接相关的所有功能,要设计的方便使用,并尽量隐藏连接的细节。
4.1 对外接口
我们首先来看看ConnectionManager
需要向Chat Activity
提供哪些接口。
- 监听。当应用运行起来后,聊天应用需要启动对其它蓝牙设备的监听,迎接随时可能到来的连接请求。所以
ConnectionManager
需要提供启动监听-startListen()
和停止监听-stopListen()
的两个接口; - 主动连接。应用搜索到可连接到设备后,可以主动的连接对方设备。假如正在连接,用户可能会取消连接;假如已经连接上,用户可能会想断开这个连接。所以
ConnectionManager
需要提供连接-connect()
、断开连接-disconnect()
(包含取消连接的功能)两个接口; - 主动查询当前连接状态。应用在采取某些操作的时候,可能需要知道当前的连接状态。所以需要提供主动查询当前连接状态的接口。
连接的状态,我们可以把它分成两类,一个是监听的状态,一个是连接的状态。监听的状态表示当前是否在监听;连接的状态应该包括没有连接,正在连接和已经连接。
所以接口应该有
getCurrentListenState()
和getCurrentConnectState()
。 - 获取连接状态变化通知。应用界面需要根据连接的状态做出相应的变化,需要
ConnectionManager
能在连接状态发生变化的时候,主动把信息通知给ChatActivity
。所以还要提供一个通知状态变化的接口。 - 接收数据。
ConnectionManager
收到数据以后,需要一个机制将数据通知给ChatActivity
,这样ChatActivity
才能显示接收到的内容。 - 发送数据。用户要发送数据给被连接的设备,所以
ConnectionManager
要提供一个发送数据的接口-sendData()
。
4
与5
都需要ConnectionManager
主动向ChatActivity
传递信息(收到的数据、状态改变),可以通过设计监听器来实现。因此需要设计安装监听器的方法:定义一个回调的接口-ConnectionListener
;把监听器设置到ConnectioManager
中。
综上所述,ConnectionManager
大概应该是这个样子,
public class ConnectionManager {
//定义监听器
public interface ConnectionListener {
//当ConnectionManager的连接状态发生变化,
//通过onConnectStateChange()将变化通知到ChatActivity
public void onConnectStateChange(int oldState, int State);
//当ConnectionManager的监听状态发生变化,
//通过onListenStateChange()将变化通知到ChatActivity
public void onListenStateChange(int oldState, int State);
//当ConnectionManager的数据发送完成后,
//通过onSendData()将发送的内容通知到ChatActivity
public void onSendData(boolean suc, byte[] data);
//当ConnectionManager的接收到对方连接设备传来的数据,
//通过onReadData()将传来的数据通知到ChatActivity
public void onReadData(byte [] data);
}
private ConnectionListener mConnectionListener;
//构造函数中设置监听器
public ConnectionManager(ConnectionListener cl) {
mConnectionListener = cl;
}
public void startListen() {
}
public void stopListen() {
}
public void connect() {
}
public void disconnect() {
}
public int getCurrentListenState() {
}
public int getCurrentConnectState() {
}
public void sendData(byte[] data) {
}
}
/*******************************************************************/
* 版权声明
* 本教程只在CSDN和安豆网发布,其他网站出现本教程均属侵权。
/*******************************************************************/
4.2 ConnectionManager的结构
我们已经明确了ConnectionManager
对外的接口。接下来就需要来实现功能了。
由Android SDK提供的蓝牙连接接口函数,很多都是阻塞的(wifi连接也是如此),例如accept()
、connect()
等等,所以就需要将这些操作放到单独的工作线程中进行。
因此,我们将会在ConnectionManager
中设计两个工作线程,
AcceptThread
:负责监听别的蓝牙设备发起的连接;ConnectThread
:用来维持与其它设备的连接。
主线程首先创建一个监听线程,用来接收其它设备可能发出的连接请求,当监听线程监听到了连接请求,就会得到一个Socket
;然后我们再创建一个连接线程,将Socket
交给连接线程,两个设备就可以通过这个Socket
,在连接线程中进行消息的发送和接收了。
4.2.1 ConnectionManager
AcceptThread
线程的工作状态,反应的就是监听状态。它包括
LISTEN_STATE_IDLE
和LISTEN_STATE_LISTENING
。这个状态保存在mListenState
变量中;当监听状态发生改变的时候,通过
setListenState()
设置状态的改变,并利用回调的方式,将改变的状态通知给关注者。public class ConnectionManager { ...... //监听的两种状态 public static final int LISTEN_STATE_IDLE = 3; public static final int LISTEN_STATE_LISTENING = 4; //记录当前监听的状态 private int mListenState = LISTEN_STATE_IDLE; //修改当前监听的状态 private void setListenState(int state) { //状态没有发生变化,不用通知 if(mListenState == state) { return; } int oldState = mListenState; mListenState = state; //状态发生变化,发起通知 if(mConnectionListener != null) { mConnectionListener.onListenStateChange(oldState, mListenState); } } ...... }
ConnectedThread
线程的工作状态,反应的就是连接状态。它包括
CONNECT_STATE_IDLE
CONNECT_STATE_CONNECTING
和CONNECT_STATE_CONNECTED
;这个状态保存在
mConnectState
变量中;当连接状态发生改变的时候,通过
setConnectState()
设置状态的改变,并利用回调的方式,将改变的状态通知给关注者。public class ConnectionManager { ...... //连接的三种状态 public static final int CONNECT_STATE_IDLE = 0; public static final int CONNECT_STATE_CONNECTING = 1; public static final int CONNECT_STATE_CONNECTED = 2; //记录当前连接的状态 private int mConnectState = CONNECT_STATE_IDLE; //修改当前连接的状态 private void setConnectState(int state) { ///状态没有发生变化,不用通知 if(mConnectState == state) { return; } int oldState = mConnectState; mConnectState = state; //状态发生变化,发起通知 if(mConnectionListener != null) { mConnectionListener.onConnectStateChange(oldState, mConnectState); } } ...... }
ConnectionManager
对外提供的接口,实际上就是对这两个工作线程的控制;
public class ConnectionManager {
......
private ConnectionListener mConnectionListener;
private AcceptThread mAcceptThread;
private ConnectedThread mConnectedThread;
private final BluetoothAdapter mBluetoothAdapter;
//构造函数中设置监听器
public ConnectionManager(ConnectionListener cl) {
mConnectionListener = cl;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
public void startListen() {
//创建监听线程
if(mAcceptThread != null) {
mAcceptThread.cancel();
}
mAcceptThread = new AcceptThread();
mAcceptThread.start();
}
public void stopListen() {
//停止监听线程
if(mAcceptThread != null) {
mAcceptThread.cancel();
}
}
public void connect() {
//发起连接
if(mConnectedThread != null) {
mConnectedThread.cancel();
}
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(deviceAddr);
try {
//创建发起主动连接使用的Socket
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(BT_UUID);
//启动连接线程
mConnectedThread = new ConnectedThread(socket, true);
mConnectedThread.start();
} catch (IOException e) {
}
}
public void disconnect() {
//停止连接线程
if(mConnectedThread != null) {
mConnectedThread.cancel();
}
}
public int getCurrentListenState() {
//查询当前监听线程的状态
return mListenState;
}
public int getCurrentConnectState() {
//查询当前连接线程的状态
return mConnectState;
}
public boolean sendData(byte[] data) {
//发送数据
if(mConnectedThread != null
&& mConnectState == CONNECT_STATE_CONNECTED) {
mConnectedThread.sendData(data);
return true;
}
return false;
}
......
}
通过这里,我们可以看出,为了能够取消正在运行的工作线程,在设计AcceptThread
和ConnectedThread
的时候,我们需要给它们添加上取消的方法-cancel()
;为了在连接后能够发送数据,需要给ConnectedThread
添加发送数据的方法-sendData()
。
4.2.2 监听线程
监听线程进行的工作有,
- 等待其它蓝牙设备发起的连接;
- 如果接收到连接的请求,就创建出一个
Socket
; - 之后继续等待其它设备可能发起的连接;
- 假如已经处于正在连接或者已经连接的状态,就断开最新收到的连接,因为我们假定了每次只能连接一个设备;
- 监听线程可以被取消,退出运行;
public class ConnectionManager {
......
private class AcceptThread extends Thread {
private BluetoothServerSocket mServerSocket;
private boolean mUserCancel;
public AcceptThread() {
BluetoothServerSocket tmp = null;
mUserCancel = false;
//创建监听用的ServerSocket
try {
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(
BT_NAME, BT_UUID);
} catch (IOException e) {
}
mServerSocket = tmp;
}
//监听线程开始运行
@Override
public void run() {
setName("AcceptThread");
//将ConnectionManger监听的状态设置成“正在监听”
setListenState(LISTEN_STATE_LISTENING);
BluetoothSocket socket = null;
while(!mUserCancel) {
try {
//阻塞在这里,等待别的设备连接
socket = mServerSocket.accept();
} catch (IOException e) {
//阻塞过程中,如果其它地方调用了mServerSocket.close(),
//将会进入到这个异常当中
mServerSocket = null;
break;
}
if(mConnectState == CONNECT_STATE_CONNECTED
|| mConnectState == CONNECT_STATE_CONNECTING) {
//如果当前正在连接别的设备,
//或者已经和别的设备连接上了,就放弃这个连接,
//因为每次只能和一个设备连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
else if(mConnectState == CONNECT_STATE_IDLE) {
//如果当前没有和别的设备连接上,
//启动连接线程
mConnectedThread = new ConnectedThread(socket, false);
mConnectedThread.start();
}
}
if(mServerSocket != null) {
try {
mServerSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
mServerSocket = null;
}
setListenState(LISTEN_STATE_IDLE);
mAcceptThread = null;
}
//让监听线程退出运行
public void cancel() {
try {
mUserCancel = true;
//ServerSocket此时阻塞在accept()方法中,
//关闭之后,会让accept()方法抛出异常,实现监听线程的退出
if(mServerSocket != null) {
mServerSocket.close();
}
} catch (IOException e) {
}
}
}
......
}
4.2.3 连接线程
连接线程的工作是,
- 假如是主动连接,连接线程要主动调用
connect()
方法;该方法是一个阻塞调用,在调用前,应该把ConnectionManager
的连接状态,设置成CONNECT_STATE_CONNECTING
;如果连接成功,就把状态设置成CONNECT_STATE_CONNECTED
; - 假如是被动连接,那么从监听线程获取的
Socket
就已经是被连接上了的Socket
,不需要进行connect()
的操作了; - 从已经连接的
Socket
当中,获取输入、输出的流接口,以后这个连接上的数据读取和写入,就是通过它们对应的流接口进行的; - 连接线程进入循环,不断的尝试通过
InputStream
的read()
方法,读取数据; - 连接线程可以被取消,退出连接;
public class ConnectionManager {
......
private class ConnectedThread extends Thread {
private final int MAX_BUFFER_SIZE = 1024;
private BluetoothSocket mSocket;
private InputStream mInStream;
private OutputStream mOutStream;
private boolean mUserCancel;
private boolean mNeedConnect;
//needConnect参数告诉连接线程是否需要主动发起连接,
//因为通过监听线程启动的连接线程是不需要发起主动连接的,
//所以需要一个标志位来控制这种情况
public ConnectedThread(BluetoothSocket socket, boolean needConnect) {
//保存下工作线程中要用到的参数
setName("ConnectedThread");
mNeedConnect = needConnect;
mSocket = socket;
mUserCancel = false;
}
@Override
public void run() {
//将ConnectionManager的连接状态修改成CONNECT_STATE_CONNECTING
setConnectState(CONNECT_STATE_CONNECTING);
//如果这是一个主动连接,说明Socket还没有和对方相连接,就需要发起主动连接
if(mNeedConnect && !mUserCancel) {
try {
mSocket.connect();
} catch (IOException e) {
//主动连接发生异常,回到未连接的状态
setConnectState(CONNECT_STATE_IDLE);
mSocket = null;
mConnectedThread = null;
return;
}
}
InputStream tmpIn = null;
OutputStream tmpOut = null;
//从连接的Socket中获取读数据和写数据的流接口
try {
tmpIn = mSocket.getInputStream();
tmpOut = mSocket.getOutputStream();
} catch (IOException e) {
setConnectState(CONNECT_STATE_IDLE);
mSocket = null;
mConnectedThread = null;
return;
}
mInStream = tmpIn;
mOutStream = tmpOut;
//将ConnectionManager的连接状态修改成CONNECT_STATE_CONNECTED
setConnectState(CONNECT_STATE_CONNECTED);
byte[] buffer = new byte[MAX_BUFFER_SIZE];
int bytes;
while (!mUserCancel) {
try {
//阻塞在这里,用流接口等待读取数据
bytes = mInStream.read(buffer);
//将读取到的数据传递给关注它的组件
if(mConnectionListener != null && bytes > 0) {
byte [] data = new byte[bytes];
System.arraycopy(buffer, 0, data, 0, bytes);
mConnectionListener.onReadData(data);
}
} catch (IOException e) {
//阻塞过程中,如果其它地方调用了mSocket.close(),
//或者对方的连接关闭
//将会进入到这个异常当中
break;
}
}
setConnectState(CONNECT_STATE_IDLE);
mSocket = null;
mConnectedThread = null;
}
//让连接线程退出运行
public void cancel() {
try {
mUserCancel = true;
//Socket此时阻塞在InputStream的read()方法中,
//关闭之后,会让read()方法抛出异常
if(mSocket != null) {
mSocket.close();
}
} catch (IOException e) {
}
}
//向对方发送数据
public void sendData(byte[] data) {
try {
//用流接口发送数据
mOutStream.write(data);
//向关心的组件通知发送成功
if(mConnectionListener != null) {
mConnectionListener.onSendData(true, data);
}
} catch (IOException e) {
//向关心的组件通知发送失败
if(mConnectionListener != null) {
mConnectionListener.onSendData(false, data);
}
}
}
}
......
}
至此,ConnectionManager
就设计好了。