Android经典蓝牙开发

1,947 阅读13分钟

前言

即将毕业,前往公司主要是负责蓝牙模块的开发,为了能更快在公司上手,阅读了官方文档以及博客,自己理解总结了一些蓝牙的相关基础知识,方便后续查阅。本文章着重是在于经典蓝牙的开发,对于低功耗蓝牙BLE的开发,我会接下来写一篇专门的总结文章。

蓝牙起源

蓝牙(Bluetooth)一词最早出现是在第十世纪的一位丹麦国王Harald BlatandBlatand 在英文里的意思可以被解释为 Bluetooth( 蓝牙 )。据说该国王酷爱吃蓝莓而导致有一个牙齿是蓝色,而被称呼为蓝牙王。

而将蓝牙与后来的无线通讯技术标准关联在一起的,是一位来自英特尔的工程师Jim Kardach。 哈拉尔国王以统一了因宗教战争和领土争议而分裂的挪威与丹麦而闻名于世,国王的成就与Jim Kardach的理念不谋而合,他希望蓝牙也可以成为统一的通用传输标准——将所有分散的设备与内容互联互通,故而在一次无限通讯行业上,提议将Bluetooth作为无线通讯技术标准的名称。

经典蓝牙开发

经典蓝牙是电池量更大操作的正确选择,其中包括设备之间的流式传输和通信。接下来主要介绍如何使用蓝牙API来完成以下几点:

  • 设置蓝牙
  • 查找已配对或本地可用的设备
  • 连接设备
  • 在设备之间传输数据

关键类介绍

  • BluetoothAdapter:蓝牙适配器,是进行所有蓝牙交互的切入点。通过该对象可以发现蓝牙设备、查询已绑定设备列表、使用mac地址实例化BluetoothDevice对象以及创建BluetoothServerSocket监听其他设备的通信。
  • BluetoothDevice:蓝牙设备,利用其可以通过BluetoothSocket去和远程设备请求连接以及获取设备的name、class、address、bonding state。
  • BluetoothSocket:蓝牙套接字,类似于TCP的socket,通过该对象,可以使得应用app通过inputStream和outputStream来和另一个蓝牙设备交换数据。
  • BluetoothServerSocket: 蓝牙服务器套接字,类似于TCP的server socket,监听来到的请求。为了两台设备可以进行连接,一个设备必须使用该类创建一个server socket,当远程蓝牙设备连接该设备的时候,该设备接受连接并且会返回一个连接的BluetoothSocket
  • BluetoothClass:描述蓝牙设备的一般特性和功能。这是一组只读的特性,用来定义该设备的类和服务。尽管其告知了关于蓝牙设备类型的有用提示,但是不代表描述了蓝牙设备支持的所有配置以及服务。
  • BluetoothProfile:蓝牙配置文件的接口。蓝牙配置文件是针对设备之间基于蓝牙通信的无线接口规范。例如免提配置文件。
  • BluetoothHeadSet:是BluetoothProfile的实现类,提供与移动设备的蓝牙耳机支持。包括蓝牙耳机以及免提(v1.5)配置文件。
  • BluetoothA2dp:是BluetoothProfile的实现类,定义了高质量音频如何通过蓝牙连接,流式传输从一个设备到另一个设备,涉及高级音频分发配置文件A2DP。
  • BluetoothHealth:是BluetoothProfile的实现类,控制蓝牙服务的健康设备配置文件代理。
  • BluetoothHealthCallback:BluetoothHealth的抽象回调类,你需要继承该类实现抽象方法来获取关于应用注册状态和蓝牙状态改变的更新。
  • BluetoothHealthAppConfiguration:第三方蓝牙健康设备注册配置,以便于于远程蓝牙健康设备通信。
  • BluetoothProfile.ServiceListener:在 BluetoothProfile(IPC)客户端从运行特定配置文件的内部服务,连接或断开连接时向其发送通知的接口。

