关键类和接口
android.bluetooth 软件包中提供了所有 Bluetooth API。以下是创建蓝牙连接所需的类和接口: BluetoothAdapter 表示本地蓝牙适配器(蓝牙无线装置)。BluetoothAdapter 是所有蓝牙交互的入口点。借助此功能,您可以发现其他蓝牙设备,查询绑定(配对)设备的列表,使用已知的 MAC 地址实例化 BluetoothDevice,以及创建 BluetoothServerSocket 以监听来自其他设备的通信。 BluetoothDevice 表示远程蓝牙设备。可使用此方法通过 BluetoothSocket 请求与远程设备建立连接,或查询设备的相关信息,例如设备的名称、地址、类和绑定状态。 BluetoothSocket表示蓝牙套接字的接口(类似于 TCP Socket)。这是允许应用使用 InputStream 和 OutputStream 与其他蓝牙设备交换数据的连接点。 BluetoothServerSocket表示用于监听传入请求的开放服务器套接字(类似于 TCP ServerSocket)。为了连接两台设备,其中一台设备必须使用此类打开一个服务器套接字。当远程蓝牙设备向此设备发出连接请求时,该设备会接受连接,然后返回已连接的 BluetoothSocket。 BluetoothClass描述蓝牙设备的一般特征和功能。 这是一组只读属性,用于定义设备的类和服务。虽然这些信息提供了有关设备类型的实用提示,但该类的属性不一定要描述设备支持的所有蓝牙配置文件和服务。 BluetoothProfile表示蓝牙配置文件的接口。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。免触摸配置文件便是一个示例。如需详细了解配置文件,请参阅蓝牙配置文件。 BluetoothHeadset支持可与手机搭配使用的蓝牙耳机。这包括蓝牙耳机配置文件和免提 (v1.5) 配置文件。 BluetoothA2dp定义如何使用高级音频分发配置文件 (A2DP) 通过蓝牙连接将高质量音频从一台设备流式传输到另一台设备。 BluetoothHealth表示用于控制蓝牙服务的健康设备配置文件代理。 BluetoothHealthCallback用于实现 BluetoothHealth 回调的抽象类。您必须扩展此类并实现回调方法,以接收关于应用注册状态和蓝牙通道状态变化的更新。 BluetoothHealthAppConfiguration表示“蓝牙健康”第三方应用注册的应用配置,该配置是用来与远程蓝牙健康设备通信的。 BluetoothProfile.ServiceListener一个接口,当 BluetoothProfile 进程间通信 (IPC) 客户端连接到运行特定配置文件的内部服务或已断开连接时,用于通知这些客户端。
经典传统蓝牙
设置蓝牙
第一步是向清单文件添加蓝牙权限。
"android.permission.BLUETOOTH"
"android.permission.BLUETOOTH_ADMIN"
"android.permission.ACCESS_COARSE_LOCATION"
"android.bluetooth.adapter.action.STATE_CHANGED"
获取 BluetoothAdapter
BluetoothManager bluetoothManager = getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
}
启用蓝牙
if (!bluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
监听蓝牙状态 监听 ACTION_STATE_CHANGED 广播 intent,每当蓝牙状态发生变化时,系统都会广播此 intent。此广播包含 extra 字段 EXTRA_STATE 和 EXTRA_PREVIOUS_STATE,分别包含新旧蓝牙状态。这些额外字段的可能值包括
@IntDef({STATE_OFF, STATE_TURNING_ON, STATE_ON, STATE_TURNING_OFF, STATE_BLE_TURNING_ON,
STATE_BLE_ON, STATE_BLE_TURNING_OFF})
扫描发现蓝牙
1.startLeScan
BluetoothAdapter. startLeScan(LeScanCallback callback);
startLeScan()方法的特点:callback中在onLeScan() 中不能做耗时操作,特别是周围的BLE设备多的时候,容易导致底层堵塞。
2.自定义BroadcastReceiver中监听 BluetoothDevice.ACTION_FOUND、BluetoothDevice.ACTION_DISCOVERY_FINISHED 再执行,BluetoothAdapter startDiscovery(),startDiscovery是个异步调用,不会立即返回。如果不调用cancelDiscovery主动停止扫描的话,最多扫描12s。 另外要注意startDiscovery返回的设备不包括已配对设备,如要获取已配对设备,需要额外调用getBondedDevices。
【注册通知】
mBluetoothStatusReceiver = new BluetoothStatusReceiver();
registerReceiver(mBluetoothStatusReceiver, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
【通知收取扫描蓝牙数据】
private final BroadcastReceiver mReceiver = 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 (device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + " 被发现 \n" +
device.getAddress());
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED
.equals(action)) {
progressBar.setVisibility(View.GONE);
Log.d(TAG, "[ACTION_DISCOVERY_FINISHED] 蓝牙搜索完毕!");
// Toast.makeText(DeviceList.this, "搜索完毕", Toast.LENGTH_SHORT).show();
if (mNewDevicesArrayAdapter.getCount() == 0) {
String noDevices = getResources().getText(
R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
};
3.获取已配对蓝牙列表
Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
mPairedDevicesArrayAdapter.add(device.getName() + " 已配对 \n"
+ device.getAddress()+
"\nUUID : "+device.getUuids());
}
} else {
String noDevices = getResources().getText(R.string.none_paired)
.toString();
mPairedDevicesArrayAdapter.add(noDevices);
}
连接蓝牙设备
如需在两台设备之间创建连接,您必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的 MAC 地址发起连接。服务器设备和客户端设备分别以不同的方式获取所需的 BluetoothSocket。接受传入连接时,服务器会收到套接字信息。客户端会在打开到服务器的 RFCOMM 通道时提供套接字信息。
当服务器和客户端在同一 RFCOMM 通道上都具有已连接的 BluetoothSocket 时,就会将两者视为彼此连接。此时,每台设备都可以获得输入和输出流,并且可以开始传输数据。
RFCOMM 通道
RFCOMM(Radio Frequency Communications)是蓝牙协议栈中用于串行数据传输的协议。它建立在L2CAP(Logical Link Control and Adaptation Protocol)上,并提供一种可靠的序列化数据流通道,类似于传统的串行通信。 RFCOMM通过虚拟串口的方式,允许应用程序之间进行数据交换。它使用一个唯一的通道标识符(Channel Identifier)来标识每个RFCOMM通道,范围从1到30。每个通道都可以与一个远程设备上的服务关联,如串口服务、文件传输服务等。 建立RFCOMM连接:
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(uuid);
socket.connect();
uuid是你要连接的服务的UUID,需要与远程设备上的服务UUID匹配。
客户端连接服务端
在线程中执行 连接线程,专门用来对外发出连接对方蓝牙的请求并进行处理 构造函数里通过BluetoothDevice.createRfcommSocketToServiceRecord(), 从待连接的device产生BluetoothSocket,然后在run方法中connect 成功后调用 BluetoothChatService的connnected()方法,定义cancel()在关闭线程时能关闭socket
/*****************************链接线程 BluetoothSocket*********************************************/
// 连接线程,专门用来对外发出连接对方蓝牙的请求并进行处理
// 构造函数里通过BluetoothDevice.createRfcommSocketToServiceRecord(),
// 从待连接的device产生BluetoothSocket,然后在run方法中connect
// 成功后调用 BluetoothChatService的connnected()方法,定义cancel()在关闭线程时能关闭socket
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
mmDevice = device;
BluetoothSocket tmp = null;
// Get a BluetoothSocket to connect with the given BluetoothDevice
try {
// MY_UUID is the app's UUID string, also used by the server code
Log.d(TAG, "[ConnectThread] MY_UUID:" + MY_UUID);
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "[ConnectThread] createRfcommSocketToServiceRecord异常:" + e);
}
mmSocket = tmp;
}
public void run() {
// Cancel discovery because it will slow down the connection
mAdapter.cancelDiscovery();
try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.connect();
Log.d(TAG, "[run()]mmSocket.connect 正常进行!!: ");
} catch (IOException e) {
e.printStackTrace();
Log.e(TAG, "[run()]mmSocket.connect 失败22: " + e);
connectionFailed();
// Unable to connect; close the socket and get out
try {
mmSocket.close();
Log.e(TAG, "[run()]mmSocket.close 链接被关闭: ");
} catch (IOException e2) {
e2.printStackTrace();
Log.e(TAG, "Could not close the client socket: " + e2.getMessage());
}
//ChatService.this.start();
return;
}
synchronized (ChatService.this) {
mConnectedThread = null;
}
connected(mmSocket, mmDevice);
}
public void cancel() {
try{
mmSocket.close();
}catch (IOException e){}
}
}
客户端读写数据
客户端与服务端蓝牙连接后一直运行的线程。构造函数中设置输入输出流。 Run方法中使用阻塞模式的InputStream.read()循环读取输入流 然后psot到UI线程中更新聊天信息。也提供了write()将聊天消息写入输出流传输至对方, 传输成功后回写入UI线程。最后cancel()关闭连接的socket
/****************************** 数据传输 ****************************************/
// 双方蓝牙连接后一直运行的线程。构造函数中设置输入输出流。
// Run方法中使用阻塞模式的InputStream.read()循环读取输入流
// 然后psot到UI线程中更新聊天信息。也提供了write()将聊天消息写入输出流传输至对方,
// 传输成功后回写入UI线程。最后cancel()关闭连接的socket
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 = mmSocket.getInputStream();
tmpOut = mmSocket.getOutputStream();
} catch (IOException e) {
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024];
int bytes;
while (true) {
try {
bytes = mmInStream.read(buffer);
mHandler.obtainMessage(MainActivity.MESSAGE_READ, bytes, -1, buffer).sendToTarget();
} catch (IOException e) {
connectionLost();
break;
}
}
}
public void write(byte[] buffer) {
try {
mmOutStream.write(buffer);
} catch (IOException e) {
Log.d("MainActivity", "Send Fail");
}
mHandler.obtainMessage(MainActivity.MESSAGE_WRITE, buffer).sendToTarget();
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
}
}
}
Bluetooth作为服务端
当您想要连接两台设备时,其中一台设备必须保持打开状态的 BluetoothServerSocket 来充当服务器。服务器套接字的用途是监听传入的连接请求,并在请求被接受后提供已连接的 BluetoothSocket。
如需设置服务器套接字并接受连接,请完成以下步骤序列:
1.通过调用 listenUsingRfcommWithServiceRecord(String, UUID) 获取 BluetoothServerSocket。
该字符串是您的服务的可识别名称,系统会自动将其写入设备上的新服务发现协议 (SDP) 数据库条目。该名称可以任意设置,可以直接使用应用名称。 通用唯一标识符 (UUID) 也包含在 SDP 条目中,并且构成了与客户端设备连接协议的基础。也就是说,当客户端尝试与此设备连接时,它会携带 UUID,该 UUID 可唯一标识其想要连接的服务。这两个 UUID 必须匹配,系统才会接受连接。
UUID 是用于唯一标识信息的字符串 ID 的 128 位标准化格式。UUID 用于标识在系统或网络中需要具有唯一性的信息,因为 UUID 重复的概率实际上为零。它可以独立生成,无需使用集中式授权机构。在这种情况下,它用于唯一标识应用的蓝牙服务。如需获取用于您的应用的 UUID,您可以使用网络上的众多随机 UUID 生成器之一,然后使用 fromString(String) 初始化该 UUID。
2.通过调用 accept() 开始监听连接请求。
这是阻塞调用。它会在连接被接受或发生异常时返回。仅当远程设备发送的连接请求中包含的 UUID 与使用此监听服务器套接字注册的 UUID 相匹配时,系统才会接受连接。如果操作成功,accept() 会返回一个已连接的 BluetoothSocket。
3.如果不想接受其他连接,请调用BluetoothServerSocket. close()。
close()方法调用会释放服务器套接字及其所有资源,但不会关闭 accept() 返回的已连接 BluetoothSocket。与 TCP/IP 不同,RFCOMM 一次只允许每个通道有一个已连接的客户端,因此在大多数情况下,在接受已连接的套接字后立即在 BluetoothServerSocket 上调用 close() 是最合理的。
由于 BluetoothServerSocket.accept() 调用是一个阻塞调用,因此请勿在主 activity 界面线程中执行该调用。在其他线程中执行该操作可确保您的应用仍然可以响应其他用户互动。 通常而言,可以在应用管理的这个新线程中执行涉及 BluetoothServerSocket 或 BluetoothSocket 的所有工操作作。如需取消被阻塞的调用(例如 accept()),请从另一个线程对 BluetoothServerSocket 或 BluetoothSocket 调用 close()。请注意,BluetoothServerSocket 或 BluetoothSocket 上的所有方法都是线程安全的。 示例代码
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
//使用稍后分配给mmServerSocket的临时对象
//因为mmServerSocket是最终的。
BluetoothServerSocket tmp = null;
try {
//MY_UUID是应用程序的UUID字符串,也由客户端代码使用。
tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
Log.d(TAG, "listenUsingRfcommWithServiceRecord");
} catch (IOException e) {
Log.e(TAG, "Socket's listen() method failed", e);
}
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
Log.d(TAG, "[run()] 线程运行中");
// Keep listening until exception occurs or a socket is returned.
while (true) {
try {
Log.d(TAG, "socket is accept()333333333");
socket = mmServerSocket.accept();
Log.d(TAG, "socket is accept()");
} catch (IOException e) {
Log.e(TAG, "Socket's accept() method failed", e);
break;
}
if (socket != null) {
//已接受连接。执行与关联的工作
//单独线程中的连接。
manageMyConnectedSocket(socket);
Log.d(TAG, "socket is 开始管理@@@@");
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket's close() method failed" + e.getMessage());
e.printStackTrace();
}
break;
}
}
}
// Closes the connect socket and causes the thread to finish.
public void cancelServerSocket() {
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close the connect socket", e);
}
}
void manageMyConnectedSocket(BluetoothSocket socket) {
Log.d(TAG, "[manageMyConnectedSocket] 连接开始ooooooooooo : " );
ConnectedThread mConnectedThread = new ConnectedThread(socket);
mConnectedThread.start();
}
}
服务端蓝牙传输蓝牙数据
成功连接到蓝牙设备后,每台设备都会有一个已连接的 BluetoothSocket。使用 BluetoothSocket 传输数据的一般过程如下: 1.获取 InputStream 和 OutputStream,分别使用 getInputStream() 和 getOutputStream() 处理通过套接字进行的传输。 2.使用 read(byte[]) 和 write(byte[]) 读取和写入数据流。 使用专用线程从数据流读取数据以及向数据流写入数据。这一点很重要,因为 read(byte[]) 和 write(byte[]) 方法都会阻塞调用。read(byte[]) 方法会阻塞,直到从数据流中读取数据为止。write(byte[]) 方法通常不会阻塞,但如果远程设备调用 read(byte[]) 的速度不够快,导致中间缓冲区已满,则该方法可能会阻塞以执行流控制。因此,您应将线程中的主循环专门用于从 InputStream 读取数据。您可以在线程中使用单独的公共方法发起对 OutputStream 的写入。
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 = mmSocket.getInputStream();
tmpOut = mmSocket.getOutputStream();
} catch (IOException e) {
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024];
int bytes;
while (true) {
try {
bytes = mmInStream.read(buffer);
String readMessage = new String(buffer, 0, bytes);
Log.d(TAG, "[manageMyConnectedSocket]服务端收到信息 : " + readMessage);
mmOutStream.write("服务端收到你的来信".getBytes());
if(!mmSocket.isConnected()){
Log.d(TAG, "[ConnectedThread]socket断开连接!!!!");
cancel();
}
} catch (IOException e) {
// connectionLost();
break;
}
}
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
}
}
}
-
该线程会等待数据通过 InputStream 传入。当 read(byte[]) 返回数据流中的数据时,通过Handler 将数据发送到主 activity。然后,线程会等待从 InputStream 中读取更多字节。
-
发送传出数据,请从主 activity 调用线程的 write() 方法,并传入要发送的字节。此方法会调用 write(byte[]) 将数据发送到远程设备。如果在调用 write(byte[]) 时抛出 IOException,则说明设备无法将给定的字节发送到另一台(已连接的)设备。
-
线程的 cancel() 方法允许您随时通过关闭 BluetoothSocket 来终止连接。用完蓝牙连接后,请始终调用此方法。
蓝牙配置文件(扩展功能)
Bluetooth API 支持使用蓝牙配置文件。蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范,例如免触摸配置文件。如需让移动设备连接到无线耳机,两台设备都必须支持免触摸配置文件。
-
耳机。耳机配置文件支持蓝牙耳机,以便与手机配合使用。Android 提供了 BluetoothHeadset 类,它是用于控制蓝牙耳机服务的代理。这包括蓝牙耳机和免提 (v1.5) 配置文件。BluetoothHeadset 类包含对 AT 命令的支持
-
A2DP。高级音频分发配置文件 (A2DP) 配置文件定义了如何通过蓝牙连接将高质量音频从一个设备流式传输到另一个设备。Android 提供了 BluetoothA2dp 类,它是用于控制蓝牙 A2DP 服务的代理。
-
健康设备。Android 支持蓝牙健康设备配置文件 (HDP)。这样,您就可以创建使用蓝牙与支持蓝牙功能的健康设备(例如心率监测仪、血糖仪、温度计、体重秤等)进行通信的应用。如需查看受支持设备及其对应的设备数据专业化代码的列表,请参阅蓝牙的 HDP 设备数据专精领域。 这些值在 ISO/IEEE 11073-20601 [7] 规范的命名法附录中也以 MDC_DEV_SPEC_PROFILE_* 的形式引用。
图: 蓝牙配置文件示意图
使用配置文件的基本步骤:
- 获取默认适配器
- 设置 BluetoothProfile.ServiceListener。当 BluetoothProfile 客户端连接到服务或断开服务连接时,此监听器会通知这些客户端。
- 使用 getProfileProxy() 连接到与个人资料相关联的个人资料代理对象。在以下示例中,个人资料代理对象是 BluetoothHeadset 的实例。
- 在 onServiceConnected() 中,获取配置文件代理对象的句柄。
- 获得配置文件代理对象后,即可使用它来监控连接状态并执行与该配置文件相关的其他操作。
BluetoothHeadset bluetoothHeadset;
// Get the default adapter
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
private BluetoothProfile.ServiceListener profileListener = new BluetoothProfile.ServiceListener() {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = (BluetoothHeadset) proxy;
}
}
public void onServiceDisconnected(int profile) {
if (profile == BluetoothProfile.HEADSET) {
bluetoothHeadset = null;
}
}
};
// 建立与代理的连接。 Establish connection to the proxy.
bluetoothAdapter.getProfileProxy(context, profileListener, BluetoothProfile.HEADSET);
//...蓝牙耳机的通话功能
//使用后关闭代理连接。
bluetoothAdapter.closeProfileProxy(bluetoothHeadset);
供应商特定的 AT 命令
应用可以注册接收耳机发送的预定义供应商专用 AT 命令(例如 Plantronics +XEVENT 命令)的系统广播。例如,应用可以接收指示所连接设备电池电量的广播,并根据需要通知用户或执行其他操作。为 ACTION_VENDOR_SPECIFIC_HEADSET_EVENT intent 创建广播接收器,以处理耳机的供应商专用 AT 命令。
健康设备配置文件
Android 支持蓝牙健康设备配置文件 (HDP)。Bluetooth Health API 包含 < span style="color: blue; font-size: 16px;">BluetoothHealth < span style="color: blue; font-size: 16px;">BluetoothHealthCallback < span style="color: blue; font-size: 16px;">BluetoothHealthAppConfiguration, 注意 :系统不再使用健康设备配置文件 (HDP) 和 MCAP 协议,新应用应使用基于蓝牙低功耗。
Android 蓝牙-BLE
end