11. 蓝牙通信
11.1 问题
在应用程序中,要通过蓝牙通信实现不同设备之间的数据传输。
11.2 解决方案
(API Level 5)
可以使用API Level5中引入的蓝牙API,在射频通信(RFCCOMM)协议接口上创建一个点对点的连接。蓝牙是一种非常流行的无线电技术,几乎现在所有的移动设备都支持该技术。很多用户认为蓝牙只能用来连接移动设备与无线耳机或者与车载音响系统整合。但实际上,对于开发人员来说,在应用程序中蓝牙还是一种用来创建点对点连接的简单的高效的方式。
11.3 实现机制
要点
Android模拟器现在还不支持蓝牙,因此要想执行本例中的代码,必须把它们运行在一台Android设备上。此外,要很好地测试这个功能,需要在两台设备上同时运行这个应用程序。
蓝牙点对点
以下三个代码演示了使用蓝牙查找附近的其他用户并快速交换联系信息(本例中,只是交换电子邮件地址)。蓝牙上的连接是通过发现可用的“服务”,并通过全局唯一的128位UUID值连接到相应的服务。也就是说,在连接某个服务之前,必须首先发现或知道它的UUID。
在本例中,连接两端的设备运行的是相同的应用程序,两个应用都会有对应的UUID,因此我们可以在代码中自由地将UUID定义为常数。
注意:
为了确保选择的UUID是唯一的,请使用网络上免费的UUID生成器,或者使用相应的工具,例如Mac/Linux上的uuidgen。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.examples.bluetooth">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<application
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity android:name=".ExchangeActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
要点:
记住,要想使用这些API,需要在清单中声明android.permission.BLUETOOTH权限。另外,要想改变蓝牙的可发现性以及启用/禁用蓝牙适配器,还要在清单中声明android.permission.BLUETOOTH_ADMIN权限。
res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Enter Your Email:"/>
<EditText
android:id="@+id/emailField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/label"
android:singleLine="true"
android:inputType="textEmailAddress"/>
<Button
android:id="@+id/scanButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="Connect and Share" />
<Button
android:id="@+id/listenButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/scanButton"
android:text="Listen for Sharers" />
</RelativeLayout>
这个示例的UI由一个让用户输入电子邮件的EditText和两个用于初始化通信的按钮组成。名为“Listen for Shares”的按钮用来将设备设为监听模式。在这种模式下,设备会接受其他设备的连接,并尝试与之进行通信。名为“Connect and Share”的按钮用来将设备设为搜索模式。在这种模式下,设备会搜索当前处于监听模式的监听,并与之进行连接(参见以下代码)。
蓝牙交换Activity
public class ExchangeActivity extends Activity {
// 本应用程序唯一的UUID (从网上生成)
private static final UUID MY_UUID = UUID.fromString("321cb8fa-9066-4f58-935e-ef55d1ae06ec");
//发现时用于匹配的一个更加友好的名称
private static final String SEARCH_NAME = "bluetooth.recipe";
BluetoothAdapter mBtAdapter;
BluetoothSocket mBtSocket;
Button listenButton, scanButton;
EditText emailField;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle("Activity");
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.main);
//检查系统状态
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
if(mBtAdapter == null) {
Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
finish();
return;
}
if (!mBtAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE);
}
emailField = (EditText)findViewById(R.id.emailField);
listenButton = (Button)findViewById(R.id.listenButton);
listenButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//首先要确保设备是可以发现的
if (mBtAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivityForResult(discoverableIntent, REQUEST_DISCOVERABLE);
return;
}
startListening();
}
});
scanButton = (Button)findViewById(R.id.scanButton);
scanButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBtAdapter.startDiscovery();
setProgressBarIndeterminateVisibility(true);
}
});
}
@Override
public void onResume() {
super.onResume();
//为Activity注册广播Intent
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, filter);
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}
@Override
public void onDestroy() {
super.onDestroy();
try {
if(mBtSocket != null) {
mBtSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static final int REQUEST_ENABLE = 1;
private static final int REQUEST_DISCOVERABLE = 2;
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case REQUEST_ENABLE:
if(resultCode != Activity.RESULT_OK) {
Toast.makeText(this, "Bluetooth Not Enabled.", Toast.LENGTH_SHORT).show();
finish();
}
break;
case REQUEST_DISCOVERABLE:
if(resultCode == Activity.RESULT_CANCELED) {
Toast.makeText(this, "Cannot listen unless we are discoverable.", Toast.LENGTH_SHORT).show();
} else {
startListening();
}
break;
default:
break;
}
}
//启动服务器套接字并监听
private void startListening() {
AcceptTask task = new AcceptTask();
task.execute(MY_UUID);
setProgressBarIndeterminateVisibility(true);
}
//AsyncTask接受传入的连接
private class AcceptTask extends AsyncTask<UUID,Void,BluetoothSocket> {
@Override
protected BluetoothSocket doInBackground(UUID... params) {
String name = mBtAdapter.getName();
try {
//监听时,将发现名设置为指定的值
mBtAdapter.setName(SEARCH_NAME);
BluetoothServerSocket socket = mBtAdapter.listenUsingRfcommWithServiceRecord("BluetoothRecipe", params[0]);
BluetoothSocket connected = socket.accept();
//复位蓝牙适配名称
mBtAdapter.setName(name);
return connected;
} catch (IOException e) {
e.printStackTrace();
mBtAdapter.setName(name);
return null;
}
}
@Override
protected void onPostExecute(BluetoothSocket socket) {
if(socket == null) {
return;
}
mBtSocket = socket;
ConnectedTask task = new ConnectedTask();
task.execute(mBtSocket);
}
}
//AsyncTask接收一行数据并发送
private class ConnectedTask extends AsyncTask<BluetoothSocket,Void,String> {
@Override
protected String doInBackground(BluetoothSocket... params) {
InputStream in = null;
OutputStream out = null;
try {
//发送数据
out = params[0].getOutputStream();
out.write(emailField.getText().toString().getBytes());
//接收其他数据
in = params[0].getInputStream();
byte[] buffer = new byte[1024];
in.read(buffer);
//从结果创建一个空字符串
String result = new String(buffer);
//关闭连接
mBtSocket.close();
return result.trim();
} catch (Exception exc) {
return null;
}
}
@Override
protected void onPostExecute(String result) {
Toast.makeText(ExchangeActivity.this, result, Toast.LENGTH_SHORT).show();
setProgressBarIndeterminateVisibility(false);
}
}
// 用来监听发现设备的BroadcastReceiver
private BroadcastReceivercovered devices mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 当找到一台设备时
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 从Intent中获得BluetoothDevice对象
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if(TextUtils.equals(device.getName(), SEARCH_NAME)) {
//匹配找到的设备,连接
mBtAdapter.cancelDiscovery();
try {
mBtSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
mBtSocket.connect();
ConnectedTask task = new ConnectedTask();
task.execute(mBtSocket);
} catch (IOException e) {
e.printStackTrace();
Toast.makeText(ExchangeActivity.this, "Error connecting to remote", Toast.LENGTH_SHORT).show();
}
}
//发现完成
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
setProgressBarIndeterminateVisibility(false);
}
}
};
}
应用程序初次启动后,会首先对设备上蓝牙的状态做一些基本的检查。如果BluetoothAdapter.getDefaultAdapter()返回null,表明设备不支持蓝牙,应用程序无法继续使用。即使设备上有蓝牙,它也必须是启用的,这样应用程序才能使用它。如果蓝牙是禁用的,推荐启用适配器的方法是向系统发送一个action值为BluetoothAdapter.ACTION_REQUEST_ENABLED的Intent。这样就会通知用户启用蓝牙。可以用enable()方法手动启动BluetoothAdapter,但我们不推荐这种方法,除非需要通过某种特别的方式来获得用户的权限。
验证过蓝牙可用后,应用程序会等待用户输入。正如之前提到的,这个示例可以将每台设备设为两种模式之一:监听模式或搜索模式。接下来看看这两种模式各种的工作方式。
监听模式
单击“Listen for Sharers”按钮开始让应用程序对接入的连接进行监听。对于一台设备来说,如果想要接收未知设备的接入连接,那么该设备必须设置为可被发现的。应用程序会检查适配器的扫描模式是否等于SCAN_MODE_CONNECTABLE_DISCOVERABLE,从而确定设备是否是可被发现的。如果适配器不满足这个要求,就会给系统发送一个Intent,告诉用户应该让设备处于可发现状态,这和要求用户启用蓝牙的方法很相似。如果用户拒绝了该请求,Activity会返回Activity.RESULT_CANCELED。这个示例会在onActivityResult()中处理用户拒绝请求的行为,即在这些条件下终止应用。
如果用户启用了可发现状态或者设备已经处于可发现状态,就会创建并执行一个AcceptTask。这个任务会为我们所定义服务的UUID创建监听器端口,通过这个端口阻塞主调线程并等待接入连接请求。在收到有效的请求时,就会接受。然后应用程序会切换到Connected Mode(已连接模式)。
在设备处于监听状态的过程中,其蓝牙的名称会被设定为已知的唯一值(SEARCH_NAME)来加速发现的过程(具体原因见后面的“搜索模式”小节)。连接建立后,适配器就会恢复到默认名称。
搜索模式
单击“Connect and Share”按钮,通知应用程序开始搜索另一台想要连接的设备。这当中会首先启动蓝牙发现过程并且在BroadcastReceiver中处理搜索结果。通过BluetoothAdapter.startDiscovery()开始一次发现后,以下两种情况下,Android会通过广播进行异步回调:找到了另一台设备或发现过程完成。
私有的接收器mReceiver在Activity对用户可见时会随时进行注册,对于每一台新发现的设备,它都会收到一个广播。回忆一下,在讨论监听模式时,监听设备的名称被设置成唯一的值。在每次发现完成后,接收器会检测与当前名称匹配的设备,并且在搜索到结果后尝试进行连接。这对于发现过程的速度非常重要,因为验证一台设备是否可用的唯一途径就是将这台设备与一个特殊的服务UUID进行尝试性连接,以查看操作是否成功。蓝牙连接过程属于重量级操作,并且很慢,应该在确保其他一切运行良好时才进行这个操作。
这种匹配设备的方式同样减轻了用户手动连接其想要连接的设备的过程。应该程序会智能地寻找到同样运行这个应用程序并且处于监听模式的另一台设备来完成传输。移除用户也意味着这个值是唯一且非常少见的,就是为了避免查找其他设备时,这些设备可能意外具有相同的名称。
找到匹配的设备后,就会停止发现过程(因为它同样是重量级操作,并且会减缓连接过程),然后连接到服务的UUID。在连接成功后,应用程序就进入了已连接模式。
提示:
可以在许多地方生成自己的唯一ID(UUID)值。各种网站,如http://www.uuidgenerator.net/,将自动创建一个UUID。Mac OS X和Linux用户还可以从命令行运行到uuidgen命令。
已连接模式
一旦连接成功,两台设备上的应用程序将创建一个ConnectedTask来发送和接收用户联系信息。连接的BluetoothSocket会用一个InputStream和一个OutputStream进行数据传输。首先,电子邮件的文本字段的当前值在封装后被写入OutputStream。然后,从InputStream读取以接收远程设备的信息。最后,每台设备都需要将它接收的原始数据封装成一个单纯的字符串显示给用户。
ConnectedTask.onPostExecute()方法的任务是向用户显示交流的结果;目前,是将接收的内容显示在一个Toast中。交流完成后,连接被关闭,两台设备都会进入相同的模式,准备进行下一次交流。
有关此主题的更多信息,可以查看Android SDK提供的BlueoothChat示例应用程序。这个应用程序很好地演示了如何使用一个长连接在设备之间发生聊天信息。
Android之外的蓝牙:
正如本节开始是描述的那样,除了手机和平板电脑,许多无线设备上也有蓝牙。在诸如蓝牙调制解调器和串行适配器这样的设备上同样有RFCOMM接口。在Android设备上创建点对点连接是使用的API同样可以用来连接其他嵌入式蓝牙设备,从而实现对设备的监控和控制。
要想与这些嵌入式设备建立连接,关键是要获得它们所支持的RFCOMM服务的UUID。作为配置文件标准的一部分的蓝牙服务及其标识符由蓝牙特别兴趣小组(Special Interest Group,SIG)定义;因此,我们能够从www.bluetooth.org提供的文档中获得给定设备所需的UUID。然而,如果设备制造商为自定义服务类型定义了设备特有的UUID,并且没有归入文档,则必须通过某种方式来发现该UUID。与前面的示例一样,我们可以使用适当的UUID创建一个BluetoothSocket和传输数据。
SDK就拥有这种能力,虽然在Android 4.0.3(API Level 15)之前它并不是SDK的开放部分。对于蓝牙设备来说,有两个方法能够提供此信息:fetchUuidsWithSdp()和getUuids()。后者只会简单地返回在发现期间找到的设备的缓存实例,而前者则会异步连接设备并进行一个新的查询。正因为如此,在使用fetchUuidsWithSdp()时,必须注册一个BroadcastReceiver,它将接收action值为BluetoothDevice.ACTION_UUID的Intent并发现UUID值。