一、权限声明

进行蓝牙开发之前,我们需要声明权限。

特别注意:位置权限需要动态申请!!!

//使用蓝牙必须声明的权限,您需要此权限才能执行任何蓝牙通信,例如请求连接、接受连接和传输数据等
<uses-permission android:name="android.permission.BLUETOOTH"/>

//需让应用启动设备或者操纵蓝牙设置,必须再声明下面的权限
//大多数应用只是需利用此权限发现本地蓝牙设备。除非应用是根据用户请求修改蓝牙设置的“超级管理员”
//否则不应使用此权限所授予的其他功能。
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

//您的应用需要此权限,因为蓝牙扫描可用于收集用户的位置信息。此类信息可能来自用户自己的设备,以及在商店和交通设施等位置使用的蓝牙信标。
//如果您的应用适配 Android 9(API 级别 28)或更低版本,则您可以声明 `ACCESS_COARSE_LOCATION` 权限而非 `ACCESS_FINE_LOCATION` 权限。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

二、设置蓝牙

包括检测设备是否支持蓝牙、蓝牙开启以及广播监听蓝牙状态的改变。

是否支持蓝牙

通过获取BluetoothAdapter对象,如果该对象为空的话,则设备不支持蓝牙,反之则支持。

//方法一获取对象,通过静态方法获取,但是最新版本已经弃用。
//BluetoothAdapter mAdapter = BluetoothAdapter.getDefaultAdapter();

//最新方法,先获取BluetoothManager对象,在根据其获取BluetoothAdapter对象
BluetoothManager mManager = getSystemService(BluetoothManager.class);
BluetoothAdapter mAdapter = mManager.getAdapter();

if(mAdapter == null){
    //设备不支持蓝牙
}

蓝牙开启

如果设备支持蓝牙之后,根据mAdapter.isEnabled()判断蓝牙是否处于不可用状态,如果不可用我们可以通过Intent请求用户开启蓝牙,此时应用会弹出窗口,询问用户是否允许开启蓝牙。接着在onActivityResult获取用户的结果,来进行下一步的判断。如果返回结果是RESULT_OK则是用户同意开启蓝牙,如果是RESULT_CANCELED则是不同意。

5CA685FC112CFFDDFDA39069DD5CECC6.jpg

if(!mAdapter.isEnabled()){
    Intent openBluetoothIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(openBluetoothIntent,1);
}

...

protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode){
        case 1:{
            if(resultCode==RESULT_OK){
                Toast.makeText(this,"蓝牙已经打开",Toast.LENGTH_SHORT).show();
            }else {
                Toast.makeText(this,"拒绝打开蓝牙",Toast.LENGTH_SHORT).show();
            }
            break;
        }
        case 2:
            break;
        default:
    }
}

监听蓝牙状态改变

每当蓝牙状态改变的时候,系统会广播一条ACTION_STATE_CHANGED的intent,我们可以监听该广播来做相应的操作。此广播会包含两个额外字段EXTRA_STATEEXTRA_PREVIOUS_STATE,分别代表蓝牙的新旧状态,其对应的值有STATE_TURNING_ONSTATE_ONSTATE_TURNING_OFFSTATE_OFF

private final BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)){
         switch (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,-1)){
            case BluetoothAdapter.STATE_ON:
                Toast.makeText(MainActivity.this,"BluetoothAdapter.STATE_ON",Toast.LENGTH_SHORT).show();
                break;
            case BluetoothAdapter.STATE_OFF:
                Toast.makeText(MainActivity.this,"BluetoothAdapter.STATE_OFF",Toast.LENGTH_SHORT).show();
                break;
            case BluetoothAdapter.STATE_TURNING_ON:
                Toast.makeText(MainActivity.this,"BluetoothAdapter.STATE_TURNING_ON",Toast.LENGTH_SHORT).show();
                break;
            case BluetoothAdapter.STATE_TURNING_OFF:
                Toast.makeText(MainActivity.this,"BluetoothAdapter.STATE_TURNING_OFF",Toast.LENGTH_SHORT).show();
                break;
            default:
        }
    
   }
};
...

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(receiver,intentFilter);

