概述
前段时间学习了一些蓝牙开发的知识,记录一下Android中蓝牙的简单开发。下面是最重要的两个类。 BluetoothAdapter : 蓝牙适配器,通过getDefaultAdapter ()去获取一个实例,如果设备不支持蓝牙的话,返回的是一个null对象,通过它,可以打开、关闭蓝牙,扫描设备、向指定设备创建socket通道… BluetoothDevice : 代表一个设备对象,可以通过它获取设备的名字、地址、类型等,也可以创建匹配,建立socket通道等等。
1、权限申请
<uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限)
Android6以上版本,扫描其他蓝牙还需要位置权限
<user-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
2、打开蓝牙
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null){
return;
}
if (!mBluetoothAdapter.isEnabled()){
startBlueTooth.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE));
}
打开蓝牙功能是通过startActivity去启动的,但是startActivity这个函数已经过期了,所以我使用官方推荐的Activity Result替代它,感兴趣的请看我写过的这篇文章。
ActivityResultLauncher<Intent> startBlueTooth = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
new ActivityResultCallback<ActivityResult>() {
@Override
public void onActivityResult(ActivityResult result) {
if (result==null){
Toast.makeText(BlueToothActivity.this, "open failed", Toast.LENGTH_SHORT).show();
}else {
if (result.getResultCode() == RESULT_CANCELED){
Toast.makeText(BlueToothActivity.this,"用户取消",Toast.LENGTH_SHORT);
}
}
}
});
3、接收蓝牙状态的改变
通过广播去接收蓝牙状态的改变
class BluetoothStateChangeReceiver extends BroadcastReceiver{
public int DEFAULT_VALUE_BLUETOOTH = 1000;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)){
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,DEFAULT_VALUE_BLUETOOTH);
switch(state){
case BluetoothAdapter.STATE_ON:
Log.d(TAG, "onReceive: open");
break;
case BluetoothAdapter.STATE_OFF:
Log.d(TAG, "onReceive: off");
break;
case BluetoothAdapter.STATE_TURNING_ON :
Log.d(TAG, "onReceive: 正在打开");
break;
case BluetoothAdapter.STATE_TURNING_OFF:
Log.d(TAG, "onReceive: 正在关闭");
break;
}
}
}
}
别忘了广播的注册和解注册
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
stateReceiver = new BluetoothStateChangeReceiver() ;
registerReceiver(stateReceiver,filter);
4、扫描其他的设备
同样通过广播接收,action是BluetoothDevice.ACTION_FOUND
class MyReceiver extends 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) {
blueNames.add(device.getName()+"\t"+device.getAddress());
}
blueAdpater.notifyDataSetChanged();
Log.d(TAG, "onReceive: " + device.getName());
}
}
}
动态注册广播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver,filter);
开启扫描
mBluetoothAdapter.startDiscovery();
5、蓝牙配对
public class BondReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())){
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
switch(device.getBondState()){
case BluetoothDevice.BOND_BONDED:
Log.d(TAG, "onReceive: 配对完成");
break;
case BluetoothDevice.BOND_BONDING:
Log.d(TAG, "onReceive: 正在配对");
break;
case BluetoothDevice.BOND_NONE:
Log.d(TAG, "onReceive: 取消配对");
break;
}
}
}
}
6、获取已经配对的设备
已经配对的设备会被存储起来,通过BluetoothAdpater直接获取即可
Set<BluetoothDevice> paireDevices = mBluetoothAdapter.getBondedDevices();
if (paireDevices.size()>0){
for (BluetoothDevice pairedDevice : pairedDevices) {
blueNames.add(pairedDevice.getName()+" "+pairedDevice.getAddress());
Log.d(TAG, "onClick: "+pairedDevice.getName());
}
}
7、连接设备
想要在两台设备之间创建连接,必须实现客户端和服务端机制,他们之间使用套接字机制进行连接,服务端开放服务器套接字,客户端通过MAC地址向服务端发起连接。客户端和服务端以不同的方式获得BluetoothSocket ,当客户端和服务端在同一个RFCOMM通道上分别拥有已连接的BluetoothSocket 时,将他们视为彼此已经连接,于是每台设备都获得输入和输出流式传输,并开始传输数据。
连接技术
一种实现技术是自动将每台设备准备为一个服务器,从而使每台设备开放一个服务套接字并侦听连接,在此情况下,任何一台设备都可以发起与另一台设备的连接并称为客户端。 服务器
设置服务器套接字并接受连接,步骤依次如下
1、调用listenUsingRfcommWithServiceRecord()获取一个BluetoothServerSocket , 该函数需要两个参数,第一个是服务器的名称,自己取一个即可,第二个是UUID,用来对信息做唯一性标识,我们可以从网上众多UUID生成器中随机的生成一个,然后使用UUID.fromString(String)初始化一个UUID。
2、通过accept()函数开始侦听连接请求
只有远程设备发送的连接请求中UUID与使用此套接字注册的UUID相匹配时服务器才会接受请求,accept函数会返回已连接的BluetoothSocket
3、连接成功后调用close() 关闭BluetoothSocket
private class AcceptThread extends Thread{
private final BluetoothServerSocket mmServerSocket;
private String mSocketType;
public AcceptThread(boolean secure){
BluetoothServerSocket tmp = null;
mSocketType = secure ? "secure" : "Insercure";
try{
if (secure){
tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,MY_UUID_SECURE);
}else{
tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME_INSECURE,MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG,"socket type"+ mSocketType + "listen() failed",e);
}
mmServerSocket = tmp;
}
@Override
public void run() {
Log.d(TAG, "Socket Type: " + mSocketType +
"BEGIN mAcceptThread" + this);
setName("AcceptThread"+ mSocketType);
BluetoothSocket socket = null;
Log.d(TAG, "run: 开始监听");
while (true){
try{
socket = mmServerSocket.accept();
Log.d("acceptThread", "run: 连接成功");
connected(socket,socket.getRemoteDevice(),mSocketType);
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
break;
}
}
Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType);
}
public void cancel() {
Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
}
}
}
上面的secure和Insecure只是使用了不同的UUID而已。
客户端 远程设备开启监听后,我们就发起向此设备的连接,首先必须先获得远程设备的BluetoothDevice对象,然后获取BluetoothSocket发起连接。
基本步骤如下
1、使用BluetoothDevice 通过调用createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket 。
2、通过connect发起连接
private class ConnectThread extends Thread{
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType;
public ConnectThread(BluetoothDevice device, boolean secure){
mmDevice = device;
BluetoothSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
try {
if (secure){
tmp = device.createRfcommSocketToServiceRecord(MY_UUID_SECURE);
}else {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID_INSECURE);
}
} catch (IOException e) {
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
}
mmSocket = tmp;
}
@Override
public void run() {
Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
setName("ConnectThred"+mSocketType);
bluetoothAdapter.cancelDiscovery();
try {
mmSocket.connect();
Log.d(TAG, "run: socket连接成功");
} catch (IOException e) {
Log.d(TAG, "run: 关闭socket");
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() " + mSocketType +
" socket during connection failure", e2);
}
return;
}
connected(mmSocket,mmDevice,mSocketType);
}
public void cancel(){
try{
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
}
}
}
发送数据 连接成功后,我们就可以通过socket发送数据了,客户端的Socket对象是BluetoothSocket , 服务端的socket是BluetoothServerSocket ,特别注意不要混淆了。使用getInputStream 和getOutputStream 分别获取通过套接字处理数据传输的InputStream 和OutputStream 。写数据比较简单,但是读数据就需要一个单独的线程一直监听才行。
private class ConnectedThread extends Thread{
private final BluetoothSocket mmSocket;
private InputStream mmInStream;
private OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket, String socketType) throws IOException {
Log.d(TAG, "create ConnectedThread: " + socketType);
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
try{
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
if (socket != null){
tmpOut.write(new String("hello").getBytes());
Log.d(TAG, "ConnectedThread: socket不是null");
}
} catch (IOException e) {
Log.e(TAG,"temp socket not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public void run() {
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
while (true){
try{
bytes = mmInStream.read(buffer);
String text = encodeByteToString(buffer,bytes);
Log.d(TAG, "run: 收到消息:"+ text);
chatItems.add(text);
mHandler.sendMessage(mHandler.obtainMessage());
} catch (IOException e) {
Log.d(TAG, "run: 没有收到消息");
e.printStackTrace();
break;
}
}
}
public String encodeByteToString(byte[] data,int length) {
byte[] temp = new byte[length];
for (int i = 0; i < length; i++) {
temp[i] = data[i];
}
try {
return new String(temp,"utf-8");
} catch (UnsupportedEncodingException e) {
return "";
}
}
public void write(byte[] buffer){
try{
mmOutStream.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
public void cancel(){
try{
mmSocket.close();
Log.d(TAG, "cancel: connectedThread");
} catch (IOException e) {
Log.e(TAG, "close() of connect socket failed", e);
}
}
}
上面的例子我主要是学习官网上的蓝牙聊天项目写的代码,大家也可以直接看官网项目。从上面的例子中可知,接受到的数据流都是一些二进制,要用到实际的项目中还需要进行一定的编码和转换。也就是自己编写一些协议,学过socket编程的同学一定都懂,其实蓝牙已经有很多的好用的协议了,就比如AVRCP(Audio Video Remote Control Profile),定义了蓝牙设备和audio/video控制功能通信的特点和过程, 结合MediaSession 可以很容易的实现设备音视频控制。
|