一.概述

在第一篇​​Android开发之蓝牙详解(一)​​中我主要介绍了一些理论性的知识以及比较基础的操作,比如控制蓝牙的状态,查找设备,显示当前已连接的设备,今天我们看看其他的一些操作。

二.基础

1.可发现模式

在上一篇最后,我们介绍了如何发现设备,但是有些时候,我们当前设备是不可被发现的,也就是说即使我们打开了蓝牙其他设备也是无法搜索到的。所以我们要学学如何启用设备的可发现性。如果要让本地设备可以被其他设备发现,那么就要调用ACTION_REQUEST_DISCOVERABLE操作意图的startActivityForResult(Intent, int)方法。这个方法会向系统设置发出一个启用可发现模式的请求(不终止应用程序)。默认情况下,设备的可发现模式会持续120秒。通过给Intent对象添加EXTRA_DISCOVERABLE_DURATION附加字段,可以定义不同持续时间。应用程序能够设置的最大持续时间是3600秒,0意味着设备始终是可发现的。任何小于0或大于3600秒的值都会自动的被设为120秒。例如,以下代码把持续时间设置为300秒:

Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//定义持续时间
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

效果图:

Android开发之蓝牙详解(二)_套接字

如图所示,申请用户启用设备的可发现模式时,会显示这样一个对话框。如果响应“与喜怒”,那么设备的可发现模式会持续指定的时间,而且你的Activity会接收带有结果代码等于可发现设备持续时间的onActivityResult()回调方法的调用。如果用户响应“拒绝”或有错误发生,则结果代码等于RESULT_CANCELED.

注意:如果设备没有开启蓝牙功能,那么开启设备的可发现模式会自动开启蓝牙。
在可发现模式下,设备会静静的把这种模式保持到指定的时长。如果你想要在可发现模式被改变时获得通知,那么你可以注册一个ACTION_SCAN_MODE_CHANGED类型的Intent广播。这个Intent对象中包含了EXTRA_SCAN_MODE和EXTRA_PREVIOUS_SCAN_MODE附加字段,它们会分别告诉你新旧扫描模式。它们每个可能的值是:SCAN_MODE_CONNECTABLE_DISCOVERABLE,SCAN_MODE_CONNECTABLE或SCAN_MODE_NONE,它们分别指明设备是在可发现模式下,还是不在可发现模式下但依然可接收连接,或者是不在可发现模式下,不能接收连接。
下面是示例代码:

public class MainActivity extends Activity {
public static final int TURN = 1;
private BluetoothController mController = new BluetoothController();
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,1);
switch (mode){
case BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE:
Toast.makeText(MainActivity.this, "现在是可发现模式", Toast.LENGTH_SHORT).show();
break;
case BluetoothAdapter.SCAN_MODE_CONNECTABLE:
Toast.makeText(MainActivity.this, "现在不是可发现模式,但是可以连接", Toast.LENGTH_SHORT).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//注册广播,监听模式改变
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
registerReceiver(mReceiver,filter);
}
public void click(View view){
switch (view.getId()){
case R.id.turnOn:
mController.turnOnBluetooth(this, TURN);
break;
case R.id.canFind:
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//定义持续时间
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 10);
startActivity(discoverableIntent);
break;
}
}
}

如果你要初始化跟远程设备的连接,你不需要启用设备的可现性。只有在你想要把你的应用程序作为服务端来接收输入连接时,才需要启用可发现性,因为远程设备在跟你的设备连接之前必须能够发现它。