三、查找设备

设备发现是一个扫描过程,它会搜索局部区域内已启用蓝牙功能的设备,并请求与每台设备相关的某些信息。此过程有时也被称为发现查询扫描。但是,只有在当下接受信息请求时,附近区域的蓝牙设备才会通过启用可检测性响应发现请求。如果设备已启用可检测性,它会通过共享一些信息(例如设备的名称、类及其唯一的 MAC 地址)来响应发现请求。借助此类信息,执行发现过程的设备可选择发起对已检测到设备的连接。

在首次与远程设备建立连接后,系统会自动向用户显示配对请求。当设备完成配对后,系统会保存关于该设备的基本信息(例如设备的名称、类和 MAC 地址),并且可使用 Bluetooth API 读取这些信息。借助远程设备的已知 MAC 地址,您可以随时向其发起连接,而无需执行发现操作(假定该设备仍处于有效范围内)。

请注意,被配对与被连接之间存在区别:

  • 被配对是指两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
  • 被连接是指设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求规定,只有先对设备进行配对,然后才能建立 RFCOMM 连接。在使用 Bluetooth API 发起加密连接时,系统会自动执行配对。

查询已经配对设备

通过BluetoothAdapter对象来调用getBondedDevices()来获取已配对设备,此方法会返回一组BluetoothDevice对象,利用该对象可以获取设备的名称以及Mac地址。

Set<BluetoothDevice> devices = mBluetoothAdapter.getBondedDevices();
for(device : devices){
   String deviceName = device.getName();       //设备名称
   String deviceAddress = device.getAddress(); //设备Mac地址
}

发现设备

开始发现设备涉及了三个蓝牙API。

  • mAdapter.startDiscovery()开始发现设备,该进程为异步操作,并且会返回一个布尔值,指示发现进程是否已成功启动。发现进程通常包含约 12 秒钟的查询扫描,随后会对发现的每台设备进行页面扫描,以检索其蓝牙名称。
  • mAdapter.cancelDiscovery()取消发现设备,执行设备发现将消耗蓝牙适配器的大量资源。在找到要连接的设备后,请务必使用 cancelDiscovery() 停止发现,然后再尝试连接。
  • mAdapter.isDiscovering()是否正在执行发现设备。 在调用开始发现设备之后,接着需要通过广播接收器监听BluetoothDevice.ACTION_FOUND的intent,该intent包含两个额外的字段EXTRA_DEVICEEXTRA_CLASS,前者包含BluetoothDevice信息,后者包含BluetoothClass信息。
private final BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)){
         ...
        }else if(action.equals(BluetoothDevice.ACTION_FOUND)){
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        
         ...
        }

       
    
   }
};
...

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//监听蓝牙状态改变广播
intentFilter.addActin(BluetoothDevice.ACTION_FOUND); //监听发现设备广播
registerReceiver(receiver,intentFilter);

启用设备可见性

当我们希望我们自己的设备能被别的设备检测到的时候,我们应该开启设备的可检测性。通过下面intent代码指定我们的可见时间,并且会在onActivityResult中收到用户的允许情况,其结果代码等于设备可检测到的持续时间。如果用户响应“No”或出现错误,则结果代码为 RESULT_CANCELED

微信图片_20220512211429.jpg

Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DUTATION,10);//10s可见
startActivityForResult(intent,1)

...

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
   super.onActivityResult(requestCode, resultCode, data);
    if(requestCode==1){
        if(resultCode != RESULT_CANCELED){
            Toast.makeText(this,"允许可见"+resultCode+"s",Toast.LENGTH_SHORT).show();
        }else{
            Toast.makeText(this,"不允许可见",Toast.LENGTH_SHORT).show();
        }
    }
}

