Android藍牙聊天開源項目
前言
基于Android Classic Bluetooth的藍牙聊天軟件,目前僅支持一對一實時通信、文件傳輸、好友添加、好友分組、好友在線狀態(tài)更新等功能,其中消息發(fā)送支持文本、表情等方式。
項目地址:Android藍牙聊天項目
前景
藍牙技術(shù)作為一種小范圍無線連接技術(shù),能夠在設(shè)備間實現(xiàn)方便快捷、靈活安全、低成本、低功耗的數(shù)據(jù)和語音通信,是目前實現(xiàn)無線個人局域網(wǎng)的主流技術(shù)之一。同時,藍牙系統(tǒng)以自組式組網(wǎng)的方式工作,每個藍牙設(shè)備都可以在網(wǎng)絡(luò)中實現(xiàn)路由選擇的功能,可以形成移動自組網(wǎng)絡(luò)。藍牙的特性在許多方面正好符合Ad Hoc和WPAN的概念,顯示了其真正的潛力所在。而且,將藍牙與其他網(wǎng)絡(luò)相連接可帶來更廣泛的應(yīng)用,例如接入互聯(lián)網(wǎng)、PSTN或公眾移動通信網(wǎng),可以使用戶應(yīng)用更方便或給用戶帶來更大的實惠。
藍牙聊天作為一款針對局域網(wǎng)范圍內(nèi)的聊天軟件,在辦公密集,想實現(xiàn)快速穩(wěn)定實時通訊還是比較有實用價值的。目前藍牙技術(shù)發(fā)展迅速,5.0傳輸速率已經(jīng)達到2Mbps,傳輸級別達到無損級別,有效工作距離可達300米,在藍牙組網(wǎng)方面技術(shù)也在進一步更新,相信要不了多久會有很成熟的方案出來,這樣一來就可以實現(xiàn)多人在線實時聊天功能,打破只能一對多實時聊天的界限。
技術(shù)簡介
1、藍牙通信的主從關(guān)系
藍牙技術(shù)規(guī)定每一對設(shè)備之間進行藍牙通訊時,必須一個為主角色,另一為從角色,才能進行通信,通信時,必須由主端進行查找,發(fā)起配對,建鏈成功后,雙方即可收發(fā)數(shù)據(jù)。理論上,一個藍牙主端設(shè)備,可同時與7個藍牙從端設(shè)備進行通訊。一個具備藍牙通訊功能的設(shè)備, 可以在兩個角色間切換,平時工作在從模式,等待其它主設(shè)備來連接,需要時,轉(zhuǎn)換為主模式,向其它設(shè)備發(fā)起呼叫。一個藍牙設(shè)備以主模式發(fā)起呼叫時,需要知道對方的藍牙地址,配對密碼等信息,配對完成后,可直接發(fā)起呼叫。
2、藍牙的呼叫過程
藍牙主端設(shè)備發(fā)起呼叫,首先是查找,找出周圍處于可被查找的藍牙設(shè)備。主端設(shè)備找到從端藍牙設(shè)備后,與從端藍牙設(shè)備進行配對,此時需要輸入從端設(shè)備的PIN碼,也有設(shè)備不需要輸入PIN碼。配對完成后,從端藍牙設(shè)備會記錄主端設(shè)備的信任信息,此時主端即可向從端設(shè)備發(fā)起呼叫,已配對的設(shè)備在下次呼叫時,不再需要重新配對。已配對的設(shè)備,做為從端的藍牙設(shè)備也可以發(fā)起建鏈請求,但做數(shù)據(jù)通訊的藍牙模塊一般不發(fā)起呼叫。鏈路建立成功后,主從兩端之間即可進行雙向的數(shù)據(jù)或語音通訊。在通信狀態(tài)下,主端和從端設(shè)備都可以發(fā)起斷鏈,斷開藍牙鏈路。
3、藍牙一對一的串口數(shù)據(jù)傳輸應(yīng)用
藍牙數(shù)據(jù)傳輸應(yīng)用中,一對一串口數(shù)據(jù)通訊是最常見的應(yīng)用之一,藍牙設(shè)備在出廠前即提前設(shè)好兩個藍牙設(shè)備之間的配對信息,主端預(yù)存有從端設(shè)備的PIN碼、地址等,兩端設(shè)備加電即自動建鏈,透明串口傳輸,無需外圍電路干預(yù)。一對一應(yīng)用中從端設(shè)備可以設(shè)為兩種類型,一是靜默狀態(tài),即只能與指定的主端通信,不被別的藍牙設(shè)備查找;二是開發(fā)狀態(tài),既可被指定主端查找,也可以被別的藍牙設(shè)備查找建鏈。
功能概述
藍牙聊天功能主要分為以下幾個模塊:消息模塊、好友模塊以及個人模塊。
消息模塊
支持一對一、一對多、多對多實時聊天,能傳輸文字、表情、圖片、文件等。對方不在線時可支持離線消息發(fā)送,在對方在線時能及時推送過去。消息支持歷史消息存儲與查看。
好友模塊
支持附近好友添加,好友刪除,好友分組顯示,好友上下線提醒,好友昵稱及分組名稱修改。
個人模塊
展示個人信息,包含昵稱、圖像、加入時間等信息。

