经典蓝牙实现Android手机之间蓝牙通信,记一次技术预研_1

1,073 阅读6分钟

背景

某天直属领导找到我,要我和iOS开发一起看一下蓝牙通信,后续可能要在2台设备的2个App之间实现消息的传递。大致上就是把工行转账用的那个U盾给换成一个手机。

阅读所需最小必要知识

  1. 了解广播的动态注册、监听
  2. 了解申请权限
  3. 了解Handler的消息发送、接收
  4. 了解UUID可以随机生成和从字符串转化

技术分歧

和我搭档的iOS开发曾经具有嵌入式设备开发经验,他告诉我说“这个需求做不了”,要设备的Mac地址还有好几个UUID,这些都是硬件给出的,但是在手机上因为隐私保护拿不到。 不需要硬件给出,明明是自己约定的。

而我搜索了一下,蓝牙分为经典蓝牙和低功耗蓝牙(BLE)这两种,低功耗蓝牙才是需要Mac地址和好几个UUID的,经典蓝牙只需要双方约定好同一个UUID,一个等待连接,一个主动连接即可。因此确定了后续的搜索方向为经典蓝牙。

勘误:iOS设备不支持经典蓝牙,使用低功耗蓝牙BLE也可以实现手机间的通信,请参考这篇BLE蓝牙完成手机之间的通信,记一次技术预研_2 - 掘金 (juejin.cn)

实现过程

  1. 配对
  2. 连接
  3. 发送消息
  4. 解除连接
  5. 解除配对(可选)

image.png

配对和解除配对

手机设置配对或者代码中配对,任选其一即可。我这次只是个演示demo,就使用了手机设置中配对。

手机设置配对: 2个手机均打开设置-蓝牙,找到对方后点击配对即可,略

代码中配对:

涉及反射,搜索来的,我没有验证过,请按需选用

配对代码,解除配对把createBond换成removeBond即可

Method createBondMethod = btClass.getMethod("createBond");
Boolean returnValue = (Boolean) createBondMethod.invoke(btDevice);
return returnValue.booleanValue();

连接

在经典蓝牙的体系中,设备分为服务端和客户端,双方在这个demo场景下并无高下之分,只是用的API不一样。注意这里的服务端并不是说服务器接口。

两端都要申请权限,大概是因为蓝牙可以用来辅助定位?所以需要定位权限?:

private static final String[] Permission = {
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION,
};

别忘了Manifest文件:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!--    如果需要app在没有硬件时不能安装,就把false改成true-->
<uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="false" />

拿到权限之后检查蓝牙硬件、蓝牙和GPS开关:

public class BlueToothHelper {
    private static final String TAG = "BlueToothHelper";
    public static boolean checkHasBlueToothHardware(Context context) {
        if (context == null) {
            return false;
        }
        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
    }

    public static boolean checkAll(Context context) {
        if (context == null) {
            return false;
        }

        if (!enable(context)) {
            Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)未打开手机蓝牙,蓝牙功能无法使用......");
            Toast.makeText(context, "未打开手机蓝牙,蓝牙功能无法使用...", Toast.LENGTH_LONG).show();
            return false;
        }
        if (!isOPenGps(context)) {
            Log.e(TAG, "BleHelp初始化失败:" + "(用户操作)GPS未打开,蓝牙功能无法使用...");
            Toast.makeText(context, "GPS未打开,蓝牙功能无法使用", Toast.LENGTH_LONG).show();
            return false;
        }
        return true;
    }

    /**
     * 打开手机蓝牙
     *
     * @return true 表示打开成功
     */
    public static boolean enable(Context context) {
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH) != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
        if (!getBluetoothAdapter().isEnabled()) {
            //若未打开手机蓝牙,则会弹出一个系统的是否打开/关闭蓝牙的对话框,禁止或者未处理返回false,允许返回true
            //若已打开手机蓝牙,直接返回true
            if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
            boolean enableState = getBluetoothAdapter().enable();
            Log.d(TAG, "(用户操作)手机蓝牙是否打开成功:" + enableState);
            return enableState;
        } else {
            return true;
        }
    }

    /**
     * 判断GPS是否开启,GPS或者AGPS开启一个就认为是开启的
     *
     * @return true 表示开启
     */
    public static boolean isOPenGps(Context context) {
        LocationManager locationManager
                = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
        // 通过GPS卫星定位,定位级别可以精确到街(通过24颗卫星定位,在室外和空旷的地方定位准确、速度快)
        boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
        // 通过WLAN或移动网络(3G/2G)确定的位置(也称作AGPS,辅助GPS定位。主要用于在室内或遮盖物(建筑群或茂密的深林等)密集的地方定位)
        boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
        if (gps || network) {
            Log.d(TAG, "GPS状态:打开");
            return true;
        }
        Log.e(TAG, "GPS状态:关闭");
        return false;
    }

    public static BluetoothAdapter getBluetoothAdapter() {
        return BluetoothAdapter.getDefaultAdapter();
    }
}

服务端