我们还可以通过广播监听ACTION_SCAN_MODE_CHANGED的intent来检测可见性的改变。该intent有两个额外的字段,分别是EXTRA_SCAN_MODEEXTRA_PREVIOUS_SCAN_MODE。两者都有以下三种可能的值:

  • SCAN_MODE_CONNECTABLE_DISCOVERABLE:设备处于可检测到模式.
  • SCAN_MODE_CONNECTABLE:设备未处于可检测到模式,但仍能收到连接。
  • SCAN_MODE_NONE:设备未处于可检测到模式,且无法收到连接。 如果您要发起对远程设备的连接,则无需启用设备可检测性。只有当您希望应用对接受传入连接的服务器套接字进行托管时,才有必要启用可检测性,因为在发起对其他设备的连接之前,远程设备必须能够发现这些设备。

四、连接设备

如要在两台设备之间创建连接,您必须同时实现服务器端和客户端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的 MAC 地址发起连接。服务器设备和客户端设备均会以不同方法获得所需的 BluetoothSocket。当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,即可将二者视为彼此连接。这种情况下,每台设备都能获得输入和输出流式传输,并开始传输数据。

作为服务器连接

一般为以下三个步骤:

  1. 通过listenUsingRfcommWithServiceRecord(String,UUID)来获取BluetoothServerSocket对象。字符串String是代表服务的可识别名称,可以是应用名称。UUID是通用唯一标识符,这是客户端连接协议的基础。当客户端尝试连接此设备时,它会携带 UUID,从而对其想要连接的服务进行唯一标识。为了让服务器接受连接,这些 UUID 必须互相匹配。UUID可以在网上随机生成,由于数量庞大,重复的概率几乎为无,接着用fromString(String)来初始化一个UUID。
  2. 通过accept()来监听请求。这是一个阻塞调用,故而应在子线程中调用。当服务器接受连接或异常发生时,该调用便会返回。只有当远程设备发送包含 UUID 的连接请求,并且该 UUID 与使用此侦听服务器套接字注册的 UUID 相匹配时,服务器才会接受连接。连接成功后,accept() 将返回已连接的 BluetoothSocket
  3. 当我们连接成功的时候,及时调用close(),释放服务器套接字及其所有资源,但不会关闭 accept() 所返回的已连接的 BluetoothSocket
public class ServerThread extends Thread {

    private final BluetoothServerSocket mServerSocket;
    private static final String TAG = "ServerThread";

    public ServerThread(BluetoothAdapter bluetoothAdapter) {
        BluetoothServerSocket socket = null;
        try {
            socket = bluetoothAdapter.listenUsingRfcommWithServiceRecord("BluetoothApp", UUID.fromString("46FF4956-DFA8-B581-6BC2-613BEF4BAF24"));
        } catch (IOException e) {
            Log.e(TAG, "Socket监听出错", e);
        }
        mServerSocket = socket;
    }
    
    @Override
    public void run() {
        while (true) {
            BluetoothSocket socket = null;
            try {
                socket = mServerSocket.accept();
            } catch (IOException e) {
                e.printStackTrace();
                break;
            }
            if (socket != null) {
                try {
                    managerInfo(socket);
                    mServerSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }


        }
    }

    public void cancel() {
        try {
            mServerSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }


    }

    public void managerInfo(BluetoothSocket socket) {
    }
}

作为客户端连接

一般为以下两个步骤:

