时间:2022-03-03 09:47:54 | 栏目:Android代码 | 点击:次
前段时间学习了一些蓝牙开发的知识,记录一下Android中蓝牙的简单开发。下面是最重要的两个类。
BluetoothAdapter : 蓝牙适配器,通过getDefaultAdapter ()去获取一个实例,如果设备不支持蓝牙的话,返回的是一个null对象,通过它,可以打开、关闭蓝牙,扫描设备、向指定设备创建socket通道…
BluetoothDevice : 代表一个设备对象,可以通过它获取设备的名字、地址、类型等,也可以创建匹配,建立socket通道等等。
<uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限)
Android6以上版本,扫描其他蓝牙还需要位置权限
// Android 9 以下版本 <user-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> // Android 9 以上 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // 如果设备不支持蓝牙 if (mBluetoothAdapter == null){ return; } // 设备支持蓝牙功能,调用startActivityForResult去启动蓝牙 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); } } } });
通过广播去接收蓝牙状态的改变
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);
同样通过广播接收,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)) { // 从intent对象中获取蓝牙设备的信息 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();
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; } } } }
已经配对的设备会被存储起来,通过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()); } }
想要在两台设备之间创建连接,必须实现客户端和服务端机制,他们之间使用套接字机制进行连接,服务端开放服务器套接字,客户端通过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(); // connect // Make a connection to the BluetoothSocket try { // This is a blocking call and will only return on a // successful connection or an exception mmSocket.connect(); Log.d(TAG, "run: socket连接成功"); } catch (IOException e) { // Close the socket 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; // mmOutStream.write(new String("hello").getBytes()); } @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); // send the bytes to the ui Activity 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); // mHandler.obtainMessage(Constants.MESSAGE_WRITE,-1,-1,buffer).sendToTarget(); } 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 可以很容易的实现设备音视频控制。