android 传统蓝牙

前言

  • 最近业务上需要用到蓝牙与硬件交互,经了解,现有分为传统蓝牙和低功耗蓝牙(BLE),本篇讲解传统蓝牙使用
  • 现在市面上蓝牙模块大多数都支持低功耗蓝牙,传统蓝牙适用于较为耗电的操作,如 Android 设备之间的流式传输和通信等,使用场景有限。
  • Android 平台包含蓝牙网络堆栈支持,此支持能让设备以无线方式与其他蓝牙设备交换数据。应用框架提供通过 Android Bluetooth API 访问蓝牙功能的权限。这些 API 允许应用以无线方式连接到其他蓝牙设备,从而实现点到点和多点无线功能。

权限

  • android 6.0 后需要申请运行时权限
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <!-- 如果targetSdkVersion<=29,可以声明ACCESS_COARSE_LOCATION代替 -->
    <!-- android 6.0后 需要申请运行时权限-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

设备是否支持蓝牙

  • BluetoothAdapter表示设备自身的蓝牙适配器,整个系统只有一个蓝牙适配器,使用此对象进行交互。
    private val mBluetoothAdapter: BluetoothAdapter? by lazy {
        BluetoothAdapter.getDefaultAdapter()
    }

    /**
     * 设备是否支持蓝牙
     */
    fun isSupportBluetooth(): Boolean {
        return mBluetoothAdapter != null
    }

蓝牙是否打开

   /**
     * 蓝牙是否打开
     */
    fun isEnabled(): Boolean {
        return mBluetoothAdapter?.isEnabled == true
    }

开启蓝牙

  • 这里使用了透明的fragment获取 onActivityResult() 返回的结果,方便处理
    /**
     * 打开蓝牙
     *
     * @param activity
     * @param callback 结果回调
     */
    fun openBluetooth(activity: FragmentActivity, callback: ((Boolean) -> Unit)? = null) {
        if (!isEnabled()) {
            val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
            if (callback != null) {
                InvisibleFragment.instance(activity).apply {
                    callResult(REQUEST_ENABLE_BT) { _, _ -> callback.invoke(isEnabled()) }
                    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
                }
            } else {
                activity.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
            }
        } else {
            Timber.tag(TAG).d("蓝牙已开启")
        }
    }

蓝牙改变监听

 	/**
     * 注册蓝牙更改监听器
     *
     * @param listener 监听
     */
    fun registerBluetoothChangeListener(listener: (Boolean) -> Unit) {
        this.mBluetoothChangeListener = listener
        val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
        getAppContext().registerReceiver(mBluetoothChangeReceiver, filter)
    }

    /**
     * 注销蓝牙更改监听器
     */
    fun unregisterBluetoothChangeListener() {
        getAppContext().unregisterReceiver(mBluetoothChangeReceiver)
    }

   private val mBluetoothChangeReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0)) {
                    BluetoothAdapter.STATE_TURNING_ON -> {
                        Timber.tag(TAG).d("蓝牙正在打开")
                    }
                    BluetoothAdapter.STATE_TURNING_OFF -> {
                        Timber.tag(TAG).d("蓝牙正在关闭")
                    }
                    BluetoothAdapter.STATE_ON -> {
                        Timber.tag(TAG).d("蓝牙已开启")
                        mBluetoothChangeListener?.invoke(true)
                    }
                    BluetoothAdapter.STATE_OFF -> {
                        Timber.tag(TAG).d("蓝牙已关闭")
                        mBluetoothChangeListener?.invoke(false)
                    }
                }
            }
        }
    }
	