  1. 使用BluetoothDevice,通过createRfcommSocketToServiceRecord(UUID)方法获取BluetoothSocket对象。此方法会初始化 BluetoothSocket 对象,以便客户端连接至 BluetoothDevice
  2. 调用connect()方法发起连接,为阻塞调用。 请注意,代码在尝试连接之前先调用了 cancelDiscovery()。您应始终在 connect() 之前调用 cancelDiscovery(),这是因为无论当前是否正在执行设备发现,cancelDiscovery() 都会成功。请注意,此段代码在尝试连接之前先调用了 cancelDiscovery()。您应始终在 connect() 之前调用 cancelDiscovery(),这是因为无论当前是否正在执行设备发现,cancelDiscovery() 都会成功。
public class ClientThread extends Thread{
    private final BluetoothSocket mSocket;
    private final BluetoothDevice mDevice;
    private final BluetoothAdapter mAdapter;
    private static final String TAG = "ClientThread";
    public ClientThread(BluetoothAdapter bluetoothAdapter, BluetoothDevice device){
        mAdapter = bluetoothAdapter;
        mDevice = device;
        BluetoothSocket socket = null;
        try {
            socket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString("46FF4956-DFA8-B581-6BC2-613BEF4BAF24"));
        } catch (IOException e) {
            Log.e(TAG, "socket创建失败");
        }
        mSocket = socket;
    }
    @Override
    public void run() {
        mAdapter.cancelDiscovery();
        try {
            mSocket.connect();
        } catch (IOException e) {
            Log.e(TAG, "socket连接失败" );
            try {
                mSocket.close();
            } catch (IOException ioException) {
                Log.e(TAG, "socket关闭失败" );
            }
            return;
        }
        manageSocket(mSocket);

    }
    public void cancel(){
        try {
            mSocket.close();
        } catch (IOException e) {
            Log.e(TAG, "socket关闭失败" );
        }
    }

    public void manageSocket(BluetoothSocket mSocket) {
    }
}

五、数据传输

当我们设备连接成功的时候,双方设备根据BluetoothSocket对象,通过getInputStream()getOutputStream()分别获取通过套接字处理数据传输的 InputStream 和 OutputStream。 接着使用 read(byte[]) 和 write(byte[]) 读取数据以及将其写入数据流。 特别注意这两个方法是阻塞的,需要在线程中来进行流的读取及输出。

...
       InputStream mInputStream;
       OutputStream mOutputStream;
       
     //该方法在连接中有写到,是在线程中
    public void manageSocket(BluetoothSocket mSocket) {
    
    try{
        mInputStream = mSocket.getInputStream();
        mOutputStream = mSocket.getOutputStream();
    }catch(IOException e){
       Log.e(TAG,"获取输入输出流失败");
    }
    
    //一直监听数据流,由于在子线程,故而通过Handler发送给主线程。
    if(inputStream!=null){
        byte[] bytes = new byte[1024];
        int num;
        while(true){
        
            try{
                num = inputStream.read(bytes);
                
                //mHandler主程序中传过来
                //到时候接受数据通过
                // byte[] bytes = message.obj;String text = new String(bytes,0,message.arg1);
                Message message = mHandler.obtainMessage(
                    1,  // 1是message.what,代表消息处理类型
                    num, // 代表字节数组长度
                    -1, //随便参数
                    bytes //字节数组
                );
                messsage.sendToTarget();
            }catch(IOException e){
               Log.e(TAG, "输入流断开连接", e);
               break;

            }
        }
    
    }
}

    public void write(String text){
        try{
            byte[] bytes = text.getbytes();
            mOutputStream.write(bytes);
            Message message = mHandler.obtainMessage(
                    1,  // 1是message.what,代表消息处理类型
                    -1, // 代表字节数组长度
                    -1, //随便参数
                    bytes //字节数组
                );
                messsage.sendToTarget();
        }catch(IOException e){
                 Message writeErrorMsg =
                        mHandler.obtainMessage(2);
                Bundle bundle = new Bundle();
                bundle.putString("toast",
                        "不能发信息给ui线程,出错");
                writeErrorMsg.setData(bundle);
                mHandler.sendMessage(writeErrorMsg);

        }
        
        
    }
    

...

总结

到这里为止,基础的蓝牙知识已经详细写完了,如果有问题,欢迎大家留言评论。