Android CompanionDeviceManager 从未找到任何附近的蓝牙设备

Cod*_*ody 3 android bluetooth bluetooth-lowenergy android-bluetooth android-companion-device

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

我尝试利用 Android 的CompanionDeviceManager API在运行 Android 13 的 Pixel 5 上查找附近的蓝牙(非 LE)设备,但它似乎只能找到附近的 WiFi 网络。我怀疑它deviceFilter无法正常工作。

最初,我的配置代码BluetoothDeviceFilter如下所示:

private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
    // Match only Bluetooth devices whose name matches the pattern
    .setNamePattern(Pattern.compile("(?i)\\b(Certain Device Name)\\b"))
    .build()

private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // Find only devices that match our request filter
    .addDeviceFilter(deviceFilter)
    // Don't stop scanning as soon as one device matching the filter is found.
    .setSingleDevice(false)
    .build()
Run Code Online (Sandbox Code Playgroud)

然而,使用此代码,系统生成的“配套设备配对”屏幕中不会出现任何设备。旋转器旋转直至超时

在此输入图像描述

考虑到我的正则表达式可能无意中过于严格,我将过滤器更改为使用允许一切的正则表达式,如下所示:

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

但即使这个过滤器也无法允许任何附近的蓝牙设备出现在配对屏幕中。

当我故意不添加任何过滤器时,我看到的都是 WiFi 网络,因此配套设备管理器可以工作,但它似乎对蓝牙结果配置错误。

    private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // No filter, let's see it all!
    .setSingleDevice(false)
    .build()
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

使用 Android 操作系统的系统蓝牙菜单,我可以清楚地看到我的设备范围内有蓝牙设备,我什至可以连接到它们,但相同的设备永远不会出现在我的应用程序中。

我做错了什么导致CompanionDeviceManager配对屏幕中没有出现附近的蓝牙设备?

代码如下:

HomeFragment.kt 类 HomeFragment : Fragment() {

//Filter visible Bluetooth devices so only Mozis within range are displayed
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
    // Match only Bluetooth devices whose name matches the pattern.
    .setNamePattern(Pattern.compile(BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR))
    .build()

private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
    // Find only devices that match this request filter.
    .addDeviceFilter(deviceFilter)
    // Don't stop scanning as soon as one device matching the filter is found.
    .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()

}

/**
 * This callback listens for the result of connection attempts to our Mozi Bluetooth devices
 */
@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 -> {
                // The user chose to pair the app with a Bluetooth device.
                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) {
            /**
             * This is the approach to show a pairing dialog for Android 33+
             */
            deviceManager.associate(pairingRequest, executor,
                object : CompanionDeviceManager.Callback() {
                    // Called when a device is found. Launch the IntentSender so the user
                    // can select the device they want to pair with
                    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) {
                        // Association created.

                        // AssociationInfo object is created and get association id and the
                        // macAddress.
                        var associationId = associationInfo.id
                        var macAddress: MacAddress? = associationInfo.deviceMacAddress
                    }

                    override fun onFailure(errorMessage: CharSequence?) {
                        // Handle the failure.
                        showBluetoothErrorMessage(errorMessage)
                    }
                })
        } else {
            /**
             * This is the approach to show a pairing dialog for Android 32 and below
             */

            // When the app tries to pair with a Bluetooth device, show the
            // corresponding dialog box to the user.
            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?) {
                        // Handle the failure.
                       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"
}}
Run Code Online (Sandbox Code Playgroud)

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 {
            // Explain to the user that the feature is unavailable because the
            // feature requires a permission that the user has denied. At the
            // same time, respect the user's decision. Don't link to system
            // settings in an effort to convince the user to change their
            // decision.
            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() {
    //Here we setup the behavior of the button in our rationale dialog: basically we need to
    //  rerun the permissions check logic if it was already denied
    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) {
        // Device doesn't support Bluetooth
        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) {
        // Check if Bluetooth permissions have been granted before we try to enable the
        //  device
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_CONNECT //TODO: test if this needs variant for legacy devices
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            /**
             * We DON'T have Bluetooth permissions. We have to get them before we can ask the
             *  user to enable Bluetooth
             */
            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 {
            /**
             * We DO have Bluetooth permissions. Now let's prompt the user to enable their
             *  Bluetooth radio
             */
            binding.loadingSpinner.hide()
            bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
        }
    } else {
        /**
         * Bluetooth is enabled, we're good to continue with normal app flow
         */
        binding.loadingSpinner.hide()
    }
}
Run Code Online (Sandbox Code Playgroud)

}

安卓清单

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

<!-- Bluetooth Permissions -->
<uses-feature android:name="android.software.companion_device_setup" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<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" />
<!-- Needed only if your app looks for Bluetooth devices.
     If your app doesn't use Bluetooth scan results to derive physical
     location information, you can strongly assert that your app
     doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags= "neverForLocation"
    tools:targetApi="s" />

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

...
</manifest>
Run Code Online (Sandbox Code Playgroud)

小智 5

文档没有提到这一点,但似乎即使使用 CompanionDeviceManager,也必须在设备上启用位置访问。
该应用程序不再需要位置权限,但必须启用它。