获取已配对设备

  • 被配对是指两台设备知晓彼此的存在,具有可用于身份验证的共享链路密钥,并且能够与彼此建立加密连接。
  • 被连接是指设备当前共享一个 RFCOMM 通道,并且能够向彼此传输数据。当前的 Android Bluetooth API 要求规定,只有先对设备进行配对,然后才能建立 RFCOMM 连接。在使用 Bluetooth API 发起加密连接时,系统会自动执行配对。
   /**
     * 获取已配对设备
     */
    fun getPairedDevices(): MutableSet<BluetoothDevice>? {
        return mBluetoothAdapter?.bondedDevices
    }
    
	 /**
     * 可获取到对应的蓝牙设备名称和mac地址
     */
	  val list = BluetoothUtils.getPairedDevices()
              ?.map { BluetoothDeviceVo(it.name, it.address) }
              ?.toMutableList()

扫描 / 发现设备

  • 执行设备发现将消耗蓝牙适配器的大量资源。在找到要连接的设备后,使用 cancelDiscovery() 停止发现,然后再尝试连接。此外,不应在连接到设备的情况下执行设备发现,因为发现过程会大幅减少可供任何现有连接使用的带宽。
  • 发现设备,只需调用 startDiscovery()。该进程为异步操作,并且会返回一个布尔值,判断发现进程是否已成功启动。发现进程通常包含约 12 秒钟的查询扫描,随后会对发现的每台设备进行页面扫描,以检索其蓝牙名称。
  • Android 6.0后需要开启定位信息,才可以扫描蓝牙设备。

	 /**
     * 扫描/发现设备
     */
    fun scanDevice(callback: (BluetoothDevice?) -> Unit) {
        this.mScanDeviceCallback = callback
        if (mBluetoothAdapter?.startDiscovery() == true) {
            val filter = IntentFilter(BluetoothDevice.ACTION_FOUND)
            getAppContext().registerReceiver(mScanDeviceReceiver, filter)
        } else {
            Timber.tag(TAG).d("开启扫描设备失败")
        }
    }

  private val mScanDeviceReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent?) {
            if (intent?.action == BluetoothDevice.ACTION_FOUND) {
                val device: BluetoothDevice? =
                    intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
                mScanDeviceCallback?.invoke(device)
            }
        }
    }


    /**
     * 停止扫描/发现设备
     */
    fun cancelScanDevice() {
        try {
            mBluetoothAdapter?.cancelDiscovery()
            getAppContext().unregisterReceiver(mScanDeviceReceiver)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

位置信息

  /**
     * 检查是否打开定位
     */
    fun checkLocation(): Boolean {
        val manager = getAppContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
        return LocationManagerCompat.isLocationEnabled(manager)
    }

    /**
     * 打开位置信息
     */
    fun openLocation(activity: FragmentActivity,callback: ((Boolean) -> Unit)? = null) {
        val locationIntent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
        if (callback != null) {
            InvisibleFragment.instance(activity).apply {
                callResult(REQUEST_ENABLE_LOCATION) { _, _ -> callback.invoke(checkLocation()) }
                startActivityForResult(locationIntent, REQUEST_ENABLE_LOCATION)
            }
        } else {
            activity.startActivityForResult(locationIntent, REQUEST_ENABLE_BT)
        }
    }

启用可检测性(可被其他设备发现)

  • 启用可检测性若蓝牙未开启可自动开启蓝牙。
  • 默认情况下,设备处于可检测到模式的时间为 120 秒(2 分钟),最高可为3600秒(一小时)。
  • 若可检测时间设为0,则设备将始终处于可检测到模式。此配置安全性低,非常不建议使用。
  • 如果要发起对远程设备的连接,则无需启用设备可检测性。只有当应用对接受传入连接的服务器套接字进行托管时,才有必要启用可检测性,因为在发起对其他设备的连接之前,远程设备必须能够发现这些设备。
    /**
     * 启用可检测性(可被其他设备发现)
     * 
     * @param activity
     * @param time 可检测时间 最高可为3600秒(一小时)
     * @param callback 开启结果回调
     */
    fun enableDiscoverable(activity: FragmentActivity,
                           time: Int? = null,
                           callback: ((Boolean) -> Unit)? = null) {
        val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
        time?.let {
            discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, it)
        }
        if (callback != null) {
            InvisibleFragment.instance(activity).apply {
                callResult(REQUEST_ENABLE_DISCOVERABLE) { _, resultCode ->
                    callback.invoke(resultCode != Activity.RESULT_CANCELED)
                }
                startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)
            }
        } else {
            activity.startActivityForResult(discoverableIntent, REQUEST_ENABLE_DISCOVERABLE)
        }
    }