該模塊還未實現(xiàn),目前實現(xiàn)功能主要有一對一實時聊天、能傳輸文字、表情、文件,支持好友添加、刪除、分組。下文主要介紹已經(jīng)實現(xiàn)的藍牙通信流程。
操作流程
查找已配對設(shè)備(即好友列表)
代碼實現(xiàn):
private void findDevice(){
// 獲得已經(jīng)保存的配對設(shè)備
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);
}
}
好友列表示例圖:

啟用設(shè)備的可發(fā)現(xiàn)性
如果要讓本地設(shè)備可以被其他設(shè)備發(fā)現(xiàn),那么就要調(diào)用ACTION_REQUEST_DISCOVERABLE操作意圖的startActivityForResult(Intent, int)方法。這個方法會向系統(tǒng)設(shè)置發(fā)出一個啟用可發(fā)現(xiàn)模式的請求。默認情況下,設(shè)備的可發(fā)現(xiàn)模式會持續(xù)120秒。通過給Intent對象添加EXTRA_DISCOVERABLE_DURATION附加字段,可以定義不同持續(xù)時間。應(yīng)用程序能夠設(shè)置的最大持續(xù)時間是3600秒,0意味著設(shè)備始終是可發(fā)現(xiàn)的。任何小于0或大于3600秒的值都會自動的被設(shè)為120秒。例如,以下代碼把持續(xù)時間設(shè)置為300秒:
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); startActivity(discoverableIntent);
申請用戶啟用設(shè)備的可發(fā)現(xiàn)模式時,會顯示一個對話框。如果響應(yīng)“Yes”,那么設(shè)備的可發(fā)現(xiàn)模式會持續(xù)指定的時間,而且你的Activity會接收帶有結(jié)果代碼等于可發(fā)現(xiàn)設(shè)備持續(xù)時間的onActivityResult()回調(diào)方法的調(diào)用。如果用戶響應(yīng)“No”或有錯誤發(fā)生,則結(jié)果代碼等于RESULT_CANCELED.
在可發(fā)現(xiàn)模式下,設(shè)備會靜靜的把這種模式保持到指定的時長。如果你想要在可發(fā)現(xiàn)模式被改變時獲得通知,那么你可以注冊一個ACTION_SCAN_MODE_CHANGED類型的Intent廣播。這個Intent對象中包含了EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE附加字段,它們會分別告訴你新舊掃描模式。它們每個可能的值是:SCAN_MODE_CONNECTABLE_DISCOVERABLE,SCAN_MODE_CONNECTABLE或SCAN_MODE_NONE,它們分別指明設(shè)備是在可發(fā)現(xiàn)模式下,還是在可發(fā)現(xiàn)模式下但依然可接收連接,或者是在可發(fā)現(xiàn)模式下并不能接收連接。
如果你要初始化跟遠程設(shè)備的連接,你不需要啟用設(shè)備的可現(xiàn)性。只有在你想要把你的應(yīng)用程序作為服務(wù)端來接收輸入連接時,才需要啟用可發(fā)現(xiàn)性,因為遠程設(shè)備在跟你的設(shè)備連接之前必須能夠發(fā)現(xiàn)它。
搜索設(shè)備并進行配對(即添加好友)
簡單的調(diào)用startDiscovery()方法就可以開始發(fā)現(xiàn)設(shè)備。該過程是異步的,并且該方法會立即返回一個布爾值來指明發(fā)現(xiàn)處理是否被成功的啟動。通常發(fā)現(xiàn)過程會查詢掃描大約12秒,接下來獲取掃描發(fā)現(xiàn)的每個設(shè)備的藍牙名稱。
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)){
//掃描到藍牙設(shè)備
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)) {
//掃描設(shè)備結(jié)束
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)){
//取得狀態(tài)改變的設(shè)備,更新設(shè)備列表信息(配對狀態(tài))
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;
}
}
}
配對信息示例圖:

配對過程示例圖:

