Android 使用经典蓝牙

本文内容基本是按照安卓官方文档来进行经典蓝牙的学习,大体都从官方文档粘贴而来,英文部分做了写粗略的翻译,作为自己学习安卓经典蓝牙的一个记录。官方文档可参考https://developer.android.com/guide/topics/connectivity/bluetooth.html

基础知识

Android Bluetooth基础类、API,包含在android.bluetooth包下

  • BluetoothAdaper 表示本地蓝牙适配器,是所有蓝牙交互的入口点, BluetoothAdapter 是所有蓝牙交互的入口点。 利用它可以发现其他蓝牙设备,查询绑定(配对)设备的列表,使用已知的 MAC 地址实例化 BluetoothDevice,以及创建 BluetoothServerSocket 以侦听来自其他设备的通信。
  • BluetoothDevice 表示远程蓝牙设备。利用它可以通过 BluetoothSocket 请求与某个远程设备建立连接,或查询有关该设备的信息,例如设备的名称、地址、类和绑定状态等。
  • BluetoothSocket 表示蓝牙套接字接口(与 TCP Socket 相似)。这是允许应用通过 InputStream 和 OutputStream 与其他蓝牙设备交换数据的连接点。
  • BluetoothServerSocket 表示用于侦听传入请求的开放服务器套接字(类似于 TCP ServerSocket)。 要连接两台 Android 设备,其中一台设备必须使用此类开放一个服务器套接字。 当一台远程蓝牙设备向此设备发出连接请求时, BluetoothServerSocket 将会在接受连接后返回已连接的 BluetoothSocket
  • BluetoothClass 描述蓝牙设备的一般特征和功能。 这是一组只读属性,用于定义设备的主要和次要设备类及其服务。 不过,它不能可靠地描述设备支持的所有蓝牙配置文件和服务,而是适合作为设备类型提示
  • BluetoothProfile 表示蓝牙配置文件的接口。 蓝牙配置文件是适用于设备间蓝牙通信的无线接口规范。 免提配置文件便是一个示例。 如需了解有关配置文件的详细讨论,请参阅使用配置文件
使用蓝牙
获取蓝牙权限
<uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
设置蓝牙
  • 获取BluetoothAdapter
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // 若为Null,则说明不支持蓝牙,在这里做出相应处理
}
  • 启用蓝牙
if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

若蓝牙为启用,则将显示对话框,请求用户允许启用蓝牙

查找蓝牙
  • 查询配对的设备:在执行设备发现之前,有必要查询已配对的设备及,故使用getBondedDevices()方法,其返回一组已配对的BluetoothDevice
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
// 如果有配对的设备
if (pairedDevices.size() > 0) {
    // 遍历匹配设备组
    for (BluetoothDevice device : pairedDevices) {
        // 将已配对设备的名称及地址存储在一个ArrayAdapter中
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}
  • 发现设备:要开始发现设备,只需调用 startDiscovery()。该进程为异步进程,并且该方法会立即返回一个布尔值,指示是否已成功启动发现操作。 发现进程通常包含约 12 秒钟的查询扫描,之后对每台发现的设备进行页面扫描,以检索其蓝牙名称。您的应用必须针对 ACTION_FOUND Intent 注册一个 BroadcastReceiver,以便接收每台发现的设备的相关信息。 针对每台设备,系统将会广播 ACTION_FOUNDIntent。此 Intent 将携带额外字段 EXTRA_DEVICEEXTRA_CLASS,二者分别包含 BluetoothDeviceBluetoothClass。 例如,下面说明了在发现设备时如何注册以处理广播。
// 为 ACTION_FOUND 创建一个广播接收器
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        //当发现一个蓝牙设备时
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            //利用EXTRA_DEVICE字段, 从intent中获取该设备实例
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // 使用getName和getAddress方法获取蓝牙设备名和地址
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
};
//注册该广播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
//搜索完成
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, filter); // 要在onDestry方法中取消注册该广播
//取消注册该广播
@Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mBtFoundReceiver);
    }
  • 启用本地可检测性(可选)
  • 如果您希望将本地设备设为可被其他设备检测到,请使用ACTION_REQUEST_DISCOVERABLE 操作 Intent 调用 startActivityForResult(Intent, int)。 这将通过系统设置发出启用可检测到模式的请求(无需停止您的应用)。 默认情况下,设备将变为可检测到并持续 120 秒钟。 您可以通过添加 EXTRA_DISCOVERABLE_DURATION Intent Extra 来定义不同的持续时间。 应用可以设置的最大持续时间为 3600 秒,值为 0 则表示设备始终可检测到。 任何小于 0 或大于 3600 的值都会自动设为 120 秒。
//设置持续时间为300秒
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);

