Android 蓝牙相关开发
描述
最近公司有个项目,App从后台获取到数据,App连接打印机,将数据在打印机上打印。公司提供的测试设备是蓝牙打印机,为了完成项目,决定学习一下Android的蓝牙通信机制,在此记录全部过程
基于文档
本文相关资料:
Android Bluetooth官方文档:https://developer.android.com/guide/topics/connectivity/bluetooth?hl=zh-cn#EnablingDiscoverability
Android 蓝牙知识
Android平台包含蓝牙网络堆栈的支持,设备能以无线方式与其他蓝牙设备交换数据。应用框架提供了通过Android Bluetooth API访问蓝牙功能的途径。
使用Bluetooth API,Android应用可执行以下操作:
- 扫描其他蓝夜设备
- 查询本地蓝牙适配器的配对蓝牙设备
- 建立RFCOMM通道
- 通过服务发现连接到其他设备
- 与其他设备进行双向数据传输
- 管理多个连接
基础知识
Android中的 android.bluetooth 包中提供了所有Bluetooth API
BluetoothAdapter
表示本地蓝牙适配器(蓝牙无线装置)。BluetoothAdapter是所有蓝夜交互的入口点。利用本地蓝牙适配器可以发现其他蓝牙设备,查询绑定(配对)设备的列表。使用已知的MAC地址实例化BluetoothDevice。以及创建BluetoothServerSocket 以侦听来自其他设备的通信
BluetoothDevice
表示远程蓝牙设备。利用它可以通过BluetoothSocket请求与某个远程设备建立连接,或查询有关该设备的信息,例如设备的名称、地址、类和绑定状态等
BluetoothSocket
表示蓝牙套接字接口,允许应用通过InputStream和OutputStream与其他蓝牙设备交换数据的连接点
BluetoothServerSocket
表示用于侦听传入请求的开放服务器套接字,要连接两台Android设备,其中一台设备必须使用此类开放一个服务套接字。当一台远程蓝牙设备向此设备发出连接请求是BluetoothServerSocket将会在接受连接后返回已经连接的BluetoothSocket
BluetoothClass
描述蓝牙设备的一般特征和功能。这是一组只读属性,用于定义设备的主要和次要设备类及其服务。不过它不能可靠的描述设备的所有蓝牙配置文件和服务,而是适合作为设备类型提示
BluetoothProfile
表示蓝牙配置文件的接口。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。
BluetoothHeadset
提供蓝牙耳机支持
BluetoothA2dp
定义高质量音频如何通过蓝牙连接和流式传输,从一台设备传输到另一台设备 “A2DP”代表高级音频分发配置文件
BluetoothHealth
表示用于控制蓝牙服务的健康设备配置文件袋里
BluetoothHealthCallback
用于实现BluetoothHealth回调的抽象类
BluetoothHealthAppConfiguration
表示第三方蓝牙健康应用注册的应用配置,以便于远程蓝牙健康设备通信
BluetoothProfile.ServiceListener
在BluetoothProfile IPC客户端连接到服务或断开服务连接时向其发送通知的接口
蓝牙权限
要在应用中使用蓝牙功能,必须声明蓝牙权限BLUETOOTH,需要权限才能执行任何蓝牙通信
如果希望应用启动设备发现或操作蓝牙设置,则还必须声明BLUETOOTH_ADMIN权限。如果要使用BLUETOOTH_ADMIN权限,则还必须拥有BLUETOOTH权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
设置蓝牙
在开发中使用蓝牙时,需要验证设备支持蓝牙,如果支持蓝牙还需要确保蓝牙启用。
如果设备不支持蓝牙,那还开发个毛线
设置蓝牙的步骤分为:
- 获取BluetoothAdapter
所有蓝牙Activity都需要BluetoothAdapter。要获取BluetoothAdapter 请调用静态的getDefaultAdapter()方法,这个方法将返回一个表示设备自身的蓝牙适配器(蓝牙无线装置)的BluetoothAdapter。整个系统有一个蓝牙适配器,并且应用可使用此对象与设备交互 如果getDefaultAdapter()返回null,则表示该设备不支持蓝牙。
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
// TODO: 设备不支持蓝牙
Log.d(AssistUtils.TAG, "device not support bluetooth");
}
- 启用蓝牙
需要确保启用蓝牙,调用isEnabled()
以检查当前是否已经启用蓝牙,如果此方法返回false,则表示蓝牙处于停用状态。在这个时候,需要我们使用intent来启用蓝牙,使用ACTION_REQUEST_ENABLE 操作Intent调用startActivityForResult()。将通过系统设置发出启用蓝牙的请求
//判断蓝牙是否可用
if (!bluetoothAdapter.isEnabled()) {
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent,REQUESTCODE_BLUETOOTH_ENABLECODE);
}
调用后,将显示如下图的对话框,请求用户允许启用蓝牙没如果用户响应yes,并在该进程完成后将焦点返回到您的应用
蓝牙用户授权对话框
传递给startActivityForResult()的REQUESTCODE_BLUETOOTH_ENABLECODE常量是在局部定义的整型。系统会将其作为requestCode参数传递会onActivityResult()
实现。
如果成功启用蓝牙,Activity将会在onActivityResult()回调中收到RESULT_OK结果代码。如果由于某个错误(用户响应no)而没有启用蓝牙,则结果代码为RESULT_CANCELED
除此外还可以选择侦听 [ACTION_STATE_CHANGED]
广播 Intent,每当蓝牙状态发生变化时,系统都会广播此 Intent。 此广播包含额外字段 [EXTRA_STATE]
和 [EXTRA_PREVIOUS_STATE]
,二者分别包含新的和旧的蓝牙状态。 这些额外字段可能的值包括 [STATE_TURNING_ON]
、[STATE_ON]
、[STATE_TURNING_OFF]
和 [STATE_OFF]
。侦听此广播适用于检测在您的应用运行期间对蓝牙状态所做的更改。
/**
* @author wxblack-mac
* @DESCRIBE:自定义蓝牙广播接收者
* @DATE 2018/11/28 11:04
* GOOD LUCK
*/
public class PrinterBlueToothReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int intExtra = intent.getIntExtra(EXTRA_STATE, -1);
Log.d(AssistUtils.TAG, "onReceive: intExtra" + intExtra);
if (intExtra == STATE_TURNING_ON) {
Log.d(AssistUtils.TAG, "onReceive: 蓝牙开启");
}
}
}
- 注册广播
private void registerPrintReceiver() {
printerBlueToothReceiver = new PrinterBlueToothReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(printerBlueToothReceiver, intentFilter);
}
查找设备
使用BluetoothAdapter 可以通过设备发现或通过查询配对设备的列表来查找远程的蓝牙设备
设备发现是一个扫描过程,它会搜索局部区域内已启动蓝牙功能的设备,然后请求一些关于各台设备的信息。但是局部区域内的蓝牙设备仅在其当前已启用可检测性是才会响应发现请求。如果设备可检测到,设备将通过共享一些信息(例如设备名称、类及其唯一MAC地址)来响应发现请求。利用此信息,执行发现的设备可以选择发起到设备的连接
在当首次与设备建立连接后,就会自动向用户显示配对请求。设备完成配对后,将会保存关于该设备的基本信息。并且可以使用上面提到过的Bluetooth API读取这些信息。利用远程设备的已知MAC地址可以随时向其
发起连接,而无需执行发现操作。
被配对与被连接之间是存在差别的,被配对意味着两台设备知晓彼此的存在,具有可用于身份验证的共享链路秘钥,并且能够与彼此简历加密连接。而被连接意味着设备当前共享一个RFCOMM通道,并且能够向彼此传输数据。当前的Android Bluetooth API要求对设备进行配对然后才能建立RFCOMM连接。
也就是说 使用本地蓝牙适配器可以扫描发现蓝夜设备,设备发现会扫描启用蓝牙功能的设备,然后可以请求启用了蓝牙发现设备的相关设备信息,如MAC地址、设备名称等。在首次建立设备连接后,就会向用户发起配对请求,完成配对后,会保存配对设备的相关信息。如果利用远程设备的 唯一的MAC地址时,并且当设备处于发现状态时,可以直接发起连接,而无需执行发现扫描的操作
查询配对设备
在执行设备发现以前,可以查询已经配对的设备集,以了解所需的设备是否处于已知状态。可以调用getBonderDevices()
。这将返回标识已配对设备的一组BluetoothDevice.
//获取已经配对的设备,返回一个集合
/**
* 查询配对设备集
*/
private void findPairDevices() {
Set<BluetoothDevice> bondedDevices = bluetoothAdapter.getBondedDevices();
if (bondedDevices.size() > 0) {
for (BluetoothDevice bluetoothDevice : bondedDevices) {
Log.d(TAG, "findPairDevices: " + bluetoothDevice.getName() + "\taddress:" + bluetoothDevice.getAddress());
}
}
}
发现设备
要开始发现设备,需要调用startDiscovery()
。该进程为异步进程,并且该方法会立即返回一个布尔值,表示是否已成功启动发现操作。启动的发现进程一般包含12秒的查询扫描,之后对每台发现的设备进行页面扫描。
App必须针对ACTION_FOUND
Intent注册一个BroadcastReceiver,以便接收每台发现的设备的相关信息。针对每台设备,系统将会广播ACTION_FOUNT 的Intent。这个返回的Intent将携带额外的字段 EXTRA_DEVICE
和EXTRA_CLASS
。分别包含BluetoothDevice
和 BluetoothClass
//获取发现的设备实体
findDeviceReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
//获取发现的设备实体
BluetoothDevice device = (BluetoothDevice) intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "发现的设备" + device.getAddress() + "\tname:" + device.getName());
}
}
};
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(findDeviceReceiver, filter);
执行设备发现对于蓝牙适配器来说是一个非常繁重的操作过程,并且会消耗大量的资源,找到要连接的设备后,确保使用cancelDiscovery()
停止发现,然后尝试连接,如果已经保持与某台设备的连接,执行发现操作可能会减少可用于该连接的带宽,不应该在处于连接状态时执行发现操作
启用可检测性
如果希望本地设备可以被其他设备检测到,请使用ACTION_REQUEST_DISCOVERABLE
操作Intent调用startActivityForResult(),浙江通过系统设置发出启用可检测到模式的请求。在默认情况下,设备将变为可检测到并持续120秒钟。可以通过添加EXTRA_DISCOVERABLE_DURATION
Intent Extra来定义不同的持续时间。应用可以设置的最大持续时间为3600秒,值为0则表示设备始终可检测到 任何小于0或者大于3600的值都会自动这只为120秒
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注册BroadcastReceiver。接收到的intent将包含额外字段EXTRA_SCAN_MODE
和EXTRA_PREVIOUS_SCAN_MODE
或SCAN_MODE_NONE
这些值分别指示设备处于可检测到模式、未处于可检测到模式但仍能接收连接,或未处于可检测到模式并且无法接收连接
如果您将要发起到远程设备的连接,则无需启用设备可检测性。仅当您希望您的应用托管将用户接受传入连接的服务器套接字时,才有必要启用可检测性。因为远程设备必须能够发现该设备,然后才能发起连接
连接设备
要在两台设备上的应用之间创建连接,必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须发起连接。当服务器和客户端在同一RFCOMM通道上分别拥有已连接的BluetoothSocket时,二者将被视为彼此连接。在这种情况下,每台设备都能获得输入和输出流式传输,并且可以开始传输数据。
服务器设备和客户端设备分别以不同的方法获得需要的BluetoothSocket。服务器将在传入连接被接受时收到套接字。
客户端将在其打开到服务器的RFCOMM通道时收到该套接字
一种实现技术是自动将每台设备准备为一个服务器,从而使每台设备开发一个服务器套接字并侦听连接,然后任一设备可以发起与另一台设备的连接,并成为客户端。或者其中一台设备可显式“托管”连接并按需开放一个服务器套接字,而另一台设备则直接发起连接
连接为服务器
当您需要连接两台设备时,其中一台设备必须通过保持开放的BluetoothServerSocket来充当服务器。服务器套接字的用途是侦听传入的连接请求,并在接受一个请求后提供已经连接的BluetoothSocket。从BluetoothServerSocket获取BluetoothSocket后,可以舍弃BluetoothServerSocket。除非需要更多的连接时可以不用舍弃
关于UUID
通用唯一标识符 UUID 是用于唯一标识信息的字符串ID的128位标准化格式。UUID的特点是其足够庞大,因此,可以选择任意随机值而不会发生冲突。可以被用于唯一标识应用的蓝牙服务
设置服务器套接字并接受连接的基本过程:
1.通过调用listenUsingRfcommWithServiceRecord(String,UUID)
获取BluetoothServerSocket
该字符串是服务的可识别名称,系统会自动将其写入到设备上的新服务发现协议(SDP)数据库条目(可使用任意名称)。UUID也包含在SDP条目中,并且将作为与客户端设备的连接协议的基础。当客户端尝试连接此设备时,它会携带能够唯一标识其想要连接的服务的UUID。两个UUID必须匹配,在下一步中,连接才会被接受
2.通过调用accept()
开始侦听连接请求
阻塞调用,将在连接被接受或发生异常时返回。仅当远程设备发送的连接请求中所包含的UUID与向此侦听服务器套接字注册的UUID相匹配时,连接才会被接受。操作成功后,accept()将会返回已连接的BluetoothSocket
3.除非想要接受更多连接,否则请调用close()
这将释放服务器套接字及其所有资源。但不会关闭accept()
所返回的已连接的BluetoothSocket
。与TCP/IP不同,RFCOMM一次只允许每个通道有一个已连接的客户端。
accept()
调用不应在主Activity UI线程中执行,因为是阻塞调用会造成ANR。
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
// Use a temporary object that is later assigned to mmServerSocket,
// because mmServerSocket is final
BluetoothServerSocket tmp = null;
try {
// MY_UUID is the app's UUID string, also used by the client code
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// Keep listening until exception occurs or a socket is returned
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// If a connection was accepted
if (socket != null) {
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
/** Will cancel the listening socket, and cause the thread to finish */
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}
连接为客户端
要发起与远程设备的连接,必须先获取标识该远程设备的BluetoothDevice
对象。然后,必须使用BluetoothDevice
来获取BluetoothSocket并发起连接
基本过程
- 使用
BluetoothDevice
,通过调用createRfcommScoketToServiceRecord(UUID)
获取BluetoothSocket
这将初始化将要连接到BluetoothDevice
的BluetoothSocket
。此处传递的UUID必须与服务器设备在使用listenUsingRfcommWithServiceRecord(String,UUID)开放其BluetoothServerSocket
所使用的UUID相匹配。 - 通过调用
connect()
发起连接
执行此调用时,系统将会在远程设备上执行SDP查找,以便匹配UUID。如果查找成功并且远程设备接受了该连接。它将共享RFCOMM通道以便在连接期间使用。并且connect()
将会返回。此方法为阻塞调用,应当在主线程UI线程之外的线程执行。
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
// Use a temporary object that is later assigned to mmSocket,
// because mmSocket is final
BluetoothSocket tmp = null;
mmDevice = device;
// Get a BluetoothSocket to connect with the given BluetoothDevice
try {
// MY_UUID is the app's UUID string, also used by the server code
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}
public void run() {
// Cancel discovery because it will slow down the connection
mBluetoothAdapter.cancelDiscovery();
try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.connect();
} catch (IOException connectException) {
// Unable to connect; close the socket and get out
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
}
/** Will cancel an in-progress connection, and close the socket */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
在建立连接之前会调用cancelDiscovery(),在进行连接之前应始终执行此调用,而且调用时无需实际检查其是否正在运行(isDiscovering()
进行检查)
管理连接
在成功连接两台(多台)设备之后,每台设备都会有一个已连接的BluetoothSocket
.使用BluetoothSocket
传输数据的方式为:
- 获取InputStream和OutputStream.二者分别通过套接字以及
getInputStream()
和getOutoputStream()
来处理传输数据
2.使用read(buye[])
和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;
// Get the input and output streams, using temp objects because
// member streams are final
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()
// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/* Call this from the main activity to send data to the remote device */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
/* Call this from the main activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
构造函数获得必要的流,一旦执行,线程将会等待数据从输入流中流出。当read(byte[])返回字节时,数据将通过父类的Handler被发送至Activity。之后再返回并等待更多的字节流。
发送数据则仅仅需要简单地调用线程的write()方法即可。
线程中的cancel()方法很重要,因为连接可以随时在任意时间通过BluetoothSocket终止。该方法在结束使用蓝牙连接后,应当总是被调用。