可检测状态监听

   /**
     * 注册蓝牙可检测性监听器
     */
    fun registerBluetoothDiscoverableListener(listener: (Boolean) -> Unit) {
        this.mBluetoothDiscoverableListener = listener
        val filter = IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
        getAppContext().registerReceiver(mBluetoothDiscoverableReceiver, filter)
    }

    /**
     * 注销蓝牙可检测性监听器
     */
    fun unregisterBluetoothDiscoverableListener() {
        getAppContext().unregisterReceiver(mBluetoothDiscoverableReceiver)
    }

    private val mBluetoothDiscoverableReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {
            if (intent?.action == BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) {
                when (intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, 0)) {
                    BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE -> {
                        Timber.tag(TAG).d("设备处于可检测到模式")
                        mBluetoothChangeListener?.invoke(true)
                    }
                    BluetoothAdapter.SCAN_MODE_CONNECTABLE -> {
                        Timber.tag(TAG).d("设备未处于可检测到模式,但仍能收到连接")
                    }
                    BluetoothAdapter.SCAN_MODE_NONE -> {
                        Timber.tag(TAG).d("设备未处于可检测到模式,且无法收到连接")
                        mBluetoothChangeListener?.invoke(false)
                    }
                }
            }
        }
    }

连接设备

  • 如果在两台设备之间创建连接,必须同时实现客户端和服务端机制,因为其中一台设备必须开放服务器套接字,而另一台设备必须使用服务器设备的MAC地址发起连接。服务端设备和客户端设备均会以不同方法获取所需的BluetoothSocket。接受传入连接后,服务器会收到套接字信息。在开启与服务器连接的RFCOMM通道时,客户端会提供套接字信息。
  • 当服务器和客户端在同一 RFCOMM 通道上分别拥有已连接的 BluetoothSocket 时,即可将二者视为彼此连接。这种情况下,每台设备都能获得输入和输出流式传输,并开始传输数据。
  • 如果两台设备之前尚未配对,则在连接过程中,Android 框架会自动向用户显示配对请求通知或对话框。因此,在尝试连接设备时,应用无需担心设备是否已配对。在成功配对两台设备之前, RFCOMM 连接尝试会一直阻塞,并且如果用户拒绝配对,或者配对过程失败或超时,则该尝试便会失败。
  • 一种实现技术是自动将每台设备准备为一个服务器,从而使每台设备开放一个服务器套接字并侦听连接。在此情况下,任一设备都可发起与另一台设备的连接,并成为客户端。或者,其中一台设备可显式托管连接并按需开放一个服务器套接字,而另一台设备则发起连接。

启动服务

  1. 通过调用 listenUsingRfcommWithServiceRecord() 获取 BluetoothServerSocket,这里uuid双方使用同一标识。
  2. 开启子线程调用 accept() 阻塞侦听连接请求,接收请求后关闭当前服务。
  3. 通过keep和mIsAccept判断是否需要保持服务。
    /**
     * 启动服务
     * 
     * @param name 服务的可识别名称
     * @param keep 是否保持服务
     */
    fun start(name: String, keep: Boolean = false) {
        close()
        mIsAccept = true
        mServerSocket = BtUtils.mBluetoothAdapter?.listenUsingInsecureRfcommWithServiceRecord(name, mUUID)
        mPool.execute {
            do {
                while (mIsAccept) {
                    try {
                        mServerSocket?.accept()
                    } catch (e: IOException) {
                        Timber.tag(BtUtils.TAG).d(e, "socket的accept()方法失败")
                        try {
                            mServerSocket?.close()
                        } catch (e: IOException) {
                            Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")
                        }
                        break
                    }?.also {
                        connected(it)
                    }
                }
            } while (keep && mIsAccept)
        }
    }

    /**
     * 关闭服务
     */
    fun close() {
        mIsAccept = false
        try {
            mServerSocket?.close()
        } catch (e: IOException) {
            Timber.tag(BtUtils.TAG).e(e, "关闭ServerSocket失败")
        }
    }

