You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android CompanionDeviceManager无法发现附近蓝牙设备问题求助

CompanionDeviceManager无法显示附近的非LE蓝牙设备

更新说明

  • 添加了包含蓝牙权限逻辑的主活动代码

问题描述

我在运行Android 13的Pixel 5设备上使用CompanionDeviceManager API搜索附近的非低功耗(non LE)蓝牙设备,但它仅能发现附近的WiFi网络。我怀疑deviceFilter配置存在问题。

最初配置BluetoothDeviceFilter的代码如下:

private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
    // 仅匹配名称符合该模式的蓝牙设备
    .setNamePattern(Pattern.compile("(?i)\b(Certain Device Name)\b"))
    .build()

private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // 仅查找符合请求过滤器的设备
    .addDeviceFilter(deviceFilter)
    // 找到一个匹配过滤器的设备后不停止扫描
    .setSingleDevice(false)
    .build()

使用这段代码时,系统生成的配套设备配对界面中始终没有设备显示,加载动画会一直转圈直到超时。

我猜测可能是正则表达式限制过严,于是将过滤器改为允许所有设备的正则:

.setNamePattern(Pattern.compile(".*"))

但即使这样,配对界面中仍然没有附近的蓝牙设备显示。

当我故意不添加任何过滤器时,只能看到WiFi网络,说明CompanionDeviceManager可以正常工作,但针对蓝牙结果的配置有误。

private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // 不设置过滤器,查看所有设备
    .setSingleDevice(false)
    .build()

在Android系统的蓝牙菜单中,我能清楚看到范围内的蓝牙设备,甚至可以连接它们,但这些设备从未在我的应用中显示。

相关代码

HomeFragment.kt

class HomeFragment : Fragment() {

//过滤可见蓝牙设备,仅显示范围内的Mozi设备
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
    // 仅匹配名称符合该模式的蓝牙设备
    .setNamePattern(Pattern.compile(BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR))
    .build()

private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // 仅查找符合该请求过滤器的设备
    .addDeviceFilter(deviceFilter)
    // 找到一个匹配过滤器的设备后不停止扫描
    .setSingleDevice(false)
    .build()

private val deviceManager: CompanionDeviceManager by lazy {
    requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}

private val executor: Executor = Executor { it.run() }

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    setupPairingButton()
    return binding.root
}

/**
 * 此回调监听与Mozi蓝牙设备的连接尝试结果
 */
@Deprecated("Deprecated in Java")
@SuppressLint("MissingPermission")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    when (requestCode) {
        SELECT_DEVICE_REQUEST_CODE -> when (resultCode) {
            Activity.RESULT_OK -> {
                // 用户选择将应用与蓝牙设备配对
                val deviceToPair: BluetoothDevice? =
                    data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
                deviceToPair?.createBond()
            }
        }
        else -> super.onActivityResult(requestCode, resultCode, data)
    }
}

private fun setupPairingButton() {
    binding.buttonPair.setOnClickListener {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            /**
             * 这是Android 33及以上版本显示配对对话框的方式
             */
            deviceManager.associate(pairingRequest, executor,
                object : CompanionDeviceManager.Callback() {
                    // 找到设备时调用。启动IntentSender让用户选择要配对的设备
                    override fun onAssociationPending(intentSender: IntentSender) {
                        intentSender.let { sender ->
                            activity?.let { fragmentActivity ->
                                startIntentSenderForResult(
                                    fragmentActivity,
                                    sender,
                                    SELECT_DEVICE_REQUEST_CODE,
                                    null,
                                    0,
                                    0,
                                    0,
                                    null
                                )
                            }
                        }
                    }

                    override fun onAssociationCreated(associationInfo: AssociationInfo) {
                        // 关联已创建
                        var associationId = associationInfo.id
                        var macAddress: MacAddress? = associationInfo.deviceMacAddress
                    }

                    override fun onFailure(errorMessage: CharSequence?) {
                        // 处理失败情况
                        showBluetoothErrorMessage(errorMessage)
                    }
                })
        } else {
            /**
             * 这是Android 32及以下版本显示配对对话框的方式
             */
            deviceManager.associate(
                pairingRequest,
                object : CompanionDeviceManager.Callback() {
                    override fun onDeviceFound(chooserLauncher: IntentSender) {
                        startIntentSenderForResult(
                            chooserLauncher,
                            SELECT_DEVICE_REQUEST_CODE,
                            null,
                            0,
                            0,
                            0,
                            null
                        )
                    }

                    override fun onFailure(error: CharSequence?) {
                        // 处理失败情况
                       showBluetoothErrorMessage(error)
                    }
                }, null
            )
        }
    }
}

companion object {
    private const val SELECT_DEVICE_REQUEST_CODE = 0
    private const val BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR = "(?i)\bCertain Device Name\b"
}
}

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)

