前言
此外本文只涉及经典蓝牙(Classic Bluetooth)的点对点通信开发,并不涉及低功耗蓝牙(BLE)的开发。
开发流程
- 设置蓝牙
- 搜索附近的蓝牙设备
- 配对连接
- 通信
设置蓝牙
1.获取 BluetoothAdapter
:
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
2.开启蓝牙
- 处理6.0以下版本的蓝牙权限
1.在AndroidManifest
中添加权限:
<!-- 应用使用蓝牙的权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<!-- 扫描蓝牙设备或者操作蓝牙设置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2.开启蓝牙功能:
常量REQUEST_ENABLE_BT是本地定义的整型(需要大于0),当系统通过onActivityResult() 返回至你的应用程序时,将作为requestCode的参数。
如果成功开启了蓝牙,你的Activity将收到RESULT_OK作为resultCode。如果蓝牙不能成功开启(例如用户选择“取消”),则resultCode为RESULT_CANCELED
//1、获取BluetoothAdapter
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//2、判断是否支持蓝牙,并弹窗要求打开蓝牙
if(mBluetoothAdapter == null ||!mBluetoothAdapter.isEnabled()){
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent,REQUEST_ENABLE_BT);
}
3.对返回值进行处理
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_OK){
//已启用,进行下一步初始化工作
}else if(requestCode == REQUEST_ENABLE_BT && resultCode == RESULT_CANCELED){
//未启用,退出应用
Toast.makeText(MainActivity.this,"请启用蓝牙",Toast.LENGTH_SHORT).show();
finish();
}
}
- 处理6.0版本以上的蓝牙权限
1.在AndroidManifest
中添加一个模糊定位的权限:
<!--模糊定位权限,仅作用于6.0+-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
2.校验蓝牙权限:
if (Build.VERSION.SDK_INT >= 23) {
//校验是否已具有模糊定位权限
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 {
//具有权限
}
} else {
//系统不高于6.0执行下一步初始化
}
3.对返回值进行处理,类似于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) {
//同意权限
} else {
// 权限拒绝
}
}
}
搜索附近的蓝牙设备
1.查询已配对的设备并加入列表
//将配过对的设备加入list
Set<BluetoothDevice> paireDevices = mBluetoothAdapter.getBondedDevices();
if(paireDevices.size()>0){
for(BluetoothDevice device: paireDevices){
adapter.addData(device); //adapter为列表的适配器
}
}
2.发现设备
调用异步方法startDiscovery()
开始搜索蓝牙设备。
该进程为异步进程,并且该方法会立即返回一个布尔值,指示是否已成功启动发现操作。 发现进程通常包含约 12 秒钟的查询扫描,之后对每台发现的设备进行页面扫描,以检索其蓝牙名称。
当这个方法发现蓝牙设备时,将会广播ACTION_FOUND
的Intent
,搜索到的设备信息EXTRA_DEVICE
包含在此Intent
中,因此注册一个BroadcastReceiver
来处理广播。
//新建一个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 中注册
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//注册
registerReceiver(receiver,getIntentFilter());
}
不要忘记在onDestroy()
中进行反注销
unregisterReceiver(receiver);
执行设备搜索的操作是一项很繁重的任务,会消耗大量的资源。一旦你找到了一个设备并要进行连接,请务必确认是否停止搜索设备的操作。如果已经进行了连接,那么搜索操作将会显著地降低连接的速率,因此你应当在连接时停止搜索。可通过cancelDiscovery()方法停止搜索。
配对连接
要在两台设备之间创建连接,其中一台设备要作为服务器端,保持开放的BluetoothServerSocket
并在线程中调用 accept()
开始侦听连接请求,
而另一台设备必须利用扫描得到的服务端MAC发起连接请求。
注:如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框
- 服务器端:
- 通过调用
listenUsingRfcommWithServiceRecord(String, UUID)
获取BluetoothServerSocket
- 通过在
run()
中调用accept()
,开始监听连接请求。
由于accept()
为阻塞调用,所以需要一个专门的线程进行连接的操作。
//用于接收连接请求,并启动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;
// 在没有连接上的时候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:
// 准备通信
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
为标记当前状态的变量,规定为
STATE_NONE = 0; // 初始状态
STATE_LISTEN = 1; // 等待连接
STATE_CONNECTING = 2; // 正在连接
STATE_CONNECTED = 3; // 已经连接上设备
- 客户端:
- 利用扫描到的服务器端的MAC地址得到远程设备
BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(macAddress);
- 该远程设备调用方法
createRfcommSocketToServiceRecord(UUID)
建立安全连接。
注:
UUID
定义为00001101-0000-1000-8000-00805F9B34FB
,为手机蓝牙串口的统一UUID。
- 在
run()
中通过调用conenct
建立连接
由于connect()
为阻塞调用,因此该连接过程应始终在主 Activity 线程以外的线程中执行。
//用于蓝牙连接
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;
}
//调用外部类方法,启动用于通信线程connectedThread
connected(mmSocket);
}
public void cancel(){
try {
mmSocket.close();
setState(false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
注:connected(mmSocket)
是应用中的虚构方法,它将启动用于传输数据的线程。
//连接完成后启动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();
}
通信
在成功连接两台设备后,每台设备都会有一个已连接的 BluetoothSocket
。利用 BluetoothSocket
传输任意数据的一般过程非常简单:
- 获取
InputStream
和OutputStream
,二者分别通过套接字
以及getInputStream()
和getOutputStream()
来处理数据传输。 - 使用
read(byte[])
和write(byte[])
读取数据并写入到流式传输。
因为read(byte[]) 和 write(byte[])方法都是阻塞调用的,所以需要一个专门的线程进行读写的操作。
//蓝牙连接完成后进行输入输出
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");
//当连接状态为连接时,循环读取
while(mState == STATE_CONNECTED){
try {
// 从InputStream中读取
Scanner in = new Scanner(mmInStream,"UTF-8");
String str = in.nextLine();
Log.i(TAG,"read: "+str);
//利用handle传递数据,此时为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);
}
}
}
- 调用
mConnectedThread.write(byte[] buffer)
进行输出。 - 输入流则在
run()
中被循环读取,这里采用了Handler
处理数据传递。
Constants
的常量代表了对Message
不同的处理方式,在ConnectedThread
的run()
中,使用不同的Constants
值,调整输入流的处理方式。
例子中使用了
mHandler.obtainMessage(Constants.MESSAGE_TOAST);
代表把得到的数据以MESSAGE_TOAST的方式处理。
//利用Handler传递数据
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