2.连接设备
为了让两个设备上的两个应用程序之间建立连接,你必须同时实现服务端和客户端机制,因为一个设备必须打开服务端口,同时另一个设备必须初始化跟服务端设备的连接(使用服务端的MAC地址来初始化一个连接)。当服务端和客户端在相同的RFCOMM通道上有一个BluetoothSocket连接时,才能够被认为是服务端和客户端之间建立了连接。这时,每个设备能够获得输入和输出流,并且能够彼此开始传输数据。
服务端设备和客户端设备彼此获取所需的BluetoothSocket的方法是不同的。服务端会在接收输入连接的时候接收到一个BluetoothSocket对象。客户端会在打开跟服务端的RFCOMM通道时接收到一个BluetoothSocket对象。
一种实现技术是自动的准备一个设备作为服务端,以便在每个设备都会有一个服务套接字被打开,并监听连接请求。当另一个设备初始化一个跟服务端套接字的连接时,它就会变成一个客户端。另一种方法,一个设备是明确的”host”连接,并且根据要求打开一个服务套接字,而其他的设备只是简单的初始化连接。
注意:如果两个设备之前没有配对,那么Android框架会在连接过程期间,自动的显示一个配对请求通知或对话框给用户,如图3所示。因此在试图连接设备时,你的应用程序不需要关心设备是否被配对。FRCOMM的尝试性连接会一直阻塞,一直到用户成功的配对,或者是因用户拒绝配对或配对超时而失败。

当你想要连接两个设备时,一个必须通过持有一个打开的BluetoothServerSocket对象来作为服务端。服务套接字的用途是监听输入的连接请求,并且在一个连接请求被接收时,提供一个BluetoothSocket连接对象。在从BluetoothServerSocket对象中获取BluetoothSocket时,BluetoothServerSocket能够(并且也应该)被废弃,除非你想要接收更多的连接。

以下是建立服务套接字和接收一个连接的基本过程。
1. 调用listenUsingRfcommWithServiceRecord(String, UUID)方法来获得一个BluetoothServerSocket对象。该方法中的String参数是一个可识别的你的服务端的名称,系统会自动的把它写入设备上的Service Discovery Protocol(SDP)数据库实体(该名称是任意的,并且可以简单的使用你的应用程序的名称)。UUID参数也会被包含在SDP实体中,并且是跟客户端设备连接的基本协议。也就是说,当客户端尝试跟服务端连接时,它会携带一个它想要连接的服务端能够唯一识别的UUID。只有在这些UUID完全匹配的情况下,连接才可能被接收。

  1. 通过调用accept()方法,启动连接请求。这是一个阻塞调用。只有在连接被接收或发生异常的情况下,该方法才返回。只有在发送连接请求的远程设备所携带的UUID跟监听服务套接字所注册的一个UUID匹配的时候,该连接才被接收。连接成功,accept()方法会返回一个被连接的BluetoothSocket对象。
  2. 除非你想要接收其他连接,否则要调用close()方法。该方法会释放服务套接字以及它所占用的所有资源,但不会关闭被连接的已经有accept()方法所返回的BluetoothSocket对象。跟TCP/IP不一样,每个RFCOMM通道一次只允许连接一个客户端,因此在大多数情况下,在接收到一个连接套接字之后,立即调用BluetoothServerSocket对象的close()方法是有道理的。
    accept()方法的调用不应该在主Activity的UI线程中被执行,因为该调用是阻塞的,这会阻止应用程序的其他交互。通常在由应用程序所管理的一个新的线程中来使用BluetoothServerSocket对象或BluetoothSocket对象来工作。要终止诸如accept()这样的阻塞调用方法,就要从另一个线程中调用BluetoothServerSocket对象(或BluetoothSocket对象)的close()方法,这时阻塞会立即返回。注意在BluetoothServerSocket或BluetoothSocket对象上的所有方法都是线程安全的。

示例
以下是一个被简化的接收连接请求的服务端组件:

