Android Bluetooth
使用Android蓝牙API来进行蓝牙通信的四个任务:
- 设置蓝牙
- 检索周围匹配的或者可用的设备
- 连接设备
- 设备间传输数据
所有蓝牙APIs在android.bluetooth 包中。
创建蓝牙连接所要用到的类和接口:
-
BluetoothAdapter
表示本地蓝牙适配器(蓝牙无线电广播)。BluetoothAdapter是所有蓝牙交互的入口点。你能够通过它发现其它蓝牙设备,查询一系列已经匹配的设备,使用已知的MAC地址实例化一个 BluetoothDevice,创建 BluetoothServerSocket 监听来自其他设备的通信。
-
BluetoothDevice
表示远程蓝牙设备。用这个类通过 BluetoothSocket能够请求同远程设备的链接,或者查询设备的名字、地址、类和绑定状态。 -
BluetoothSocket
表示蓝牙套接字通信(类似于TCP Socket)的接口。这是允许应用通过InputStream和OutputStream与其他蓝牙设备进行数据交换的连接点。 -
BluetoothServerSocket
表示用于监听即将到来的请求的对外公开套接字(类似于TCP ServerSocket)。为了连接两个安卓设备,一个设备必须用这个类开放一个服务端的套接字。当远程的蓝牙设备向该设备发起连接请求时,在连接建立的时候BluetoothServerSocket会返回一个BluetoothSocket。
蓝牙权限
为了在应用中使用蓝牙特性,必须声明蓝牙权限 BLUETOOTH。如果还需要操作蓝牙设置,必须声明 BLUETOOTH_ADMIN 权限。
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
设置蓝牙
在应用能够通过蓝牙通信之前,需要校验设备是否支持蓝牙,如果支持,要确保蓝牙是处于开启状态。如果不支持蓝牙,应当提示用户不支持。如果支持蓝牙但是没有打开,可以在不离开应用的情况下请求应用开启蓝牙。
1.获取 BluetoothAdapter
所有的蓝牙Activity都需要BluetoothAdapter 。为了得到BluetoothAdapter,调用静态方法getDefaultAdapter()·
。这个方法会返回一个BluetoothAdapter,他表示设备自身的蓝牙适配器(蓝牙无线电广播)。如果 getDefaultAdapter()返回为null则表示设备不支持蓝牙。整个系统只存在一个蓝牙适配器,应用通过它来交互。
2.打开蓝牙
调用isEnabled()
去检查蓝牙是否已经启用。如果方法返回为false,则蓝牙未被启用。要请求启用蓝牙,调用 [startActivityForResult()](https://developer.android.com/reference/android/app/Activity.html#startActivityForResult(android.content.Intent, int))并且携带action intent参数 ACTION_REQUEST_ENABLE 。这会显示一个对话框请求用户权限来启用蓝牙,如果用户点击“yes”,则系统会开始启用蓝牙。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
Toast.makeText(this, "蓝牙无法使用", Toast.LENGTH_LONG).show();
} else { // 蓝牙可以使用
if (!mBluetoothAdapter.isEnabled()) { // 蓝牙未开启则开启蓝牙
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
}else{
// 蓝牙已打开
}
}
传输给 startActivityForResult()的常量REQUEST_ENABLE_BT
是一个本地定义的整型(必须大于零),在onActivityResult()实现中,系统将这个整型作为requestCode
参数返回。
如果蓝牙启动成功,在 onActivityResult() 回调中,Activity接收到RESULT_OK
结果码。如果蓝牙由于某一个错误没有启动,则返回码为 RESULT_CANCELED
。
private static final int REQUEST_ENABLE_BT = 1;
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_BT: // 开启蓝牙的处理
if (resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "蓝牙已开启!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "请开启蓝牙!", Toast.LENGTH_SHORT).show();
}
}
}
打开蓝牙还可以用enable()
方法来开启,无需询问用户(无声息的开启蓝牙设备),这时就需要用到android.permission.BLUETOOTH_ADMIN
权限。
mBluetoothAdapter.enable();
3.关闭蓝牙
关闭蓝牙直接用disable()
方法即可:
if (mBluetoothAdapter != null) {
if (mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.disable(); // 关闭蓝牙
Toast.makeText(BlueToothListActivity.this, "蓝牙已关闭", Toast.LENGTH_SHORT).show();
}
}
查找设备
使用 BluetoothAdapter,能够找到远程蓝牙设备,不论是通过设备查找还是通过查询已匹配设备列表。
已匹配意味着两个设备都知道各自的存在,拥有一个能够用来授权的共享连接密钥,并且能够互相建立一个加密的连接。而已连接意味着当前设备间共享一个RFCOMM信道,并且能够互相传递数据。
目前Android蓝牙API在一个RFCOMM连接建立之前要求设备已经配对。
查询已匹配的设备
调用getBondedDevices()
方法会返回代表已匹配设备BluetoothDevice的一个集合。能够查询所有已匹配设备,然后使用ArrayAdapter,将每个设备的名称显示给用户。
ArrayAdapter<String> pairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_item);
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
pairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
} else {
// 获取res/values/strings.xml文件中的字符串
//<string name="none_paired">没有适配的设备</string>
String noDevices = getResources().getText(R.string.none_paired).toString();
pairedDevicesArrayAdapter.add(noDevices);
}
其中,Activity对应的布局文件中应包含ListView,对应布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<ListView android:id="@+id/paired_devices"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
ArrayAdapter期望布局文件里只有一个TextView,连Layout都不能包含。ArrayAdapter对应的布局文件device_item.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:padding="5dp"
/>
之后找到ListView,将Adapter的数据绑定到页面:
ListView pairedListView = (ListView) findViewById(R.id.paired_devices);
pairedListView.setAdapter(pairedDevicesArrayAdapter);
扫描新设备
调用startDiscovery()
即可开始感应设备。这个处理过程是异步的,并且方法会立即返回一个boolean值,用于指明感应操作是否已经成功启动。感应过程通常包含一个大约十二秒的查询扫描操作,紧接着的页面扫描每个已找到的设备取出它的蓝牙名称。
public void searchNewDevices(){
Toast.makeText(this, "开始搜索!", Toast.LENGTH_SHORT).show();
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
mBluetoothAdapter.startDiscovery();
}
为了接收关于每个感应到的设备的信息,必须为 ACTION_FOUND intent注册一个BroadcastReceiver。对每个设备,系统将会广播 ACTION_FOUND intent。这个intent携带额外的参数 EXTRA_DEVICE和 EXTRA_CLASS,分别包含一个相应的BluetoothDevice 和一个 BluetoothClass。
private ArrayAdapter<String> mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// When discovery finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice object from the Intent
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// If it's already paired, skip it, because it's been listed already
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
// When discovery is finished
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
Toast.makeText(MainActivity.this, "搜索完成!", Toast.LENGTH_SHORT).show();
if (mNewDevicesArrayAdapter.getCount() == 0) {
// 获取res/values/strings.xml文件中的字符串
// <string name="none_found">没有发现设备</string>
String noDevices = getResources().getText(R.string.none_found).toString();
mNewDevicesArrayAdapter.add(noDevices);
}
}
}
};
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestroy
// Register for broadcasts when discovery has finished
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
this.registerReceiver(mReceiver, filter);
ListView newDevicesListView = (ListView) findViewById(R.id.new_devices);
newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
最后记得取消广播注册:
@Override
protected void onDestroy() {
super.onDestroy();
if (mBluetoothAdapter != null) {
mBluetoothAdapter.cancelDiscovery();
}
// Unregister broadcast listeners
unregisterReceiver(mReceiver);
}
这里需要注意的是,如果你的代码将运行在(Build.VERSION.SDK_INT >= 23)的设备上,那么务必加上以下权限,并在代码中动态的申请权限
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
private static final int REQUEST_PERMISSION_ACCESS_LOCATION = 1;
private void requestPermission() {
if (Build.VERSION.SDK_INT >= 23) {
int checkAccessFinePermission = ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION);
if (checkAccessFinePermission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
REQUEST_PERMISSION_ACCESS_LOCATION);
Log.e(getPackageName(), "没有权限,请求权限");
return;
}
Log.e(getPackageName(), "已有定位权限");
//这里可以开始搜索操作
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case REQUEST_PERMISSION_ACCESS_LOCATION: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.e(getPackageName(), "开启权限permission granted!");
//这里可以开始搜索操作
} else {
Log.e(getPackageName(), "没有定位权限,请先开启!");
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
连接设备
作为客户端连接
为了初始化一个到远程设备的连接,必须首先获得一个代表远程设备的 BluetoothDevice 对象。
// address is device MAC address
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
然后必须使用BluetoothDevice去获取一个 BluetoothSocket 并且初始化这个连接。
这里是基本的操作流程:
- 使用 BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID)得到一个 BluetoothSocket.这将初始化一个连接到 BluetoothDevice的BluetoothSocket。
- 调用 connect()初始化连接
一旦调用这个接口,为了匹配UUID,系统会在远程设备上执行一个SDP查询操作。如果查询成功,并且远程设备接收这个连接,则在连接期间它会共享使用RFCOMM信道,并且connect()调用也会返回。这个方法是一个阻塞调用。如果因为任何原因,连接失败或者connect() 方法超时(大约超过12秒),则它会抛出一个异常。
因为connect() 是一个阻塞调用,这个连接处理应当总是在主activity线程之外的一个独立线程中执行。
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()是在建立连接之前调用的。应当在连接之前总是执行这个操作,并且不用检查是否在运行,调用cancelDiscovery()总是安全的(如果你确实想检查,调用 isDiscovering())。
当用完 BluetoothSocket,一定要调用 close()来完成清理工作。这么做会立即关闭掉已连接的socket并且清理所有中间资源。