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

我试着用安卓CompanionDeviceManager API在我运行安卓13的Pixel5上找到附近的蓝牙(非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()

然而,使用此代码时,no devices会出现在系统生成的伴奏设备配对屏幕中.微调按钮会一直旋转到超时

enter image description here

考虑到我的正则表达式可能无意中过于严格,我更改了筛选器以使用允许所有内容的正则表达式,如下所示:

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

但即使是这个过滤器也无法让附近的蓝牙设备出现在配对屏幕上.

当我故意不添加任何过滤器时,我看到的都是WiFi网络,这样伴奏设备管理器就可以工作,只是它似乎错误地配置了蓝牙结果.

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

enter image description here

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

}

安卓宣言

<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>

推荐答案

这可能是一个许可问题.

docs米比赛中,我读到:

蓝牙广告、蓝牙连接和蓝牙扫描权限是运行时权限.因此,在寻找蓝牙设备、使设备可被其他设备发现或与已配对的蓝牙设备通信之前,您必须在应用程序中明确请求用户批准.

因此,您可以在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)
}

在运行时请求权限.

Android相关问答推荐

如何让用户与我的应用生成的多个音频文件交互

安卓Gradle看不到dagger 柄

有人能帮我在应用程序上使用模拟位置时避免被发现吗?我已经反编译并粘贴了一个代码,S小文件

如何从Android 12的来电中获取电话号码?

尽管我们不再使用GCM SDK,但应用程序已被标记为使用GCM SDK

Modifer.Align()不适用于行/列/框中的文本.未解决的作用域实例错误

SmsMessage如何在Kotlin库中工作?

Jetpack Compose 中的用户在线指示器

如何在每次显示可组合项时执行代码(并且只执行一次)

获取 ArithmeticException:除以零,但我没有在任何地方除以零

如何将一个 Composable 作为其参数传递给另一个 Composable 并在 Jetpack Compose 中显示/运行它

如何在 compose 中使用 BottomSheetScaffold 为底页设置半展开高度?

将应用更改为暗模式后 Android MainActivity 数据泄漏

如何在 Android 上移动 EditText 上的图标?

在 Kotlin 客户端应用程序中发送 FCM 推送通知 - Firebase 云消息传递

jetpack compose 中的可点击指示是什么?

禁用通知权限后启动前台服务导致崩溃(Android 13)

操作系统会终止已启动的服务并调用Service.onDestroy吗?

升级到 android studio 花栗鼠后,应用程序未安装在模拟器中

Android:如何获取具有纬度和经度的位置的图像