上述操作将显示对话框,请求用户允许设备设为可检测到,若同意,则您的 Activity 将会收到对 onActivityResult()) 回调的调用,其结果代码等于设备可检测到的持续时间。 如果用户响应“No”或出现错误,结果代码将为 RESULT_CANCELED如果设备尚未启用蓝牙,则在启用设备可检测性将会自动启用蓝牙。仅当您希望您的应用托管将用于接受传入连接的服务器套接字时,才有必要启用可检测性,因为远程设备必须能够发现该设备,然后才能发起连接。

连接设备

要在两台设备上的应用之间创建连接,必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须发起连接(使用服务器设备的 MAC 地址发起连接)。 当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,二者将被视为彼此连接。 这种情况下,每台设备都能获得输入和输出流式传输,并且可以开始传输数据,在有关管理连接的部分将会讨论这一主题。 本部分介绍如何在两台设备之间发起连接。

连接为服务器

当您需要连接两台设备时,其中一台设备必须通过保持开放的 BluetoothServerSocket 来充当服务器。 服务器套接字的用途是侦听传入的连接请求,并在接受一个请求后提供已连接的 BluetoothSocket。 从 BluetoothServerSocket 获取 BluetoothSocket 后,可以(并且应该)舍弃 BluetoothServerSocket,除非您需要接受更多连接。

  • 基本流程
  • 通过调用 listenUsingRfcommWithServiceRecord(String, UUID) 获取 BluetoothServerSocket
  • UUID: 通用唯一标识符,是用于唯一标识信息的字符串ID的128为标准化格式
  • UUID将作为与客户端设备连接协议的基础,即当客户端尝试连接此设备时,他会携带一个想要连接的服务的UUID,两个UUID必须匹配
  • 调用accept()开始侦听连接请求,操作成功,accept将返回已连接的BluetoothSocket
  • 调用 close()。这将释放服务器套接字及其所有资源,但不会关闭 accept() 所返回的已连接的 BluetoothSocket
  • 注意:accept()不应再主Activity UI线程中执行,因为它是阻塞调用。应开启一个新线程,使用 BluetoothServerSocketBluetoothSocket 完成所有工作。要终止 accept() 等被阻塞的调用,请通过另一个线程在 BluetoothServerSocket(或 BluetoothSocket)上调用 close(),被阻塞的调用将会立即返回
//一个用于接受传入连接的服务器组件的简化线程
private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // 使用一个临时变量,之后再将它赋值给mmServerSocket,因为mmServerSocket是一个final变量
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID是此应用的UUID串,同时也被客户端使用
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        //赋值
        mmServerSocket = tmp;
    }

    public void run() {
        BluetoothSocket socket = null;
        // 持续侦听直到返回了一个socket或者发生异常
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // 连接建立成功
            if (socket != null) {
                // 在一个单独的线程中管理连接,manageConnectedSocket为虚构的方法,它将启动用于传输数据的线程
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    /** 将取消侦听socket,并使进程关闭 */
    public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}
连接为客户端

要发起与远程设备(保持开放的服务器套接字的设备)的连接,必须首先获取表示该远程设备的 BluetoothDevice 对象。(在前面有关查找设备的部分介绍了如何获取 BluetoothDevice)。 然后必须使用 BluetoothDevice 来获取 BluetoothSocket 并发起连接。

基本过程

  • 使用 BluetoothDevice,通过调用 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket。此处使用的UUID必须与服务器设备在使用 listenUsingRfcommWithServiceRecord(String, UUID) 开放其BluetoothServerSocket 时所用的 UUID 相匹配。 要使用相同的 UUID,只需将该 UUID 字符串以硬编码方式编入应用,然后通过服务器代码和客户端代码引用该字符串。
  • 通过connect()发起连接。执行此调用时,系统将会在远程设备上执行 SDP 查找,以便匹配 UUID。 如果查找成功并且远程设备接受了该连接,它将共享 RFCOMM 通道以便在连接期间使用,并且 connect() 将会返回。 此方法为阻塞调用。 如果由于任何原因连接失败或 connect() 方法超时(大约 12 秒之后),它将会引发异常。
    由于 connect() 为阻塞调用,因此该连接过程应始终在主 Activity 线程以外的线程中执行
    注意:注:在调用 connect() 时,应始终确保设备未在执行设备发现。 如果正在进行发现操作,则会大幅降低连接尝试的速度,并增加连接失败的可能性。
//发起蓝牙连接的线程的基本示例
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) { }
    }
}
管理连接
  • 获取 InputStreamOutputStream,二者分别通过套接字以及 getInputStream()getOutputStream() 来处理数据传输。
  • 使用 read(byte[])write(byte[]) 读取数据并写入到流式传输。
//传输内容示例
private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {
        //获取socket及输入输出流
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024];  
        int bytes; 获取read()返回的数据

        // 持续侦听输入流,知道发生异常
        while (true) {
            try {
                从输入流中读取数据
                bytes = mmInStream.read(buffer);
                // 将包含的字节流发送到UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }

    /* 在主Activity调用此方法,用以向远程设备发送数据 */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }

    /* 在主activity调用此方法用以关闭连接*/
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}