android 蓝牙开发 前言篇: https://www.jianshu.com/p/efff9eda0537
基础
由浅入深,这篇主要是介绍蓝牙的搜索、绑定(即配对)、数据传输等最基础的操作。
权限
下面这两个是蓝牙相关的权限,用于蓝牙的连接通讯和本地蓝牙设备的使用,一起写在Manifest中就好。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
其实蓝牙相关的权限还有个BLUETOOTH_PRIVILEGED,这个是系统级应用用到的权限,暂时不做了解。
另外在android6.0以后,所有需要访问硬件唯一标识符的地方都需要申请位置权限,也就是下面这两个权限:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
蓝牙权限是普通权限,不需要在代码中动态申请,但是申请位置的权限是属于危险权限,需要在代码中动态申请。由于和本篇主题关系不大,动态申请什么的这里就不详细写了,后续应该会附带demo代码,有需要的可以下载查看。
本地蓝牙准备工作
android提供的,蓝牙操作相关的类都在android.bluetooth包下面。常用的有BluetoothAdapter,BluetoothDevice,BluetoothServerSocket,BluetoothSocket,BluetoothManager等。
其中最关键,用到最多的是BluetoothAdapter:
BluetoothAdapter表示本地设备蓝牙适配器,用来执行基本的蓝牙任务,比如开启蓝牙,扫描,绑定等等。
由于其构造方法为包可见权限获取BluetoothAdapter的方式只有一个BluetoothAdapter.getDefaultAdapter()。而通过BlueToothManager.getAdapter()获取的BluetoothAdapter其实也是通过调用getDefaultAdapter()来实现的。
开启蓝牙:
//获取蓝牙适配器
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()) {
//开启方式一:直接开启,不推荐。但是我测试的小米手机使用该方法仍然会在底部弹出一个确认窗口,需要用户同意,应该是小米对系统进行了定制化
//boolean enable = mBluetoothAdapter.enable();
//开启方式二:通过调用系统弹出界面来开启蓝牙,在onActivityResult中返回的resultCode为-1表示开启成功,0表示开启失败
Intent startBt = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
context.startActivityForResult(startBt, BT_OPEN_REQUIRE_CODE);
}
搜索蓝牙设备:
直接调用mBluetoothAdapter.startDiscovery()即可开启搜索,mBluetoothAdapter.cancelDiscovery()用来停止搜索,扫描是heavyweight procedure高消耗资源的过程,如果不手动停止,通常扫描执行12秒,在扫描的同时尽量避免新设备的连接操作现有的连接也要限制其带宽、设置高延迟。搜素是异步的,通过广播来监听搜索到的设备:
IntentFilter filter = new IntentFilter();
//发现设备
filter.addAction(BluetoothDevice.ACTION_FOUND);
//设备绑定状态改变
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
//蓝牙设备状态改变
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
//搜素完成
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mBluetoothReceiver, filter);
//开始搜索
mBluetoothAdapter.startDiscovery()
在onReceive中获取搜索到的设备,通过intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)来获取搜索到的设备:
BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "onReceive: " + action);
if (action == null) return;
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
switch (action) {
case BluetoothDevice.ACTION_FOUND:
//获取搜索到设备的信息
Log.i(TAG, "device name: " + device.getName() + " address: " + device.getAddress());
//获取绑定状态
Log.i(TAG, "device bond state : " + device.getBondState());
break;
case BluetoothDevice.ACTION_BOND_STATE_CHANGED:
Log.i(TAG, "BOND_STATE_CHANGED device name: " + device.getName() + " address: " + device.getAddress());
Log.i(TAG, "BOND_STATE_CHANGED device bond state : " + device.getBondState());
break;
case BluetoothAdapter.ACTION_STATE_CHANGED:
Log.i(TAG, "BOND_STATE_CHANGED device name: " + device.getName() + " address: " + device.getAddress());
Log.i(TAG, "BOND_STATE_CHANGED device bond state : " + device.getBondState());
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
Log.i(TAG, "bluetooth discovery finished");
break;
}
}
};
划重点android6.0之后如果没有申请设备定位权限,将无法搜索到任何蓝牙设备,即文章开头说的两个定位权限,申请其中之一便可。
本地蓝牙信息:
当然,本地蓝牙的信息也是通过BluetoothAdapter来获取的:
//获取本地蓝牙名称
String name = mBluetoothAdapter.getName();
//获取本地蓝牙地址
String address = mBluetoothAdapter.getAddress();
//获取已配对蓝牙设备
Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
//上面用到过的,判断蓝牙是否已经开启
mBluetoothAdapter.isEnabled()
绑定
绑定也很简单 mBluetoothDevice.createBond()在获取到你想要绑定的BluetoothDevice对象后直接调用它的createBond()方法。该方法是异步调用,但会立即返回一个boolean类型的返回值,返回true表示开始配对(这时候你手机应该会有个提示配对的弹出框)、返回false表示失败,失败原因可能是你蓝牙关掉了或者该设备已经配对了。
//绑定蓝牙设备
mBluetoothDevice.createBond();
要注意的是,在网上搜索到的大多数绑定是通过反射实现绑定的,这是在api19(也就是android4.4)之前createBond()方法为@hide的。至于选哪一种,就看你的需求了。
解绑
虽然绑定的方法已经公开,但是解绑的方法仍然是@hide的。所以解绑仍然需要通过反射来调用,我这里把代码贴一下:
if (device.getBondState() == BOND_BONDED) {
//对于已经绑定的设备解绑执行removeBond方法来解绑
try {
Method method = BluetoothDevice.class.getMethod("removeBond");
method.invoke(device);
} catch (Exception e) {
e.printStackTrace();
}
} else if (device.getBondState() == BOND_BONDING) {
//当蓝牙设备正在绑定时,可以调用cancelBondProcess来取消绑定行为
try {
Method method = BluetoothDevice.class.getMethod("cancelBondProcess");
method.invoke(device);
} catch (Exception e) {
e.printStackTrace();
}
}
连接与数据传输
蓝牙之前可以通过sdp协议进行连接通信,是一种socket通信,其中关键的两个类BluetoothServerSocket和BluetoothSocket开头就注释了“Bluetooth sockets类似于TCP sockets”,所以也很容易理解。
服务端
BluetoothServerSocket可以通过BluetoothAdapter.listenUsingRfcommWithServiceRecord()获取,这个方法有两个参数{String name, UUID uuid}。name就是给你的服务端起个名字,uuid如果不了解的话可以看前言篇关于uuid的介绍,本文顶端有前言篇连接。
创建好serverSocket后调用其accept()方法接收客户端发来的连接。该方法为阻塞式的,直到有接收到客户端的连接请求才会接着执行剩下的逻辑。同时该方法会返回一个BluetoothSocket对象,我们通过该BluetoothSocket的getInputStream()和getOutputStream()获取进行输入和输出流来接收和发送数据。
private static final String SERVICE_NAME = "yapple server";
private static UUID SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothAdapter mAdapter;
private BluetoothServerSocket mServerSocket;
private BluetoothSocket mSocket;
public void buildServerSocket() {
try {
mServerSocket = mAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME, SERVICE_UUID);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 创建服务端监听客户端的请求
*/
public void startServerListen() {
if (mServerSocket == null) {
buildServerSocket();
}
new Thread(() -> {
while (true) {
try {
Log.i(TAG, "startServerListen: before");
mSocket = mServerSocket.accept();
Log.i(TAG, "startServerListen: after");
if (mSocket != null) {
//获得用于传输的socket后将BluetoothServerSocket 及时关闭,避免连接其他蓝牙请求,已经不必要的资源浪费
mServerSocket.close();
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 在服务端接收客户端发过来的数据
* BufferedReader reader 在读取完成之后不要急着close,否则会关闭mSocket。根据自己的需求做逻辑上的处理。
* 注意reader.readLine()要保证对方发送的数据是有换行的“\r\n”,否则会认为一直没有读完
*/
public void receiveMsgWithServer() {
if (mSocket != null) {
new Thread(()->{
BufferedReader reader;
try {
InputStream inputStream = mSocket.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream));
Log.i(TAG, "receiveMsgWithServer: reader: " + reader.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
客户端
服务端会等待客户端去连接,客户端会通过目标BluetoothDevice的createRfcommSocketToServiceRecord方法来创建连接客户端的BluetoothSocket(该方法只有一个参数UUID,要保证和服务端的一样)。最后再通过BluetoothSocket的connect方法来请求连接服务器,如果两台设备已经配对过,则直接连接成功(服务端的mServerSocket.accept()会直接返回socket)。如果未配对,则会弹出配对提示。这时候像服务端那样,我们可通过该BluetoothSocket的getInputStream()和getOutputStream()获取进行输入和输出流来接收和发送数据。
private static UUID SERVICE_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothAdapter mAdapter;
private BluetoothSocket mClientSocket;
private BluetoothDevice mDevice;
/**
* 创建客户端socket并发送连接请求
*/
public void buildClientSocketAndConnect() {
new Thread(() -> {
try {
// 在连接蓝牙之前,还要先取消蓝牙设备的扫描,否则容易连接失败。
if (mAdapter.isDiscovering()) {
mAdapter.cancelDiscovery();
}
mClientSocket = mDevice.createRfcommSocketToServiceRecord(SERVICE_UUID);
mClientSocket.connect();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
/**
* 客户端发送数据
*
* @param msg 要发送的字符串
*/
public void sendMsgByClient(String msg) {
if (mClientSocket != null) {
try {
BufferedWriter writer;
OutputStream outputStream = mClientSocket.getOutputStream();
Log.i(TAG, "sendMsgByClient: isConnected " + mClientSocket.isConnected() + " mClientSocket.getConnectionType() " + mClientSocket.getConnectionType());
writer = new BufferedWriter(new OutputStreamWriter(outputStream));
writer.write(msg + "\r\n");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}
关闭连接:直接调用socket的close方法即可。
为了避免篇幅过于冗长,上面的方法仅仅是简单执行了逻辑、实现了功能,如果要加上界面的更新、线程的通讯可能代码就会偏离主题了。除此之外,上面的代码只写了服务端的接收和客户端的发送,这并不是说数据传输是单向的,服务端当然也是可以获取OutputStream来发送,客户端可以获取InputStream来接收。逻辑也很简单,读者自己去实践吧!
下一篇我将介绍有关BLE的相关知识,及实践。
网友评论