代碼地址以下:
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大師代發,拒絕轉載,轉載須要做者受權