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会出现在系统生成的伴奏设备配对屏幕中.微调按钮会一直旋转到超时
考虑到我的正则表达式可能无意中过于严格,我更改了筛选器以使用允许所有内容的正则表达式,如下所示:
.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()
}
}
}
安卓宣言
<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>