private var bluetoothEnableResultLauncher =
    registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        binding.loadingSpinner.hide()

        when (result.resultCode) {
            Activity.RESULT_OK -> {
                Snackbar.make(
                    binding.root,
                    resources.getString(R.string.bluetooth_enabled_lets_pair_with_your_mozi),
                    Snackbar.LENGTH_SHORT
                ).show()
            }
            Activity.RESULT_CANCELED -> {
                Snackbar.make(
                    binding.root,
                    getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
                    Snackbar.LENGTH_INDEFINITE
                )
                    .setAction(resources.getString(R.string._retry)) {
                        ensureBluetoothIsEnabled()
                    }
                    .show()
            }
        }
    }

private val requestBluetoothPermissionLauncher =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
        } else {
            Snackbar.make(
                binding.root,
                getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
                Snackbar.LENGTH_INDEFINITE
            )
                .setAction(resources.getString(R.string._retry)) {
                    ensureBluetoothIsEnabled()
                }
                .show()
        }
    }

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setupViews()
    ensureBluetoothIsEnabled()
}

private fun setupViews() {
    binding.bluetoothPermissionsRationaleDialogButton.setOnClickListener {
        binding.permissionsRationaleDialog.animateShow(false)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
        } else {
            requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
        }
    }
}

private fun ensureBluetoothIsEnabled() {
    binding.loadingSpinner.show()

    val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
    val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
    if (bluetoothAdapter == null) {
        binding.loadingSpinner.hide()
        Snackbar.make(
            binding.root,
            resources.getString(R.string.you_need_a_bluetooth_enabled_device),
            Snackbar.LENGTH_INDEFINITE
        ).show()
    }

    if (bluetoothAdapter?.isEnabled == false) {
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_CONNECT
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            binding.loadingSpinner.hide()

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
                    binding.permissionsRationaleDialog.animateShow(true)
                } else {
                    requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
                }
            } else {
                if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH)) {
                    binding.permissionsRationaleDialog.animateShow(true)
                } else {
                    requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
                }
            }
            return
        } else {
            binding.loadingSpinner.hide()
            bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
        }
    } else {
        binding.loadingSpinner.hide()
    }
}
}

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!-- 蓝牙权限 -->
<uses-feature android:name="android.software.companion_device_setup" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<!-- 在旧设备上请求传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />

<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 仅当应用需要搜索蓝牙设备时需要。如果应用不使用蓝牙扫描结果获取物理位置信息,可以声明不获取位置 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags= "neverForLocation"
    tools:targetApi="s" />

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

...
</manifest>

解决方案

1. 补充蓝牙扫描权限请求

Android 12+中,扫描蓝牙设备需要BLUETOOTH_SCAN权限,你当前只请求了BLUETOOTH_CONNECT,导致无法扫描设备。修改权限请求逻辑为请求多个权限:

// 替换原有的requestBluetoothPermissionLauncher
private val requestBluetoothPermissionsLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        val connectGranted = permissions[Manifest.permission.BLUETOOTH_CONNECT] ?: false
        val scanGranted = permissions[Manifest.permission.BLUETOOTH_SCAN] ?: false
        if (connectGranted && scanGranted) {
            bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
        } else {
            Snackbar.make(
                binding.root,
                getString(R.string.without_bluetooth_permissions_cant_pair),
                Snackbar.LENGTH_INDEFINITE
            )
                .setAction(resources.getString(R.string._retry)) {
                    ensureBluetoothIsEnabled()
                }
                .show()
        }
    }

// 修改ensureBluetoothIsEnabled中的权限请求部分
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    val needsRationale = shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT) ||
            shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_SCAN)
    if (needsRationale) {
        binding.permissionsRationaleDialog.animateShow(true)
    } else {
        requestBluetoothPermissionsLauncher.launch(
            arrayOf(
                Manifest.permission.BLUETOOTH_CONNECT,
                Manifest.permission.BLUETOOTH_SCAN
            )
        )
    }
} else {
    // 旧版本处理逻辑不变
}

2. 指定扫描传统蓝牙设备

在过滤器中明确设置设备类型为传统蓝牙(非BLE),避免默认只扫描BLE设备:

private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
    .setNamePattern(Pattern.compile(BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR))
    // 扫描传统蓝牙设备
    .setDeviceType(BluetoothDeviceFilter.DEVICE_TYPE_CLASSIC)
    .build()

3. 修复正则表达式转义问题

Kotlin中字符串的\b需要转义为\\b,否则会被解析为退格符,导致正则不匹配任何设备:

private const val BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR = "(?i)\\bCertain Device Name\\b"

4. 优化清单权限

移除不必要的ACCESS_BACKGROUND_LOCATION权限(如果不需要后台定位),减少权限申请的复杂度:

<!-- 移除不需要的后台定位权限 -->
<!-- <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> -->

内容的提问来源于stack exchange,提问作者Cody

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 15:25:10