代码地址以下:
http://www.demodashi.com/demo/12133.htmlhtml
基于Android Classic Bluetooth的蓝牙聊天软件,目前仅支持一对一实时通讯、文件传输、好友添加、好友分组、好友在线状态更新等功能,其中消息发送支持文本、表情等方式。数据库
蓝牙技术做为一种小范围无线链接技术,可以在设备间实现方便快捷、灵活安全、低成本、低功耗的数据和语音通讯,是目前实现无线我的局域网的主流技术之一。同时,蓝牙系统以自组式组网的方式工做,每一个蓝牙设备均可以在网络中实现路由选择的功能,能够造成移动自组网络。蓝牙的特性在许多方面正好符合Ad Hoc和WPAN的概念,显示了其真正的潜力所在。并且,将蓝牙与其余网络相链接可带来更普遍的应用,例如接入互联网、PSTN或公众移动通讯网,可使用户应用更方便或给用户带来更大的实惠。安全
蓝牙聊天做为一款针对局域网范围内的聊天软件,在办公密集,想实现快速稳定实时通信仍是比较有实用价值的。目前蓝牙技术发展迅速,5.0传输速率已经达到2Mbps,传输级别达到无损级别,有效工做距离可达300米,在蓝牙组网方面技术也在进一步更新,相信要不了多久会有很成熟的方案出来,这样一来就能够实现多人在线实时聊天功能,打破只能一对多实时聊天的界限。网络
一、蓝牙通讯的主从关系
蓝牙技术规定每一对设备之间进行蓝牙通信时,必须一个为主角色,另外一为从角色,才能进行通讯,通讯时,必须由主端进行查找,发起配对,建链成功后,双方便可收发数据。理论上,一个蓝牙主端设备,可同时与7个蓝牙从端设备进行通信。一个具有蓝牙通信功能的设备, 能够在两个角色间切换,平时工做在从模式,等待其它主设备来链接,须要时,转换为主模式,向其它设备发起呼叫。一个蓝牙设备以主模式发起呼叫时,须要知道对方的蓝牙地址,配对密码等信息,配对完成后,可直接发起呼叫。异步
二、蓝牙的呼叫过程
蓝牙主端设备发起呼叫,首先是查找,找出周围处于可被查找的蓝牙设备。主端设备找到从端蓝牙设备后,与从端蓝牙设备进行配对,此时须要输入从端设备的PIN码,也有设备不须要输入PIN码。配对完成后,从端蓝牙设备会记录主端设备的信任信息,此时主端便可向从端设备发起呼叫,已配对的设备在下次呼叫时,再也不须要从新配对。已配对的设备,作为从端的蓝牙设备也能够发起建链请求,但作数据通信的蓝牙模块通常不发起呼叫。链路创建成功后,主从两端之间便可进行双向的数据或语音通信。在通讯状态下,主端和从端设备均可以发起断链,断开蓝牙链路。socket
三、蓝牙一对一的串口数据传输应用
蓝牙数据传输应用中,一对一串口数据通信是最多见的应用之一,蓝牙设备在出厂前即提早设好两个蓝牙设备之间的配对信息,主端预存有从端设备的PIN码、地址等,两端设备加电即自动建链,透明串口传输,无需外围电路干预。一对一应用中从端设备能够设为两种类型,一是静默状态,即只能与指定的主端通讯,不被别的蓝牙设备查找;二是开发状态,既可被指定主端查找,也能够被别的蓝牙设备查找建链。ide
蓝牙聊天功能主要分为如下几个模块:消息模块、好友模块以及我的模块。this
支持一对1、一对多、多对多实时聊天,能传输文字、表情、图片、文件等。对方不在线时可支持离线消息发送,在对方在线时能及时推送过去。消息支持历史消息存储与查看。编码
支持附近好友添加,好友删除,好友分组显示,好友上下线提醒,好友昵称及分组名称修改。线程
展现我的信息,包含昵称、图像、加入时间等信息。
该模块还未实现,目前实现功能主要有一对一实时聊天、能传输文字、表情、文件,支持好友添加、删除、分组。下文主要介绍已经实现的蓝牙通讯流程。
代码实现:
private void findDevice(){ // 得到已经保存的配对设备 Set<BluetoothDevice> pairedDevices = BluetoothAdapter.getDefaultAdapter().getBondedDevices(); if (pairedDevices.size() > 0) { mGroupFriendListData.clear(); GroupInfo groupInfo = new GroupInfo(); groupInfo.setGroupName(BluetoothAdapter.getDefaultAdapter().getName()); List<FriendInfo> friendInfoList = new ArrayList<>(); for (BluetoothDevice device : pairedDevices) { FriendInfo friendInfo = new FriendInfo(); friendInfo.setIdentificationName(device.getName()); friendInfo.setDeviceAddress(device.getAddress()); friendInfo.setFriendNickName(device.getName()); friendInfo.setOnline(false); friendInfo.setJoinTime(DateTime.getStringByFormat(new Date(), DateTime.DEFYMDHMS)); friendInfo.setBluetoothDevice(device); friendInfoList.add(friendInfo); } groupInfo.setFriendList(friendInfoList); groupInfo.setOnlineNumber(0); mGroupFriendListData.add(groupInfo); mGroupFriendAdapter.setGroupInfoList(mGroupFriendListData); } }
好友列表示例图:
若是要让本地设备能够被其余设备发现,那么就要调用ACTION_REQUEST_DISCOVERABLE操做意图的startActivityForResult(Intent, int)方法。这个方法会向系统设置发出一个启用可发现模式的请求。默认状况下,设备的可发现模式会持续120秒。经过给Intent对象添加EXTRA_DISCOVERABLE_DURATION附加字段,能够定义不一样持续时间。应用程序可以设置的最大持续时间是3600秒,0意味着设备始终是可发现的。任何小于0或大于3600秒的值都会自动的被设为120秒。例如,如下代码把持续时间设置为300秒:
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivity(discoverableIntent);
申请用户启用设备的可发现模式时,会显示一个对话框。若是响应“Yes”,那么设备的可发现模式会持续指定的时间,并且你的Activity会接收带有结果代码等于可发现设备持续时间的onActivityResult()回调方法的调用。若是用户响应“No”或有错误发生,则结果代码等于RESULT_CANCELED.
在可发现模式下,设备会静静的把这种模式保持到指定的时长。若是你想要在可发现模式被改变时得到通知,那么你能够注册一个ACTION_SCAN_MODE_CHANGED类型的Intent广播。这个Intent对象中包含了EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE附加字段,它们会分别告诉你新旧扫描模式。它们每一个可能的值是:SCAN_MODE_CONNECTABLE_DISCOVERABLE,SCAN_MODE_CONNECTABLE或SCAN_MODE_NONE,它们分别指明设备是在可发现模式下,仍是在可发现模式下但依然可接收链接,或者是在可发现模式下并不能接收链接。
若是你要初始化跟远程设备的链接,你不须要启用设备的可现性。只有在你想要把你的应用程序做为服务端来接收输入链接时,才须要启用可发现性,由于远程设备在跟你的设备链接以前必须可以发现它。
简单的调用startDiscovery()方法就能够开始发现设备。该过程是异步的,而且该方法会当即返回一个布尔值来指明发现处理是否被成功的启动。一般发现过程会查询扫描大约12秒,接下来获取扫描发现的每一个设备的蓝牙名称。
public class ScanBroadcastReceiver extends BroadcastReceiver { private IScanCallback<BluetoothDevice> scanCallback; private final Map<String, BluetoothDevice> mDeviceMap = new HashMap<>(); public ScanBroadcastReceiver(IScanCallback<BluetoothDevice> scanCallback) { this.scanCallback = scanCallback; } @Override public void onReceive(Context context, Intent intent) { if (scanCallback == null) { return; } if(intent.getAction().equals(BluetoothDevice.ACTION_FOUND)){ //扫描到蓝牙设备 BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (bluetoothDevice == null) { return; } if (!mDeviceMap.containsKey(bluetoothDevice.getAddress())) { mDeviceMap.put(bluetoothDevice.getAddress(), bluetoothDevice); } scanCallback.discoverDevice(bluetoothDevice); }else if(intent.getAction().equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { //扫描设备结束 final List<BluetoothDevice> deviceList = new ArrayList<>(mDeviceMap.values()); if(deviceList != null && deviceList.size() > 0){ scanCallback.scanFinish(deviceList); } else{ scanCallback.scanTimeout(); } } } }
搜索好友示例图:
public class PairBroadcastReceiver extends BroadcastReceiver { private IPairCallback pairCallback; public PairBroadcastReceiver(IPairCallback pairCallback) { this.pairCallback = pairCallback; } @Override public void onReceive(Context context, Intent intent) { if(intent.getAction().equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)){ //取得状态改变的设备,更新设备列表信息(配对状态) BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if(device != null){ resolveBondingState(device.getBondState()); } } } private void resolveBondingState(final int bondState) { if (pairCallback == null) { return; } switch (bondState) { case BluetoothDevice.BOND_BONDED://已配对 pairCallback.bonded(); break; case BluetoothDevice.BOND_BONDING://配对中 pairCallback.bonding(); break; case BluetoothDevice.BOND_NONE://未配对 pairCallback.unBonded(); break; default: pairCallback.bondFail(); break; } } }
配对信息示例图:
配对过程示例图:
当你想要链接两个设备时,一个必须经过持有一个打开的BluetoothServerSocket对象来做为服务端。服务套接字的用途是监听输入的链接请求,而且在一个链接请求被接收时,提供一个BluetoothSocket链接对象。在从BluetoothServerSocket对象中获取BluetoothSocket时,BluetoothServerSocket可以(而且也应该)被废弃,除非你想要接收更多的链接。
如下是创建服务套接字和接收一个链接的基本过程:
一、调用listenUsingRfcommWithServiceRecord(String, UUID)方法来得到一个BluetoothServerSocket对象。该方法中的String参数是一个可识别的你的服务端的名称,系统会自动的把它写入设备上的Service Discovery Protocol(SDP)数据库实体(该名称是任意的,而且能够简单的使用你的应用程序的名称)。UUID参数也会被包含在SDP实体中,而且是跟客户端设备链接的基本协议。也就是说,当客户端尝试跟服务端链接时,它会携带一个它想要链接的服务端可以惟一识别的UUID。只有在这些UUID彻底匹配的状况下,链接才可能被接收。
二、经过调用accept()方法,启动链接请求。这是一个阻塞调用。只有在链接被接收或发生异常的状况下,该方法才返回。只有在发送链接请求的远程设备所携带的UUID跟监听服务套接字所注册的一个UUID匹配的时候,该链接才被接收。链接成功,accept()方法会返回一个被链接的BluetoothSocket对象。
三、除非你想要接收其余链接,不然要调用close()方法。该方法会释放服务套接字以及它所占用的全部资源,但不会关闭被链接的已经有accept()方法所返回的BluetoothSocket对象。跟TCP/IP不同,每一个RFCOMM通道一次只容许链接一个客户端,所以在大多数状况下,在接收到一个链接套接字以后,当即调用BluetoothServerSocket对象的close()方法是有道理的。
如下是以上过程实现的监听线程:
public class AcceptThread extends Thread { private BluetoothChatHelper mHelper; private final BluetoothServerSocket mServerSocket; private String mSocketType; public AcceptThread(BluetoothChatHelper bluetoothChatHelper, boolean secure) { mHelper = bluetoothChatHelper; BluetoothServerSocket tmp = null; mSocketType = secure ? "Secure" : "Insecure"; try { if (secure) { tmp = mHelper.getAdapter().listenUsingRfcommWithServiceRecord(ChatConstant.NAME_SECURE, ChatConstant.UUID_SECURE); } else { tmp = mHelper.getAdapter().listenUsingInsecureRfcommWithServiceRecord(ChatConstant.NAME_INSECURE, ChatConstant.UUID_INSECURE); } } catch (IOException e) { BleLog.e("Socket Type: " + mSocketType + "listen() failed", e); } mServerSocket = tmp; } public void run() { BleLog.i("Socket Type: " + mSocketType + "BEGIN mAcceptThread" + this); setName("AcceptThread" + mSocketType); BluetoothSocket socket = null; while (mHelper.getState() != com.vise.basebluetooth.common.State.STATE_CONNECTED) { try { BleLog.i("wait new socket:" + mServerSocket); socket = mServerSocket.accept(); } catch (IOException e) { BleLog.e("Socket Type: " + mSocketType + " accept() failed", e); break; } if (socket != null) { synchronized (this) { if(mHelper.getState() == com.vise.basebluetooth.common.State.STATE_LISTEN || mHelper.getState() == com.vise.basebluetooth.common.State.STATE_CONNECTING){ BleLog.i("mark CONNECTING"); mHelper.connected(socket, socket.getRemoteDevice(), mSocketType); } else if(mHelper.getState() == com.vise.basebluetooth.common.State.STATE_NONE || mHelper.getState() == com.vise.basebluetooth.common.State.STATE_CONNECTED){ try { socket.close(); } catch (IOException e) { BleLog.e("Could not close unwanted socket", e); } } } } } BleLog.i("END mAcceptThread, socket Type: " + mSocketType); } public void cancel() { BleLog.i("Socket Type" + mSocketType + "cancel " + this); try { mServerSocket.close(); } catch (IOException e) { BleLog.e("Socket Type" + mSocketType + "close() of server failed", e); } } }
如下是一个基本的链接过程:
一、经过调用BluetoothDevice的createRfcommSocketToServiceRecord(UUID)方法,得到一个BluetoothSocket对象。这个方法会初始化一个链接到BluetoothDevice对象的BluetoothSocket对象。传递给这个方法的UUID参数必须与服务端设备打开BluetoothServerSocket对象时所使用的UUID相匹配。在你的应用程序中简单的使用硬编码进行比对,若是匹配,服务端和客户端代码就能够应用这个BluetoothSocket对象了。
二、经过调用connect()方法来初始化链接。在这个调用中,为了找到匹配的UUID,系统会在远程的设备上执行一个SDP查询。若是查询成功,而且远程设备接收了该链接请求,那么它会在链接期间共享使用RFCOMM通道,而且connect()方法会返回。这个方法是一个阻塞调用。若是由于某些缘由,链接失败或链接超时(大约在12秒以后),就会抛出一个异常。
如下是实现以上过程的链接线程:
public class ConnectThread extends Thread { private BluetoothChatHelper mHelper; private final BluetoothSocket mSocket; private final BluetoothDevice mDevice; private String mSocketType; public ConnectThread(BluetoothChatHelper bluetoothChatHelper, BluetoothDevice device, boolean secure) { mHelper = bluetoothChatHelper; mDevice = device; BluetoothSocket tmp = null; mSocketType = secure ? "Secure" : "Insecure"; try { if (secure) { tmp = device.createRfcommSocketToServiceRecord(ChatConstant.UUID_SECURE); } else { tmp = device.createInsecureRfcommSocketToServiceRecord(ChatConstant.UUID_INSECURE); } } catch (IOException e) { BleLog.e("Socket Type: " + mSocketType + "create() failed", e); } mSocket = tmp; } public void run() { BleLog.i("BEGIN mConnectThread SocketType:" + mSocketType); setName("ConnectThread" + mSocketType); mHelper.getAdapter().cancelDiscovery(); try { mSocket.connect(); } catch (IOException e) { try { mSocket.close(); } catch (IOException e2) { BleLog.e("unable to close() " + mSocketType + " socket during connection failure", e2); } mHelper.connectionFailed(); return; } synchronized (this) { mHelper.setConnectThread(null); } mHelper.connected(mSocket, mDevice, mSocketType); } public void cancel() { try { mSocket.close(); } catch (IOException e) { BleLog.e("close() of connect " + mSocketType + " socket failed", e); } } }
在创建链接以前要调用cancelDiscovery()方法。在链接以前应该始终调用这个方法,而且不用实际的检查蓝牙发现处理是否正在运行也是安全的(若是想要检查,调用isDiscovering()方法)。
当你成功的链接了两个(或更多)设备时,每个设备都有一个被链接的BluetoothSocket对象。这是良好的开始,由于你可以在设备之间共享数据。使用BluetoothSocket对象来传输任意数据的过程是简单的:
一、分别经过getInputStream()和getOutputStream()方法来得到经过套接字来处理传输任务的InputStream和OutputStream对象;
二、用read(byte[])和write(byte[])方法来读写流中的数据。
如下为实现以上过程的通讯线程:
public class ConnectedThread extends Thread { private final BluetoothChatHelper mHelper; private final BluetoothSocket mSocket; private final InputStream mInStream; private final OutputStream mOutStream; public ConnectedThread(BluetoothChatHelper bluetoothChatHelper, BluetoothSocket socket, String socketType) { BleLog.i("create ConnectedThread: " + socketType); mHelper = bluetoothChatHelper; mSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { BleLog.e("temp sockets not created", e); } mInStream = tmpIn; mOutStream = tmpOut; } public void run() { BleLog.i("BEGIN mConnectedThread"); int bytes; byte[] buffer = new byte[1024]; // Keep listening to the InputStream while connected while (true) { try { bytes = mInStream.read(buffer); byte[] data = new byte[bytes]; System.arraycopy(buffer, 0, data, 0, data.length); mHelper.getHandler().obtainMessage(ChatConstant.MESSAGE_READ, bytes, -1, data).sendToTarget(); } catch (IOException e) { BleLog.e("disconnected", e); mHelper.start(false); break; } } } public void write(byte[] buffer) { if(mSocket.isConnected()){ try { mOutStream.write(buffer); mHelper.getHandler().obtainMessage(ChatConstant.MESSAGE_WRITE, -1, -1, buffer).sendToTarget(); } catch (IOException e) { BleLog.e("Exception during write", e); } } } public void cancel() { try { mSocket.close(); } catch (IOException e) { BleLog.e("close() of connect socket failed", e); } } }
发送消息示例图:
发送表情示例图:
发送文件示例图:
基于Android Classic Bluetooth的蓝牙聊天软件
代码地址以下:
http://www.demodashi.com/demo/12133.html
注:本文著做权归做者,由demo大师代发,拒绝转载,转载须要做者受权