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

问题描述 投票:0回答:4

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

我尝试利用 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()

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

考虑到我的正则表达式可能无意中限制太多,我更改了过滤器以使用允许一切的正则表达式,如下所示:

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

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

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

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

使用 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"
}}

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()
    }
}

}

Android 清单

<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>
android bluetooth bluetooth-lowenergy android-bluetooth android-companion-device
4个回答
3
投票

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


1
投票

可能是权限问题。

文档中,我读到:

BLUETOOTH_ADVERTISE、BLUETOOTH_CONNECT 和 BLUETOOTH_SCAN 权限是运行时权限。因此,您必须在应用程序中明确请求用户批准,然后才能查找蓝牙设备、使其他设备可发现设备或与已配对的蓝牙设备通信。

因此您可以在您的

HomeFragment
类中添加以下代码:

private val requestMultiplePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
    permissions.entries.forEach {
        Log.d("Permission Request", "${it.key} = ${it.value}")
    }
}

private val requestBluetooth = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode == RESULT_OK) {
        // granted
    } else {
        // denied
    }
}

并且在

onCreateView
方法中:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    requestMultiplePermissions.launch(arrayOf(
        Manifest.permission.BLUETOOTH_SCAN,
        Manifest.permission.BLUETOOTH_CONNECT
    ))
} else {
    val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
    requestBluetooth.launch(enableBtIntent)
}

在运行时请求权限。


1
投票

您可以尝试使用空的

BluetoothDeviceFilter
,如下所示:

private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder().build()

向 API 发出您想要蓝牙设备的信号,并查看至少手机是否可以识别您的设备。

然后您可以再次尝试使用名称过滤器,这次使用

BluetoothDeviceFilter.Builder.addServiceUuid
添加服务 UUID 过滤器。

如果您不知道设备的 UUID 或者不想将其用作过滤器,则可以使用任意一个并将掩码设置为全零(文档建议它也可以使用 null 值) .

这是一个黑客解决方案,但它可能会帮助您更进一步


0
投票

确保在运行或测试应用程序之前启用位置服务...这为我解决了问题

© www.soinside.com 2019 - 2024. All rights reserved.