连接服务/设备

  1. 开启子线程,关闭扫描,通过调用 device.createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket对象,UUID双方保持一致,确保device已启动服务。
  2. 通过调用 connect() 发起连接。
    fun connect(device: BluetoothDevice) {
        mPool.execute {
            BtUtils.cancelScanDevice()
            val socket = device.createRfcommSocketToServiceRecord(mUUID)
            try {
                socket.connect()
            } catch (e: IOException) {
                Timber.tag(TAG).e(e, "连接失败-> deviceName:${device.name}")
                ThreadUtils.runOnUiThread {
                    mStatusListener?.invoke(device, BtAction.STATE_CONNECT_FAILED, e)
                }
                return@execute
            }
            connected(socket)
        }
    }

管理连接

  1. 通过socket.remoteDevice获取远程的BluetoothDevice对象,获取address(mac)地址,通过map保存已连接的socket。
  2. 开启子线程接收对方传过来的消息
    private fun connected(socket: BluetoothSocket) {
        val device = socket.remoteDevice
        ThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_CONNECT, null) }
        Timber.tag(TAG).d("已连接-> deviceName:${device.name};deviceAddress:${device.address}")
        mBtMap[socket.remoteDevice.address] = socket
        mPool.execute {
            receiveMessage(socket)
        }
    }

    private fun receiveMessage(socket: BluetoothSocket) {
        val inputStream = socket.inputStream
        val buffer = ByteArray(1024)
        val device = socket.remoteDevice
        val name = device.name
        var numBytes: Int
        while (true) {
            numBytes = try {
                inputStream.read(buffer)
            } catch (e: IOException) {
                ThreadUtils.runOnUiThread { mStatusListener?.invoke(device, BtAction.STATE_DISCONNECT, e) }
                Timber.tag(TAG).d(e, "设备 $name 断开连接")
                mBtMap.remove(device.address)
                break
            }
            val message = String(buffer, 0, numBytes)
            Timber.tag(TAG).d("接收到设备 $name 的消息:${message}")
            ThreadUtils.runOnUiThread { mReceiveListener?.invoke(device, message) }
        }
    }

发送消息

  /**
     * 发送消息
     *
     * @param address 目标设备的mac地址
     * @param data 数据
     * @param callback 结果回调
     */
    fun send(address: String, data: ByteArray, callback: ((Boolean) -> Unit)? = null) {
        val socket = mBtMap[address]
        if (socket == null) {
            Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,发送消息失败")
            callback?.invoke(false)
            return
        }
        if (!socket.isConnected) {
            Timber.tag(TAG).e("未连接到设备mac地址为 $address 的socket,发送消息失败")
            callback?.invoke(false)
            return
        }
        mPool.execute {
            try {
                socket.outputStream.write(data)
                Timber.tag(TAG).d("发送到设备mac地址为 $address 的消息成功")
                callback?.let { ThreadUtils.runOnUiThread { it.invoke(true) } }
            } catch (e: IOException) {
                callback?.let { ThreadUtils.runOnUiThread { it.invoke(false) } }
                Timber.tag(TAG).e(e, "发送到设备mac地址为 $address 的消息失败")
            }
        }
    }

断开连接

  fun disconnect(address: String) {
        val socket = mBtMap[address] ?: return Timber.tag(TAG).e("未获取到设备mac地址为 $address 的socket,断开连接失败")
        try {
            socket.close()
        } catch (e: IOException) {
            Timber.tag(BtUtils.TAG).e(e, "关闭bluetoothSocket失败")
        }
    } 

结束语

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值