目录
一、前言
去年毕业进入公司以来,工作内容主要和蓝牙打交道,几个月的学习和实践让我这个Android蓝牙小白逐渐成长起来。但是,很多时候知识温故才能知新,每一次实践都会带来新的理解和体会。于是决定从今天开始,将这几个月以来的成长在博客中一一分享出来,给有需要的朋友作些参考,也欢迎大家提出指点和建议。
二、经典蓝牙的介绍
关于经典蓝牙的介绍,google官网上有详细的解释,此处贴上链接:https://developer.android.google.cn/guide/topics/connectivity/bluetooth
经典蓝牙的使用过程大致可分为以下几个步骤:
1、开启扫描,搜索周围蓝牙设备
2、扫描到设备后,与设备配对、建立连接
3、与设备成功连接后,实现数据通讯即收发数据
4、与设备通讯结束后,关闭与蓝牙的连接
三、经典蓝牙项目实战
1、在开始使用蓝牙之前,我们必须要声明两个权限,第一个是蓝牙权限,第二个是位置权限。
(1)蓝牙权限
<!-- 应用使用蓝牙的权限 --> <uses-permission android:name="android.permission.BLUETOOTH"/> <!--启动设备发现或操作蓝牙设置的权限--> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
(2)位置权限(注意:Android 6.0以上版本还需要动态申请位置权限!)
<!--位置权限--> <!--Android 10以上系统,需要ACCESS_FINE_LOCATION--> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <!--Android 9以及以下系统,需要ACCESS_FINE_LOCATION--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
2、创建发起蓝牙连接的线程ConnectThread.java 和 管理蓝牙连接、收发数据的线程ConnectedThread.java。
(1)ConnectThread.java,代码中已经有详细的注释。
package yc.bluetooth.androidbt; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.util.Log; import java.io.IOException; import java.util.UUID; /** * 发起蓝牙连接 */ public class ConnectThread extends Thread { private static final String TAG = "ConnectThread"; private final BluetoothAdapter mBluetoothAdapter; private BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; public ConnectThread(BluetoothAdapter bluetoothAdapter,BluetoothDevice bluetoothDevice,String uuid) { this.mBluetoothAdapter = bluetoothAdapter; this.mmDevice = bluetoothDevice; //使用一个临时变量,等会赋值给mmSocket //因为mmSocket是静态的 BluetoothSocket tmp = null ; if(mmSocket != null){ Log.e(TAG,"ConnectThread-->mmSocket != null先去释放"); try { mmSocket.close(); } catch (IOException e) { e.printStackTrace(); } } Log.d(TAG,"ConnectThread-->mmSocket != null已释放"); //1、获取BluetoothSocket try { //建立安全的蓝牙连接,会弹出配对框 tmp = mmDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid)); } catch (IOException e) { Log.e(TAG,"ConnectThread-->获取BluetoothSocket异常!" + e.getMessage()); } mmSocket = tmp; if(mmSocket != null){ Log.w(TAG,"ConnectThread-->已获取BluetoothSocket"); } } @Override public void run(){ //连接之前先取消发现设备,否则会大幅降低连接尝试的速度,并增加连接失败的可能性 if(mBluetoothAdapter == null){ Log.e(TAG,"ConnectThread:run-->mBluetoothAdapter == null"); return; } //取消发现设备 if(mBluetoothAdapter.isDiscovering()){ mBluetoothAdapter.cancelDiscovery(); } if(mmSocket == null){ Log.e(TAG,"ConnectThread:run-->mmSocket == null"); return; } //2、通过socket去连接设备 try { Log.d(TAG,"ConnectThread:run-->去连接..."); if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onStartConn(); //开始去连接回调 } mmSocket.connect(); //connect()为阻塞调用,连接失败或 connect() 方法超时(大约 12 秒之后),它将会引发异常 if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onConnSuccess(mmSocket); //连接成功回调 Log.w(TAG,"ConnectThread:run-->连接成功"); } } catch (IOException e) { Log.e(TAG,"ConnectThread:run-->连接异常!" + e.getMessage()); if(onBluetoothConnectListener != null){ onBluetoothConnectListener.onConnFailure("连接异常:" + e.getMessage()); } //释放 cancel(); } } /** * 释放 */ public void cancel() { try { if (mmSocket != null && mmSocket.isConnected()) { Log.d(TAG,"ConnectThread:cancel-->mmSocket.isConnected() = " + mmSocket.isConnected()); mmSocket.close(); mmSocket = null; return; } if (mmSocket != null) { mmSocket.close(); mmSocket = null; } Log.d(TAG,"ConnectThread:cancel-->关闭已连接的套接字释放资源"); } catch (IOException e) { Log.e(TAG,"ConnectThread:cancel-->关闭已连接的套接字释放资源异常!" + e.getMessage()); } } private OnBluetoothConnectListener onBluetoothConnectListener; public void setOnBluetoothConnectListener(OnBluetoothConnectListener onBluetoothConnectListener) { this.onBluetoothConnectListener = onBluetoothConnectListener; } //连接状态监听者 public interface OnBluetoothConnectListener{ void onStartConn(); //开始连接 void onConnSuccess(BluetoothSocket bluetoothSocket); //连接成功 void onConnFailure(String errorMsg); //连接失败 } }
(2)ConnectedThread.java,代码中已经有详细的注释。
package yc.bluetooth.androidbt; import android.bluetooth.BluetoothSocket; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; /** * 管理连接 * 1、发送数据 * 2、接收数据 */ public class ConnectedThread extends Thread{ private static final String TAG = "ConnectedThread"; private BluetoothSocket mmSocket; private InputStream mmInStream; private OutputStream mmOutStream; //是否是主动断开 private boolean isStop = false; //发起蓝牙连接的线程 private ConnectThread connectThread; public void terminalClose(ConnectThread connectThread){ isStop = true; this.connectThread = connectThread; } public ConnectedThread(BluetoothSocket socket){ mmSocket = socket; InputStream tmpIn = null; OutputStream tmpOut = null; //使用临时对象获取输入和输出流,因为成员流是静态类型 //1、获取 InputStream 和 OutputStream try { tmpIn = socket.getInputStream(); tmpOut = socket.getOutputStream(); } catch (IOException e) { Log.e(TAG,"ConnectedThread-->获取InputStream 和 OutputStream异常!"); } mmInStream = tmpIn; mmOutStream = tmpOut; if(mmInStream != null){ Log.d(TAG,"ConnectedThread-->已获取InputStream"); } if(mmOutStream != null){ Log.d(TAG,"ConnectedThread-->已获取OutputStream"); } } public void run(){ //最大缓存区 存放流 byte[] buffer = new byte[1024 * 2]; //buffer store for the stream //从流的read()方法中读取的字节数 int bytes = 0; //bytes returned from read() //持续监听输入流直到发生异常 while(!isStop){ try { if(mmInStream == null){ Log.e(TAG,"ConnectedThread:run-->输入流mmInStream == null"); break; } //先判断是否有数据,有数据再读取 if(mmInStream.available() != 0){ //2、接收数据 bytes = mmInStream.read(buffer); //从(mmInStream)输入流中(读取内容)读取的一定数量字节数,并将它们存储到缓冲区buffer数组中,bytes为实际读取的字节数 byte[] b = Arrays.copyOf(buffer,bytes); //存放实际读取的数据内容 Log.w(TAG,"ConnectedThread:run-->收到消息,长度" + b.length + "->" + bytes2HexString(b, b.length)); //有空格的16进制字符串 if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onReceiveDataSuccess(b); //成功收到消息 } } } catch (IOException e) { Log.e(TAG,"ConnectedThread:run-->接收消息异常!" + e.getMessage()); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onReceiveDataError("接收消息异常:" + e.getMessage()); //接收消息异常 } //关闭流和socket boolean isClose = cancel(); if(isClose){ Log.e(TAG,"ConnectedThread:run-->接收消息异常,成功断开连接!"); } break; } } //关闭流和socket boolean isClose = cancel(); if(isClose){ Log.d(TAG,"ConnectedThread:run-->接收消息结束,断开连接!"); } } //发送数据 public boolean write(byte[] bytes){ try { if(mmOutStream == null){ Log.e(TAG, "mmOutStream == null"); return false; } //发送数据 mmOutStream.write(bytes); Log.d(TAG, "写入成功:"+ bytes2HexString(bytes, bytes.length)); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onSendDataSuccess(bytes); //发送数据成功回调 } return true; } catch (IOException e) { Log.e(TAG, "写入失败:"+ bytes2HexString(bytes, bytes.length)); if(onSendReceiveDataListener != null){ onSendReceiveDataListener.onSendDataError(bytes,"写入失败"); //发送数据失败回调 } return false; } } /** * 释放 * @return true 断开成功 false 断开失败 */ public boolean cancel(){ try { if(mmInStream != null){ mmInStream.close(); //关闭输入流 } if(mmOutStream != null){ mmOutStream.close(); //关闭输出流 } if(mmSocket != null){ mmSocket.close(); //关闭socket } if(connectThread != null){ connectThread.cancel(); } connectThread = null; mmInStream = null; mmOutStream = null; mmSocket = null; Log.w(TAG,"ConnectedThread:cancel-->成功断开连接"); return true; } catch (IOException e) { // 任何一部分报错,都将强制关闭socket连接 mmInStream = null; mmOutStream = null; mmSocket = null; Log.e(TAG, "ConnectedThread:cancel-->断开连接异常!" + e.getMessage()); return false; } } /** * 字节数组-->16进制字符串 * @param b 字节数组 * @param length 字节数组长度 * @return 16进制字符串 有空格类似“0A D5 CD 8F BD E5 F8” */ public static String bytes2HexString(byte[] b, int length) { StringBuffer result = new StringBuffer(); String hex; for (int i = 0; i < length; i++) { hex = Integer.toHexString(b[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } result.append(hex.toUpperCase()).append(" "); } return result.toString(); } private OnSendReceiveDataListener onSendReceiveDataListener; public void setOnSendReceiveDataListener(OnSendReceiveDataListener onSendReceiveDataListener) { this.onSendReceiveDataListener = onSendReceiveDataListener; } //收发数据监听者 public interface OnSendReceiveDataListener{ void onSendDataSuccess(byte[] data); //发送数据结束 void onSendDataError(byte[] data, String errorMsg); //发送数据出错 void onReceiveDataSuccess(byte[] buffer); //接收到数据 void onReceiveDataError(String errorMsg); //接收数据出错 } }
3、使用蓝牙之前,首先要检查当前手机是否支持蓝牙。如果支持蓝牙,检查手机蓝牙是否已开启。如果没有开启,则需要先打开蓝牙。打开手机蓝牙,有两种方式,推荐使用第二种打开方式。
private void initBluetooth() { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if(bluetoothAdapter == null){ Toast.makeText(this, "当前手机设备不支持蓝牙", Toast.LENGTH_SHORT).show(); }else{ //手机设备支持蓝牙,判断蓝牙是否已开启 if(bluetoothAdapter.isEnabled()){ Toast.makeText(this, "手机蓝牙已开启", Toast.LENGTH_SHORT).show(); }else{ //蓝牙没有打开,去打开蓝牙。推荐使用第二种打开蓝牙方式 //第一种方式:直接打开手机蓝牙,没有任何提示 // bluetoothAdapter.enable(); //BLUETOOTH_ADMIN权限 //第二种方式:友好提示用户打开蓝牙 Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivity(enableBtIntent); } } }
4、确保手机蓝牙已打开,就可以开始搜索设备。搜索设备只需调用startDiscovery()方法,但搜索的结果是通过广播来获取的,所以,还需要定义广播来获取搜索到的设备。
(1)搜索设备
private void searchBtDevice() { if(bluetoothAdapter.isDiscovering()){ //当前正在搜索设备... return; } //开始搜索 bluetoothAdapter.startDiscovery(); }
(2)自定义广播接收器,接收搜索到的设备。
/** * 蓝牙广播接收器 */ private static class BtBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_STARTED)) { //开启搜索 if (onDeviceSearchListener != null) { onDeviceSearchListener.onDiscoveryStart(); //开启搜索回调 } } else if (TextUtils.equals(action, BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {//完成搜素 if (onDeviceSearchListener != null) { onDeviceSearchListener.onDiscoveryStop(); //完成搜素回调 } } else if (TextUtils.equals(action, BluetoothDevice.ACTION_FOUND)) { //3.0搜索到设备 //蓝牙设备 BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); //信号强度 int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); Log.d(TAG, "扫描到设备:" + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress()); if (onDeviceSearchListener != null) { onDeviceSearchListener.onDeviceFound(bluetoothDevice,rssi); //3.0搜素到设备回调 } } } /** * 蓝牙设备搜索监听者 * 1、开启搜索 * 2、完成搜索 * 3、搜索到设备 */ public interface OnDeviceSearchListener { void onDiscoveryStart(); //开启搜索 void onDiscoveryStop(); //完成搜索 void onDeviceFound(BluetoothDevice bluetoothDevice, int rssi); //搜索到设备 } private OnDeviceSearchListener onDeviceSearchListener; public void setOnDeviceSearchListener(OnDeviceSearchListener onDeviceSearchListener) { this.onDeviceSearchListener = onDeviceSearchListener; } }
(3)注册广播接收器
private void initBtBroadcast() { //注册广播接收 btBroadcastReceiver = new BtBroadcastReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED); //开始扫描 intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//扫描结束 intentFilter.addAction(BluetoothDevice.ACTION_FOUND);//搜索到设备 registerReceiver(btBroadcastReceiver,intentFilter); }
(4)注册过广播之后,要记得在onDestroy()中注销广播
@Override protected void onDestroy() { super.onDestroy(); //注销广播接收 unregisterReceiver(btBroadcastReceiver); }
5、搜索到目标设备之后,就可以与蓝牙设备建立连接。
(1)发起连接
/** * 开始连接设备 * @param bluetoothDevice 蓝牙设备 * @param uuid 发起连接的UUID * @param conOutTime 连接超时时间 */ public void startConnectDevice(final BluetoothDevice bluetoothDevice, String uuid, long conOutTime){ if(bluetoothDevice == null){ Log.e(TAG,"startConnectDevice-->bluetoothDevice == null"); return; } if(bluetoothAdapter == null){ Log.e(TAG,"startConnectDevice-->bluetooth3Adapter == null"); return; } //发起连接 connectThread = new ConnectThread(bluetoothAdapter,curBluetoothDevice,uuid); connectThread.setOnBluetoothConnectListener(new ConnectThread.OnBluetoothConnectListener() { @Override public void onStartConn() { Log.d(TAG,"startConnectDevice-->开始连接..." + bluetoothDevice.getName() + "-->" + bluetoothDevice.getAddress()); } @Override public void onConnSuccess(BluetoothSocket bluetoothSocket) { //移除连接超时 mHandler.removeCallbacks(connectOuttimeRunnable); Log.d(TAG,"startConnectDevice-->移除连接超时"); Log.w(TAG,"startConnectDevice-->连接成功"); Message message = new Message(); message.what = CONNECT_SUCCESS; mHandler.sendMessage(message); //标记当前连接状态为true curConnState = true; //管理连接,收发数据 managerConnectSendReceiveData(bluetoothSocket); } @Override public void onConnFailure(String errorMsg) { Log.e(TAG,"startConnectDevice-->" + errorMsg); Message message = new Message(); message.what = CONNECT_FAILURE; mHandler.sendMessage(message); //标记当前连接状态为false curConnState = false; //断开管理连接 clearConnectedThread(); } }); connectThread.start(); //设置连接超时时间 mHandler.postDelayed(connectOuttimeRunnable,conOutTime); } //连接超时 private Runnable connectOuttimeRunnable = new Runnable() { @Override public void run() { Log.e(TAG,"startConnectDevice-->连接超时" ); Message message = new Message(); message.what = CONNECT_FAILURE; mHandler.sendMessage(message); //标记当前连接状态为false curConnState = false; //断开管理连接 clearConnectedThread(); } };
(2)管理连接,数据收发
managerConnectSendReceiveData()方法中,connectedThread对象进行数据发送结果、接收结果监听。
/** * 管理已建立的连接,收发数据 * @param bluetoothSocket 已建立的连接 */ public void managerConnectSendReceiveData(BluetoothSocket bluetoothSocket){ //管理已有连接 connectedThread = new ConnectedThread(bluetoothSocket); connectedThread.start(); connectedThread.setOnSendReceiveDataListener(new ConnectedThread.OnSendReceiveDataListener() { @Override public void onSendDataSuccess(byte[] data) { Log.w(TAG,"发送数据成功,长度" + data.length + "->" + bytes2HexString(data,data.length)); Message message = new Message(); message.what = SEND_SUCCESS; message.obj = "发送数据成功,长度" + data.length + "->" + bytes2HexString(data,data.length); mHandler.sendMessage(message); } @Override public void onSendDataError(byte[] data,String errorMsg) { Log.e(TAG,"发送数据出错,长度" + data.length + "->" + bytes2HexString(data,data.length)); Message message = new Message(); message.what = SEND_FAILURE; message.obj = "发送数据出错,长度" + data.length + "->" + bytes2HexString(data,data.length); mHandler.sendMessage(message); } @Override public void onReceiveDataSuccess(byte[] buffer) { Log.w(TAG,"成功接收数据,长度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length)); Message message = new Message(); message.what = RECEIVE_SUCCESS; message.obj = "成功接收数据,长度" + buffer.length + "->" + bytes2HexString(buffer,buffer.length); mHandler.sendMessage(message); } @Override public void onReceiveDataError(String errorMsg) { Log.e(TAG,"接收数据出错:" + errorMsg); Message message = new Message(); message.what = RECEIVE_FAILURE; message.obj = "接收数据出错:" + errorMsg; mHandler.sendMessage(message); } }); }
sendData()方法中,connectedThread对象发送数据。
/** * 发送数据 * @param data 要发送的数据 字符串 * @param isHex 是否是16进制字符串 * @return true 发送成功 false 发送失败 */ public boolean sendData(String data,boolean isHex){ if(connectedThread == null){ Log.e(TAG,"sendData:string -->connectedThread == null"); return false; } if(data == null || data.length() == 0){ Log.e(TAG,"sendData:string-->要发送的数据为空"); return false; } if(isHex){ //是16进制字符串 data.replace(" ",""); //取消空格 //检查16进制数据是否合法 if(data.length() % 2 != 0){ //不合法,最后一位自动填充0 String lasts = "0" + data.charAt(data.length() - 1); data = data.substring(0,data.length() - 2) + lasts; } Log.d(TAG,"sendData:string -->准备写入:" + data); //加空格显示 return connectedThread.write(hexString2Bytes(data)); } //普通字符串 Log.d(TAG,"sendData:string -->准备写入:" + data); return connectedThread.write(data.getBytes()); }
6、与蓝牙设备通讯结束之后,可与蓝牙设备断开连接。
/** * 断开已有的连接 */ public void clearConnectedThread(){ Log.d(TAG,"clearConnectedThread-->即将断开"); //connectedThread断开已有连接 if(connectedThread == null){ Log.e(TAG,"clearConnectedThread-->connectedThread == null"); return; } connectedThread.terminalClose(connectThread); //等待线程运行完后再断开 mHandler.postDelayed(new Runnable() { @Override public void run() { connectedThread.cancel(); //释放连接 connectedThread = null; } },10); Log.w(TAG,"clearConnectedThread-->成功断开连接"); Message message = new Message(); message.what = DISCONNECT_SUCCESS; mHandler.sendMessage(message); }
7、项目演示
(1)扫描到设备,点击“连接”按钮, 会在“搜索”按钮下方显示连接结果 。注意经典蓝牙连接是,第一次连接时会有弹出一个配对框,这个具体配对方式是蓝牙设备开发人员设置的。
(2)手机给蓝牙设备(设备名为:BTyqy)发送数据成功之后,蓝牙设备把接收到的数据再回发送给手机。
(3)断开连接。点击“断开”按钮, 会在“搜索”按钮下方显示断开结果 。
四、Demo案例源码地址:
注意:源码中没有进行位置权限的静态声明以及动态申请,小伙伴们使用时需要自己添加,谢谢!
CSDN:https://download.csdn.net/download/qq_38950819/11615060