公司項目需求需要實現監聽藍牙耳機連接,且要獲取藍牙耳機電量功能,翻了不少官方文檔,記錄下技術調研代碼
注:本文沒有研究藍牙配對功能
關於藍牙權限適配
Android12以後,申請藍牙權限需要申請一組,如新增的幾個權限,需要一起申請
參考: 藍牙權限 | Connectivity | Android Developers
val permissionList = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
//android12及以上版本,這2個權限申請只會彈出一個對話框
listOf(Permission.BLUETOOTH_CONNECT, Permission.BLUETOOTH_SCAN)
} else {
//android12以下版本申請,默認是同意的,不會有權限彈窗
listOf(Permission.BLUETOOTH_CONNECT)
}
打開藍牙開關
注意,如果是Android12及以上版本,藍牙開關打開操作需要有Bluetooth_Connect權限才能執行操作
效果就是直接打開藍牙開關
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
//需要權限android.permission.BLUETOOTH_CONNECT才能執行操作
bluetoothAdapter.enable()
不過Android還是有提供另外的一個方法供我們使用,就是下面的方法
此方法是API 5 就有的方法,和上面一樣,Android12及以上版本,就是需要有Bluetooth_Connect權限才能執行成功,否則會拋出異常
兼容低版本和高版本,此方法兼容,調用此方法,系統會彈出一個是否允許打開藍牙的對話提示框
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
//這裏寫你對應的Activity,我這裏只是個例子
Activity.startActivityForResult(intent,7777)
至於接收回調,則是在對應的Activity中的onActivityResult()方法中處理返回結果:
- 返回結果RESULT_OK,藍牙模塊打開成功
- 返回結果RESULT_CANCELED,藍牙模塊打開失敗
PS: 測試的時候,用的華爲手機,系統爲鴻蒙4,Android Studio顯示爲Android12,但是使用
bluetoothAdapter.enable()
卻是能夠正常彈出申請藍牙是否打開的對話框
獲取已配對的藍牙設備列表
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
val bluetoothDevices = bluetoothAdapter.bondedDevices
//獲取已配對的藍牙設備列表
bluetoothDevices.forEach { device->
val text = when(device.type){
BluetoothDevice.DEVICE_TYPE_UNKNOWN -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_CLASSIC -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_LE -> "低功耗藍牙"
BluetoothDevice.DEVICE_TYPE_DUAL -> "傳統/低功耗雙模式藍牙"
else->"未知類型"
}
LogUtils.d("藍牙設備名稱: ${device.name} 藍牙設備地址: ${device.address} 設備類型: $text")
}
獲取藍牙耳機設備列表
fun getEarPhoneDevices(context: Context): List<BluetoothDevice> {
val bluetoothAdapter = context.getSystemService(BluetoothManager::class.java).adapter
val bluetoothDevices = bluetoothAdapter.bondedDevices
val types = listOf(
BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES,
BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET,
BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO,
BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE
)
return bluetoothDevices.filter { device ->
types.any { it == device.bluetoothClass.deviceClass }
}
}
PS: 測試過程中,發現漫步者耳機的類型識別不了爲上述的四個類型...
獲取當前已連接藍牙耳機
一般只能連接一個藍牙耳機
val bluetoothAdapter = getSystemService(BluetoothManager::class.java).adapter
//如果在連接了藍牙耳機的情況,這裏會進入到裏面獲取到數據
bluetoothAdapter.getProfileProxy(this@EarphoneActivity, object : ServiceListener {
override fun onServiceConnected(p0: Int, p1: BluetoothProfile?) {
p1?.apply {
//獲取藍牙耳機的設備列表
val devices = this.connectedDevices
devices.forEach { device ->
val text = when (device.type) {
BluetoothDevice.DEVICE_TYPE_UNKNOWN -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_CLASSIC -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_LE -> "低功耗藍牙"
BluetoothDevice.DEVICE_TYPE_DUAL -> "傳統/低功耗雙模式藍牙"
else -> "未知類型"
}
LogUtils.d("藍牙設備名稱: ${device.name} 藍牙設備地址: ${device.address} 設備類型: $text")
}
}
LogUtils.d("設備連接")
}
override fun onServiceDisconnected(p0: Int) {
}
}, BluetoothProfile.HEADSET)
}
獲取藍牙耳機電量
此方法適應市面上大多數藍牙耳機,但如果是AirPods,則無效果,下一章節會講到獲取AirPods電量方法
(雖然參考的文章說這個是AirPods的擴展AT命令,但實際對於正版AirPods無效果,反倒是我同事的華強北AirPods支持...)
通過註冊廣播,來獲取到對應的AT命令,在參數可以取值
val bluetoothIntentFilter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
+ addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY+"."+BluetoothAssignedNumbers.APPLE)
+ addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT)
}
registerReceiver(BlueToothReceiver(), bluetoothIntentFilter)
廣播詳情說明可看此鏈接藍牙耳機 | 安卓開發者
之後在Receiver可以獲取對應的AT命令參數,如下代碼:
//藍牙耳機的廣播監聽
if (BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT == action) {
Log.d(TAG, "onReceive: 藍牙設備AT命令")
//藍牙設備
val blueDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
blueDevice?.apply {
val device = this
val text = when (device.type) {
BluetoothDevice.DEVICE_TYPE_UNKNOWN -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_CLASSIC -> "傳統藍牙"
BluetoothDevice.DEVICE_TYPE_LE -> "低功耗藍牙"
BluetoothDevice.DEVICE_TYPE_DUAL -> "傳統/低功耗雙模式藍牙"
else -> "未知類型"
}
LogUtils.d("藍牙設備名稱: ${device.name} 藍牙設備地址: ${device.address} 設備類型: $text")
}
intent.extras?.apply {
val cmd = getString(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD,"")
val cmdType = getInt(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE,0)
//根據命令行類型,會有不同的參數
val cmdTypeStr = when (cmdType) {
BluetoothHeadset.AT_CMD_TYPE_ACTION -> {"AT_CMD_TYPE_ACTION"}
BluetoothHeadset.AT_CMD_TYPE_BASIC -> {"AT_CMD_TYPE_BASIC"}
BluetoothHeadset.AT_CMD_TYPE_READ -> {"AT_CMD_TYPE_READ"}
BluetoothHeadset.AT_CMD_TYPE_SET -> {"AT_CMD_TYPE_SET"}
BluetoothHeadset.AT_CMD_TYPE_TEST -> {"AT_CMD_TYPE_TEST"}
else -> {""}
}
val args = get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS) as Array<Any>
LogUtils.d("""
接收到的AT命令: AT $cmd $cmdTypeStr ${args.joinToString(",") { it.toString() }}
""".trimIndent())
if (cmd == "+IPHONEACCEV") {
//電量等級說明 0:10% 9:100%
val param = args.map { it.toString().toInt() }
val level = param.last()
//電量
val battery = (level + 1) * 10
}
}
}
AT+IPHONEACCEV命令
該命令是用來提示藍牙配件的電池狀態,可以提示兩方面:一方面是電池的電量百分比,一當面是藍牙配件的當前的充電狀態。該命令的說明見下方:
格式:AT+IPHONEACCEV=Number of key/value pairs,key1,val1,key2,val2,…
附帶的參數的含義分別是:①鍵值對的數目:接下來的參數文本的數量;②接下來就是鍵值對分別是:鍵值爲1表示的是電量,該鍵所對應的值就是電量百分比,使用字串”0“到”9“表示;鍵值爲2表示的是充電狀態,0表示不在充電,1表示正在充電。
舉例:AT+IPHONEACCEV=1,1,3 該AT指令就說明附帶了一個鍵值對(第一個參數是1);鍵是1,那麼表示的是電量,且電量是40%(因爲使用的是0~9,這裏3就對應的百分比是40%)。
有個疑問,AirPods在電量變化後,會主動發送AT命令嗎?還是說是在連接後纔會發一次,之後便不再發送了?
AT +XAPL AT_CMD_TYPE_SET AB-12-0100,18
AirPods耳機電量
起初一致沒找到方案,最終在github上輸入了AirPods關鍵字,發現了有幾個對於對應的開源庫,測試發現下面這個能夠符合要求(不過測試的時候,電量有些誤差,充電倉在iphone手機上顯示爲8%,而android這邊則顯示爲5%)
app原理則是通過藍牙掃描,獲取到藍牙設備對應的設備廠商數據,並區分型號,然後做對應的處理從而獲取到電量(比如說左耳機,右耳機,耳機倉)
通過藍牙的adapter獲取scanner,調用掃描方法,之後在掃描的回調裏處理返回結果, 從而得到對應的電量數據
藍牙掃描還需要一個獲取定位的權限(在Android12版本之下需要),不然無法掃描