检查过权限和设备开关之后,服务端就可以单独开一个线程,等待设备连接、连接完成之后通过inputStream读取数据(也可以使用outputStream写数据给客户端)

不需要读写数据的时候记得把线程关闭。

public class AcceptThread extends Thread {
      private BluetoothServerSocket serverSocket;
      private BluetoothSocket socket;
      private InputStream is;
      private OutputStream os;

      private boolean needStop = false;

      private final UUID blueToothUUID = UUID.fromString("这个UUID要使用和客户端一致的");

      public static final int MSG_WHAT_GET_DATA = 1;
      private Handler handler;

      public AcceptThread(Handler handler, BluetoothAdapter bluetoothAdapter) {
         this.handler = handler;
         //创建BluetoothServerSocket对象
         try {
            serverSocket = bluetoothAdapter.listenUsingInsecureRfcommWithServiceRecord("name", blueToothUUID);
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
      @Override
      public void run() {
         //等待接受蓝牙客户端的请求
         Log.d("AcceptThread", "线程开始");
         try {
            socket = serverSocket.accept();
            is = socket.getInputStream();
            os = socket.getOutputStream();
            while (!needStop) {
               byte[] buffer = new byte[128];
               int count = is.read(buffer);
               Message message = new Message();
               message.what = MSG_WHAT_GET_DATA;
               message.obj = new String(buffer, 0, count, "utf-8");
               if (handler != null) {
                  handler.sendMessage(message);
               }
            }
         } catch (IOException e) {
            e.printStackTrace();
         }
         Log.d("AcceptThread", "线程结束");
      }

      public void stopThread() {
         needStop = true;
         try {
            if (serverSocket != null) {
               serverSocket.close();
            }
         } catch (IOException e) {
            e.printStackTrace();
         }
      }
 
}

线程之外,Activity收到Handler发送来的消息之后,就可以做展示或者别的处理了。

客户端

一样是先申请权限、检查开关,全部就绪之后搜索设备:

这2个方法是客户端的BlueToothHelper的静态方法,getBluetoothAdapter()已在服务端的方法中出现过了,不再重复贴代码

public static void startDiscovery() {
    getBluetoothAdapter().startDiscovery();
}

public static void stopDiscovery() {
    getBluetoothAdapter().cancelDiscovery();
}

搜索设备之前先注册广播监听,然后在广播监听中,搜到了设备,进行连接。代码中的changeStatus只是一个修改TextView文字的方法,无需理会。

deviceName是要连接的设备名称;

first只是控制是否首次连接的变量,因为会重复发现设备,重复连接同一台设备会直接抛异常导致连接失败。

这里先是填写设备名称再搜索并连接的,真实需求中常用的方式是先搜索设备,展示到一个列表中,然后选择一台设备进行连接。

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            Log.i(TAG, "发现设备" + device.getName() + " " + device.getAddress());
            if (TextUtils.equals(deviceName, device.getName()) && first) {
                first = false;
                BlueToothHelper.stopDiscovery();
                connectDevice(device);
            }
        } else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
            Log.d(TAG, "开始搜索");
        } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
            Log.d(TAG, "搜索完毕");
        }
    }
};
private void startDiscovery() {
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
    registerReceiver(mReceiver, filter);
    changeStatus("正在搜索设备");
    BlueToothHelper.startDiscovery();
}
private void connectDevice(BluetoothDevice device) {
    if (isConnected) {
        return;
    }
    synchronized (this) {
        if (isConnected) {
            return;
        }
        Log.d(TAG, "connectDevice, 开始连接设备");
        new Thread() {
            @Override
            public void run() {
                try {
                    Log.d(TAG, "connectDevice, 进入线程");
                    BluetoothSocket bluetoothSocket =
                            device.createRfcommSocketToServiceRecord(UUID.fromString(UUID_BLUE_TOOTH_SERVICE));
                    bluetoothSocket.connect();
                    BlueToothCommunicationActivity.this.bluetoothSocket = bluetoothSocket;
                    isConnected = true;
                    changeStatus("设备已连接");
                } catch (IOException e) {
                    e.printStackTrace();
                    isConnected = false;
                    changeStatus("设备连接出错,请重试");
                }
                Log.d(TAG, "connectDevice, 退出线程");
            }
        }.start();
    }
}

发送消息

客户端使用如下代码发送消息:

btSend.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (!isConnected) {
            return;
        }
        String content = etContent.getText().toString();
        try {
            bluetoothSocket.getOutputStream().write(content.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

服务端在之前那个线程中接收消息。

解除连接

  • 服务端使用stopThread()方法,断开连接并且停止线程。
  • 客户端使用serverSocket.close()断开连接。

解除配对(可选)

在设置中解除配对,或者在两端使用代码反射解除配对。

踩的坑

  1. 发现设备时会每台设备收到2个发现广播,不经处理直接连接设备会导致使用connect()方法直接抛异常。

编辑历史

  1. 2024年2月1日:补上遗漏的Manifest文件内容
  2. 2024年2月29日:关于BLE的勘误,并给出BLE的文章地址