Android手机之间蓝牙通信,记一次技术预研

背景

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

阅读所需最小必要知识

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

技术分歧

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

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

实现过程

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

配对和解除配对

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

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

代码中配对:

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

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

ini 复制代码
Method createBondMethod = btClass.getMethod("createBond");
Boolean returnValue = (Boolean) createBondMethod.invoke(btDevice);
return returnValue.booleanValue();

连接

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

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

arduino 复制代码
private static final String[] Permission = {
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION,
};

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

typescript 复制代码
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写数据给客户端)

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

ini 复制代码
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()已在服务端的方法中出现过了,不再重复贴代码

scss 复制代码
public static void startDiscovery() {
    getBluetoothAdapter().startDiscovery();
}

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

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

deviceName是要连接的设备名称;

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

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

ini 复制代码
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, "搜索完毕");
        }
    }
};
scss 复制代码
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();
}
ini 复制代码
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();
    }
}

发送消息

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

scss 复制代码
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()方法直接抛异常。
相关推荐
xcl09255 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐7 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长7 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn7 小时前
Spring 注解使用详解
java·spring
一条小小yu7 小时前
Spring IoC的理解
java·后端·spring
落魄实习生8 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈8 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白9 小时前
核心功能(Service接口)
java·mybatis
乌暮10 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习