前言
此外本文只涉及經(jīng)典藍(lán)牙(Classic Bluetooth)的點(diǎn)對(duì)點(diǎn)通信開發(fā)豪娜,并不涉及低功耗藍(lán)牙(BLE)的開發(fā)房蝉。
開發(fā)流程
- 設(shè)置藍(lán)牙
- 搜索附近的藍(lán)牙設(shè)備
- 配對(duì)連接
- 通信
設(shè)置藍(lán)牙
1.獲取 BluetoothAdapter
:
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
2.開啟藍(lán)牙
- 處理6.0以下版本的藍(lán)牙權(quán)限
1.在AndroidManifest
中添加權(quán)限:
<!-- 應(yīng)用使用藍(lán)牙的權(quán)限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 掃描藍(lán)牙設(shè)備或者操作藍(lán)牙設(shè)置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2.開啟藍(lán)牙功能:
常量REQUEST_ENABLE_BT是本地定義的整型(需要大于0)矾端,當(dāng)系統(tǒng)通過onActivityResult() 返回至你的應(yīng)用程序時(shí),將作為requestCode的參數(shù)。
如果成功開啟了藍(lán)牙果善,你的Activity將收到RESULT_OK作為resultCode。如果藍(lán)牙不能成功開啟(例如用戶選擇“取消”)美侦,則resultCode為RESULT_CANCELED
//1产舞、獲取BluetoothAdapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//2、判斷是否支持藍(lán)牙菠剩,并彈窗要求打開藍(lán)牙
if(mBluetoothAdapter == null ||!mBluetoothAdapter.isEnabled()){
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);
}
3.對(duì)返回值進(jìn)行處理
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_OK){
//已啟用易猫,進(jìn)行下一步初始化工作
}else if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED){
//未啟用,退出應(yīng)用
Toast.makeText(MainActivity.this,"請(qǐng)啟用藍(lán)牙",Toast.LENGTH_SHORT).show();
finish();
}
}
- 處理6.0版本以上的藍(lán)牙權(quán)限
1.在AndroidManifest
中添加一個(gè)模糊定位的權(quán)限:
<!--模糊定位權(quán)限具壮,僅作用于6.0+-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
2.校驗(yàn)藍(lán)牙權(quán)限:
if (Build.VERSION.SDK_INT >= 23) {
//校驗(yàn)是否已具有模糊定位權(quán)限
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.ACCESS_COARSE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(context,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
REQUEST_ENABLE_BT
);
} else {
//具有權(quán)限
}
} else {
//系統(tǒng)不高于6.0執(zhí)行下一步初始化
}
3.對(duì)返回值進(jìn)行處理准颓,類似于startActivityForResult方法:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_ENABLE_BT) {
if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//同意權(quán)限
} else {
// 權(quán)限拒絕
}
}
}
搜索附近的藍(lán)牙設(shè)備
1.查詢已配對(duì)的設(shè)備并加入列表
//將配過對(duì)的設(shè)備加入list
Set<BluetoothDevice> paireDevices = mBluetoothAdapter.getBondedDevices();
if(paireDevices.size()>0){
for(BluetoothDevice device: paireDevices){
adapter.addData(device); //adapter為列表的適配器
}
}
2.發(fā)現(xiàn)設(shè)備
調(diào)用異步方法startDiscovery()
開始搜索藍(lán)牙設(shè)備。
該進(jìn)程為異步進(jìn)程棺妓,并且該方法會(huì)立即返回一個(gè)布爾值攘已,指示是否已成功啟動(dòng)發(fā)現(xiàn)操作。 發(fā)現(xiàn)進(jìn)程通常包含約 12 秒鐘的查詢掃描涧郊,之后對(duì)每臺(tái)發(fā)現(xiàn)的設(shè)備進(jìn)行頁(yè)面掃描贯被,以檢索其藍(lán)牙名稱。
當(dāng)這個(gè)方法發(fā)現(xiàn)藍(lán)牙設(shè)備時(shí)妆艘,將會(huì)廣播ACTION_FOUND
的Intent
彤灶,搜索到的設(shè)備信息EXTRA_DEVICE
包含在此Intent
中,因此注冊(cè)一個(gè)BroadcastReceiver
來處理廣播批旺。
//新建一個(gè)IntentFilter
private IntentFilter getIntentFilter(){
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
return intentFilter;
}
//新建BroadcastReceiver
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if(BluetoothDevice.ACTION_FOUND.equals(action)){
BluetoothDevice device=intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if(!list.contains(device)){//去重
adapter.addData(device);
}
}else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){
Toast.makeText(context,"掃描完畢",Toast.LENGTH_SHORT).show();
}
}
};
//在onCreate 中注冊(cè)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//注冊(cè)
registerReceiver(receiver,getIntentFilter());
}
不要忘記在onDestroy()
中進(jìn)行反注銷
unregisterReceiver(receiver);
執(zhí)行設(shè)備搜索的操作是一項(xiàng)很繁重的任務(wù)幌陕,會(huì)消耗大量的資源。一旦你找到了一個(gè)設(shè)備并要進(jìn)行連接汽煮,請(qǐng)務(wù)必確認(rèn)是否停止搜索設(shè)備的操作搏熄。如果已經(jīng)進(jìn)行了連接聪全,那么搜索操作將會(huì)顯著地降低連接的速率敢订,因此你應(yīng)當(dāng)在連接時(shí)停止搜索」┎螅可通過cancelDiscovery()方法停止搜索鞋囊。
配對(duì)連接
要在兩臺(tái)設(shè)備之間創(chuàng)建連接止后,其中一臺(tái)設(shè)備要作為服務(wù)器端,保持開放的BluetoothServerSocket
并在線程中調(diào)用 accept()
開始偵聽連接請(qǐng)求溜腐,
而另一臺(tái)設(shè)備必須利用掃描得到的服務(wù)端MAC發(fā)起連接請(qǐng)求译株。
注:如果兩臺(tái)設(shè)備之前尚未配對(duì),則在連接過程中挺益,Android 框架會(huì)自動(dòng)向用戶顯示配對(duì)請(qǐng)求通知或?qū)υ捒?/p>
- 服務(wù)器端:
- 通過調(diào)用
listenUsingRfcommWithServiceRecord(String, UUID)
獲取BluetoothServerSocket
- 通過在
run()
中調(diào)用accept()
歉糜,開始監(jiān)聽連接請(qǐng)求。
由于accept()
為阻塞調(diào)用望众,所以需要一個(gè)專門的線程進(jìn)行連接的操作匪补。
//用于接收連接請(qǐng)求,并啟動(dòng)ConnectedThread
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
BluetoothServerSocket tmp = null;
try {
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord("YourAPPName", MY_UUID);
} catch (IOException e) {
e.printStackTrace();
}
mmServerSocket = tmp;
}
public void run() {
Log.d(TAG, "BEGIN mAcceptThread" + this);
BluetoothSocket socket = null;
// 在沒有連接上的時(shí)候accept
while (mState!=3) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "accept() failed", e);
break;
}
if (socket != null) {
synchronized (MainActivity.this) {
switch (mState) {
case STATE_LISTEN:
case STATE_CONNECTING:
// 準(zhǔn)備通信
connected(socket);
break;
case STATE_NONE:
case STATE_CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close unwanted socket", e);
}
break;
}
}
}
}
Log.i(TAG, "END mAcceptThread");
}
public void cancel() {
Log.d(TAG, "Socket cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket close() of server failed", e);
}
}
}
mState
為標(biāo)記當(dāng)前狀態(tài)的變量伞辛,規(guī)定為
STATE_NONE = 0; // 初始狀態(tài)
STATE_LISTEN = 1; // 等待連接
STATE_CONNECTING = 2; // 正在連接
STATE_CONNECTED = 3; // 已經(jīng)連接上設(shè)備
- 客戶端:
- 利用掃描到的服務(wù)器端的MAC地址得到遠(yuǎn)程設(shè)備
BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(macAddress);
- 該遠(yuǎn)程設(shè)備調(diào)用方法
createRfcommSocketToServiceRecord(UUID)
建立安全連接。
注:
UUID
定義為00001101-0000-1000-8000-00805F9B34FB
叉袍,為手機(jī)藍(lán)牙串口的統(tǒng)一UUID始锚。
- 在
run()
中通過調(diào)用conenct
建立連接
由于connect()
為阻塞調(diào)用,因此該連接過程應(yīng)始終在主 Activity 線程以外的線程中執(zhí)行喳逛。
//用于藍(lán)牙連接
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
mmDevice = device;
BluetoothSocket tmp = null;
try {
//嘗試建立安全的連接
tmp = mmDevice.createRfcommSocketToServiceRecord(MY_UUID);
//嘗試建立不安全的連接
//tmp = mmDevice.createInsecureRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.i(TAG,"獲取 BluetoothSocket失敗");
e.printStackTrace();
}
mmSocket = tmp;
}
@Override
public void run() {
if(mBluetoothAdapter.isDiscovering()){
mBluetoothAdapter.cancelDiscovery();
}
try {
mmSocket.connect();
} catch (IOException e) {
Log.i(TAG,"socket連接失敗");
//利用Handler傳遞消息
Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(Constants.TOAST,"Socket連接失敗");
msg.setData(bundle);
mHandler.sendMessage(msg);
return;
}
synchronized (MainActivity.this){
mConnectThread = null;
}
//調(diào)用外部類方法,啟動(dòng)用于通信線程connectedThread
connected(mmSocket);
}
public void cancel(){
try {
mmSocket.close();
setState(false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注:connected(mmSocket)
是應(yīng)用中的虛構(gòu)方法棵里,它將啟動(dòng)用于傳輸數(shù)據(jù)的線程润文。
//連接完成后啟動(dòng)ConnectedThread
public synchronized void connected(BluetoothSocket socket){
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
setState(STATE_CONNECTED);
mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
}
通信
在成功連接兩臺(tái)設(shè)備后,每臺(tái)設(shè)備都會(huì)有一個(gè)已連接的 BluetoothSocket
殿怜。利用 BluetoothSocket
傳輸任意數(shù)據(jù)的一般過程非常簡(jiǎn)單:
- 獲取
InputStream
和OutputStream
典蝌,二者分別通過套接字
以及getInputStream()
和getOutputStream()
來處理數(shù)據(jù)傳輸。 - 使用
read(byte[])
和write(byte[])
讀取數(shù)據(jù)并寫入到流式傳輸头谜。
因?yàn)閞ead(byte[]) 和 write(byte[])方法都是阻塞調(diào)用的骏掀,所以需要一個(gè)專門的線程進(jìn)行讀寫的操作。
//藍(lán)牙連接完成后進(jìn)行輸入輸出
private class ConnectedThread extends Thread{
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
//當(dāng)連接狀態(tài)為連接時(shí)柱告,循環(huán)讀取
while(mState == STATE_CONNECTED){
try {
// 從InputStream中讀取
Scanner in = new Scanner(mmInStream,"UTF-8");
String str = in.nextLine();
Log.i(TAG,"read: "+str);
//利用handle傳遞數(shù)據(jù)截驮,此時(shí)為Toast模式
Message msg = mHandler.obtainMessage(Constants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString(Constants.TOAST,str);
msg.setData(bundle);
mHandler.sendMessage(msg);
} catch (Exception e) {
Log.e(TAG, "disconnected", e);
}
}
}
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "Exception during write", e);
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
- 調(diào)用
mConnectedThread.write(byte[] buffer)
進(jìn)行輸出。 - 輸入流則在
run()
中被循環(huán)讀取际度,這里采用了Handler
處理數(shù)據(jù)傳遞葵袭。
Constants
的常量代表了對(duì)Message
不同的處理方式,在ConnectedThread
的run()
中乖菱,使用不同的Constants
值坡锡,調(diào)整輸入流的處理方式。
例子中使用了
mHandler.obtainMessage(Constants.MESSAGE_TOAST);
代表把得到的數(shù)據(jù)以MESSAGE_TOAST的方式處理窒所。
//利用Handler傳遞數(shù)據(jù)
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case: Constants.SomeConfig:
// do something
break;
}
}
};
Demo的GitHub鏈接:https://github.com/YangLuYang/android-Demo-ClassicBluetooth