public class AcceptThread extends Thread{
private BluetoothServerSocket mServerSocket;
public AcceptThread(){
BluetoothServerSocket tmp = null;
try {
tmp =mController.getmAdapter().listenUsingRfcommWithServiceRecord("BluetoothServer", UUID.fromString(getPackageName()));
} catch (IOException e) {
e.printStackTrace();
}
mServerSocket = tmp;
}

@Override
public void run() {
super.run();
BluetoothSocket socket = null;
//不断监听直到返回连接或者发生异常
while (true){
try {
//启连接请求,这是一个阻塞方法,必须放在子线程
socket = mServerSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
//建立了连接
if(socket!=null){
//管理连接(在一个独立的线程里进行)
manageConnectedSocket(socket);
try {
mServerSocket.close();//关闭连接
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 取消正在监听的接口
*/
public void cancel(){
try {
mServerSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public void manageConnectedSocket(BluetoothSocket socket){
}

在这个例子中,只希望有一个呼入连接,因此连接一旦被接收,并获取了一个BluetoothSocket对象,应用程序就会把获得的BluetoothSocket对象发送给一个独立的线程,然后关闭BluetoothServerSocket对象并中断循环。
注意,在accept()方法返回BluetoothSocket对象时,套接字已经是被连接的,因此你不应该再调用像客户端那样调用connect()方法了。
应用程序中的manageConnectedSocket()方法是一个自定义方法,它会初始化用于传输数据的线程。
通常,一旦你监听完成呼入连接,就应该关闭BluetoothServerSocket对象。在这个示例中,close()方法是在获得BluetoothSocket对象之后被调用的。你可能还想要提供一个公共方法,以便在你的线程中能够关闭你想要终止监听的服务套接字事件中的私有BluetoothSocket对象。

作为连接的客户端

为了初始化一个与远程设备(持有打开的服务套接字的设备)的连接,首先必须获取个代表远程设备的BluetoothDevice对象。然后使用BluetoothDevice对象来获取一个BluetoothSocket对象,并初始化该连接。
以下是一个基本的连接过程:
1. 通过调用BluetoothDevice的createRfcommSocketToServiceRecord(UUID)方法,获得一个BluetoothSocket对象。这个方法会初始化一个连接到BluetoothDevice对象的BluetoothSocket对象。传递给这个方法的UUID参数必须与服务端设备打开BluetoothServerSocket对象时所使用的UUID相匹配。在你的应用程序中简单的使用硬编码进行比对,如果匹配,服务端和客户端代码就可以应用这个BluetoothSocket对象了。

  1. 通过调用connect()方法来初始化连接。在这个调用中,为了找到匹配的UUID,系统会在远程的设备上执行一个SDP查询。如果查询成功,并且远程设备接收了该连接请求,那么它会在连接期间共享使用RFCOMM通道,并且connect()方法会返回。这个方法是一个阻塞调用。如果因为某些原因,连接失败或连接超时(大约在12秒之后),就会抛出一个异常。
    因为connect()方法是阻塞调用,这个连接过程始终应该在独立与主Activity线程之外的线程中被执行。
    注意:在调用connect()方法时,应该始终确保设备没有正在执行设备发现操作。如果是在发现操作的过程中,那么连接尝试会明显的变慢,并且更像是要失败的样子。
    示例
    以下是初始化蓝牙连接线程的一个基本的例子:
public class ConnectThread extends Thread{
private BluetoothDevice mDevice;
private BluetoothSocket mSocket;
public ConnectThread(BluetoothDevice device){
BluetoothSocket temp = null;
mDevice = device;
try {
temp = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(getPackageName()));
} catch (IOException e) {
e.printStackTrace();
}
mSocket = temp;
}

@Override
public void run() {
super.run();
//取消搜索因为搜索会让连接变慢
mController.getmAdapter().cancelDiscovery();
try {
//通过socket连接设备,这是一个阻塞操作,知道连接成功或发生异常
mSocket.connect();
} catch (IOException e) {
//无法连接,关闭socket并且退出
try {
mSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
//管理连接(在独立的线程)
// manageConnectedSocket(mmSocket);
}
/**
* 取消正在进行的链接,关闭socket
*/
public void cancel() {
try {
mSocket.close();
} catch (IOException e) { }
}
}

在建立连接之前要调用cancelDiscovery()方法。在连接之前应该始终调用这个方法,并且不用实际的检查蓝牙发现处理是否正在运行也是安全的(如果想要检查,调用isDiscovering()方法)。
manageConnectedSocket()方法是一个应用程序中自定义的方法,它会初始化传输数据的线程。
当使用完BluetoothSocket对象时,要始终调用close()方法来进行清理工作。这样做会立即关闭被连接的套接字,并清理所有的内部资源。

好了,这篇文章就到这里。大家仔细理解体会。下面给出源码的下载地址
​​客户端代码​​
​服务器端代码​​