連接設(shè)備(即好友建立通信通道)
當你想要連接兩個設(shè)備時,一個必須通過持有一個打開的BluetoothServerSocket對象來作為服務(wù)端。服務(wù)套接字的用途是監(jiān)聽輸入的連接請求,并且在一個連接請求被接收時,提供一個BluetoothSocket連接對象。在從BluetoothServerSocket對象中獲取BluetoothSocket時,BluetoothServerSocket能夠(并且也應(yīng)該)被廢棄,除非你想要接收更多的連接。
以下是建立服務(wù)套接字和接收一個連接的基本過程:
1、調(diào)用listenUsingRfcommWithServiceRecord(String, UUID)方法來獲得一個BluetoothServerSocket對象。該方法中的String參數(shù)是一個可識別的你的服務(wù)端的名稱,系統(tǒng)會自動的把它寫入設(shè)備上的Service Discovery Protocol(SDP)數(shù)據(jù)庫實體(該名稱是任意的,并且可以簡單的使用你的應(yīng)用程序的名稱)。UUID參數(shù)也會被包含在SDP實體中,并且是跟客戶端設(shè)備連接的基本協(xié)議。也就是說,當客戶端嘗試跟服務(wù)端連接時,它會攜帶一個它想要連接的服務(wù)端能夠唯一識別的UUID。只有在這些UUID完全匹配的情況下,連接才可能被接收。
2、通過調(diào)用accept()方法,啟動連接請求。這是一個阻塞調(diào)用。只有在連接被接收或發(fā)生異常的情況下,該方法才返回。只有在發(fā)送連接請求的遠程設(shè)備所攜帶的UUID跟監(jiān)聽服務(wù)套接字所注冊的一個UUID匹配的時候,該連接才被接收。連接成功,accept()方法會返回一個被連接的BluetoothSocket對象。
3、除非你想要接收其他連接,否則要調(diào)用close()方法。該方法會釋放服務(wù)套接字以及它所占用的所有資源,但不會關(guān)閉被連接的已經(jīng)有accept()方法所返回的BluetoothSocket對象。跟TCP/IP不一樣,每個RFCOMM通道一次只允許連接一個客戶端,因此在大多數(shù)情況下,在接收到一個連接套接字之后,立即調(diào)用BluetoothServerSocket對象的close()方法是有道理的。
以下是以上過程實現(xiàn)的監(jiān)聽線程:
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);
}
}
}
以下是一個基本的連接過程:
1、通過調(diào)用BluetoothDevice的createRfcommSocketToServiceRecord(UUID)方法,獲得一個BluetoothSocket對象。這個方法會初始化一個連接到BluetoothDevice對象的BluetoothSocket對象。傳遞給這個方法的UUID參數(shù)必須與服務(wù)端設(shè)備打開BluetoothServerSocket對象時所使用的UUID相匹配。在你的應(yīng)用程序中簡單的使用硬編碼進行比對,如果匹配,服務(wù)端和客戶端代碼就可以應(yīng)用這個BluetoothSocket對象了。
2、通過調(diào)用connect()方法來初始化連接。在這個調(diào)用中,為了找到匹配的UUID,系統(tǒng)會在遠程的設(shè)備上執(zhí)行一個SDP查詢。如果查詢成功,并且遠程設(shè)備接收了該連接請求,那么它會在連接期間共享使用RFCOMM通道,并且connect()方法會返回。這個方法是一個阻塞調(diào)用。如果因為某些原因,連接失敗或連接超時(大約在12秒之后),就會拋出一個異常。
以下是實現(xiàn)以上過程的連接線程:
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);
}
}
}
在建立連接之前要調(diào)用cancelDiscovery()方法。在連接之前應(yīng)該始終調(diào)用這個方法,并且不用實際的檢查藍牙發(fā)現(xiàn)處理是否正在運行也是安全的(如果想要檢查,調(diào)用isDiscovering()方法)。
管理連接(即好友間通信)
當你成功的連接了兩個(或更多)設(shè)備時,每一個設(shè)備都有一個被連接的BluetoothSocket對象。這是良好的開始,因為你能夠在設(shè)備之間共享數(shù)據(jù)。使用BluetoothSocket對象來傳輸任意數(shù)據(jù)的過程是簡單的:
1、分別通過getInputStream()和getOutputStream()方法來獲得通過套接字來處理傳輸任務(wù)的InputStream和OutputStream對象;
2、用read(byte[])和write(byte[])方法來讀寫流中的數(shù)據(jù)。
以下為實現(xiàn)以上過程的通信線程:
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);
}
}
}
發(fā)送消息示例圖:

發(fā)送表情示例圖:
發(fā)送文件示例圖:

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
解決Android應(yīng)用冷啟動時出現(xiàn)的白屏問題的方法
本篇文章主要介紹了解決Android應(yīng)用冷啟動時出現(xiàn)的白屏問題的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08
Android加載對話框同時異步執(zhí)行實現(xiàn)方法
Android中通過子線程連接網(wǎng)絡(luò)獲取資料,同時顯示加載進度對話框給用戶的操作2012-11-11

