1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settingslib.bluetooth 17 18 import android.annotation.TargetApi 19 import android.bluetooth.BluetoothAdapter 20 import android.bluetooth.BluetoothDevice 21 import android.bluetooth.BluetoothLeAudioCodecConfigMetadata 22 import android.bluetooth.BluetoothLeAudioContentMetadata 23 import android.bluetooth.BluetoothLeBroadcastChannel 24 import android.bluetooth.BluetoothLeBroadcastMetadata 25 import android.bluetooth.BluetoothLeBroadcastSubgroup 26 import android.os.Build 27 import android.util.Base64 28 import android.util.Log 29 import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA 30 31 object BluetoothLeBroadcastMetadataExt { 32 private const val TAG = "BtLeBroadcastMetadataExt" 33 34 // Data Elements for directing Broadcast Assistants 35 private const val KEY_BT_BROADCAST_NAME = "BN" 36 private const val KEY_BT_ADVERTISER_ADDRESS_TYPE = "AT" 37 private const val KEY_BT_ADVERTISER_ADDRESS = "AD" 38 private const val KEY_BT_BROADCAST_ID = "BI" 39 private const val KEY_BT_BROADCAST_CODE = "BC" 40 private const val KEY_BT_STREAM_METADATA = "MD" 41 private const val KEY_BT_STANDARD_QUALITY = "SQ" 42 private const val KEY_BT_HIGH_QUALITY = "HQ" 43 44 // Extended Bluetooth URI Data Elements 45 private const val KEY_BT_ADVERTISING_SID = "AS" 46 private const val KEY_BT_PA_INTERVAL = "PI" 47 private const val KEY_BT_NUM_SUBGROUPS = "NS" 48 49 // Subgroup data elements 50 private const val KEY_BTSG_BIS_SYNC = "BS" 51 private const val KEY_BTSG_NUM_BISES = "NB" 52 private const val KEY_BTSG_METADATA = "SM" 53 54 // Vendor specific data, not being used 55 private const val KEY_BTVSD_VENDOR_DATA = "VS" 56 57 private const val DELIMITER_KEY_VALUE = ":" 58 private const val DELIMITER_ELEMENT = ";" 59 60 private const val SUFFIX_QR_CODE = ";;" 61 62 // BT constants 63 private const val BIS_SYNC_MAX_CHANNEL = 32 64 private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu 65 private const val SUBGROUP_LC3_CODEC_ID = 0x6L 66 67 /** 68 * Converts [BluetoothLeBroadcastMetadata] to QR code string. 69 * 70 * QR code string will prefix with "BLUETOOTH:UUID:184F". 71 */ 72 fun BluetoothLeBroadcastMetadata.toQrCodeString(): String { 73 val entries = mutableListOf<Pair<String, String>>() 74 // Generate data elements for directing Broadcast Assistants 75 require(this.broadcastName != null) { "Broadcast name is mandatory for QR code" } 76 entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString( 77 this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP))) 78 entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS_TYPE, this.sourceAddressType.toString())) 79 entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS, this.sourceDevice.address.replace(":", ""))) 80 entries.add(Pair(KEY_BT_BROADCAST_ID, String.format("%X", this.broadcastId.toLong()))) 81 if (this.broadcastCode != null) { 82 entries.add(Pair(KEY_BT_BROADCAST_CODE, 83 Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP))) 84 } 85 if (this.publicBroadcastMetadata != null && 86 this.publicBroadcastMetadata?.rawMetadata?.size != 0) { 87 entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString( 88 this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP))) 89 } 90 if ((this.audioConfigQuality and 91 BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD) != 0) { 92 entries.add(Pair(KEY_BT_STANDARD_QUALITY, "1")) 93 } 94 if ((this.audioConfigQuality and 95 BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH) != 0) { 96 entries.add(Pair(KEY_BT_HIGH_QUALITY, "1")) 97 } 98 99 // Generate extended Bluetooth URI data elements 100 entries.add(Pair(KEY_BT_ADVERTISING_SID, 101 String.format("%X", this.sourceAdvertisingSid.toLong()))) 102 entries.add(Pair(KEY_BT_PA_INTERVAL, String.format("%X", this.paSyncInterval.toLong()))) 103 entries.add(Pair(KEY_BT_NUM_SUBGROUPS, String.format("%X", this.subgroups.size.toLong()))) 104 105 this.subgroups.forEach { 106 val (bisSync, bisCount) = getBisSyncFromChannels(it.channels) 107 entries.add(Pair(KEY_BTSG_BIS_SYNC, String.format("%X", bisSync.toLong()))) 108 if (bisCount > 0u) { 109 entries.add(Pair(KEY_BTSG_NUM_BISES, String.format("%X", bisCount.toLong()))) 110 } 111 if (it.contentMetadata.rawMetadata.size != 0) { 112 entries.add(Pair(KEY_BTSG_METADATA, 113 Base64.encodeToString(it.contentMetadata.rawMetadata, Base64.NO_WRAP))) 114 } 115 } 116 117 val qrCodeString = SCHEME_BT_BROADCAST_METADATA + 118 entries.toQrCodeString(DELIMITER_ELEMENT) + SUFFIX_QR_CODE 119 Log.d(TAG, "Generated QR string : $qrCodeString") 120 return qrCodeString 121 } 122 123 /** 124 * Converts QR code string to [BluetoothLeBroadcastMetadata]. 125 * 126 * QR code string should prefix with "BLUETOOTH:UUID:184F". 127 */ 128 fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? { 129 if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) { 130 Log.e(TAG, "String \"$qrCodeString\" does not begin with " + 131 "\"$SCHEME_BT_BROADCAST_METADATA\"") 132 return null 133 } 134 return try { 135 Log.d(TAG, "Parsing QR string: $qrCodeString") 136 val strippedString = 137 qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA) 138 .removeSuffix(SUFFIX_QR_CODE) 139 Log.d(TAG, "Stripped to: $strippedString") 140 parseQrCodeToMetadata(strippedString) 141 } catch (e: Exception) { 142 Log.w(TAG, "Cannot parse: $qrCodeString", e) 143 null 144 } 145 } 146 147 private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String { 148 val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second } 149 return entryStrings.joinToString(separator = delimiter) 150 } 151 152 @TargetApi(Build.VERSION_CODES.TIRAMISU) 153 private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata { 154 // Split into a list of list 155 val elementFields = input.split(DELIMITER_ELEMENT) 156 .map{it.split(DELIMITER_KEY_VALUE, limit = 2)} 157 158 var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN 159 var sourceAddrString: String? = null 160 var sourceAdvertiserSid = -1 161 var broadcastId = -1 162 var broadcastName: String? = null 163 var streamMetadata: BluetoothLeAudioContentMetadata? = null 164 var paSyncInterval = -1 165 var broadcastCode: ByteArray? = null 166 var audioConfigQualityStandard = -1 167 var audioConfigQualityHigh = -1 168 var numSubgroups = -1 169 170 // List of subgroup data 171 var subgroupBisSyncList = mutableListOf<UInt>() 172 var subgroupNumOfBisesList = mutableListOf<UInt>() 173 var subgroupMetadataList = mutableListOf<ByteArray?>() 174 175 val builder = BluetoothLeBroadcastMetadata.Builder() 176 177 for (field: List<String> in elementFields) { 178 if (field.isEmpty()) { 179 continue 180 } 181 val key = field[0] 182 // Ignore 3rd value and after 183 val value = if (field.size > 1) field[1] else "" 184 when (key) { 185 // Parse data elements for directing Broadcast Assistants 186 KEY_BT_BROADCAST_NAME -> { 187 require(broadcastName == null) { "Duplicate broadcastName: $input" } 188 broadcastName = String(Base64.decode(value, Base64.NO_WRAP)) 189 } 190 KEY_BT_ADVERTISER_ADDRESS_TYPE -> { 191 require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) { 192 "Duplicate sourceAddrType: $input" 193 } 194 sourceAddrType = value.toInt() 195 } 196 KEY_BT_ADVERTISER_ADDRESS -> { 197 require(sourceAddrString == null) { "Duplicate sourceAddr: $input" } 198 sourceAddrString = value.chunked(2).joinToString(":") 199 } 200 KEY_BT_BROADCAST_ID -> { 201 require(broadcastId == -1) { "Duplicate broadcastId: $input" } 202 broadcastId = value.toInt(16) 203 } 204 KEY_BT_BROADCAST_CODE -> { 205 require(broadcastCode == null) { "Duplicate broadcastCode: $input" } 206 207 broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) } 208 .toByteArray(), Base64.NO_WRAP) 209 } 210 KEY_BT_STREAM_METADATA -> { 211 require(streamMetadata == null) { 212 "Duplicate streamMetadata $input" 213 } 214 streamMetadata = BluetoothLeAudioContentMetadata 215 .fromRawBytes(Base64.decode(value, Base64.NO_WRAP)) 216 } 217 KEY_BT_STANDARD_QUALITY -> { 218 require(audioConfigQualityStandard == -1) { 219 "Duplicate audioConfigQualityStandard: $input" 220 } 221 audioConfigQualityStandard = value.toInt() 222 } 223 KEY_BT_HIGH_QUALITY -> { 224 require(audioConfigQualityHigh == -1) { 225 "Duplicate audioConfigQualityHigh: $input" 226 } 227 audioConfigQualityHigh = value.toInt() 228 } 229 230 // Parse extended Bluetooth URI data elements 231 KEY_BT_ADVERTISING_SID -> { 232 require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" } 233 sourceAdvertiserSid = value.toInt(16) 234 } 235 KEY_BT_PA_INTERVAL -> { 236 require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" } 237 paSyncInterval = value.toInt(16) 238 } 239 KEY_BT_NUM_SUBGROUPS -> { 240 require(numSubgroups == -1) { "Duplicate numSubgroups: $input" } 241 numSubgroups = value.toInt(16) 242 } 243 244 // Repeatable subgroup elements 245 KEY_BTSG_BIS_SYNC -> { 246 subgroupBisSyncList.add(value.toUInt(16)) 247 } 248 KEY_BTSG_NUM_BISES -> { 249 subgroupNumOfBisesList.add(value.toUInt(16)) 250 } 251 KEY_BTSG_METADATA -> { 252 subgroupMetadataList.add(Base64.decode(value, Base64.NO_WRAP)) 253 } 254 } 255 } 256 Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " + 257 "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " + 258 "broadcastId=$broadcastId, broadcastName=$broadcastName, " + 259 "streamMetadata=${streamMetadata != null}, " + 260 "paSyncInterval=$paSyncInterval, " + 261 "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " + 262 "audioConfigQualityStandard=$audioConfigQualityStandard, " + 263 "audioConfigQualityHigh=$audioConfigQualityHigh") 264 265 val adapter = BluetoothAdapter.getDefaultAdapter() 266 // Check parsed elements data 267 require(broadcastName != null) { 268 "broadcastName($broadcastName) must present in QR code string" 269 } 270 var addr = sourceAddrString 271 var addrType = sourceAddrType 272 if (sourceAddrString != null) { 273 require(sourceAddrType != BluetoothDevice.ADDRESS_TYPE_UNKNOWN) { 274 "sourceAddrType($sourceAddrType) must present if address present" 275 } 276 } else { 277 // Use placeholder device if not present 278 addr = "FF:FF:FF:FF:FF:FF" 279 addrType = BluetoothDevice.ADDRESS_TYPE_RANDOM 280 } 281 val device = adapter.getRemoteLeDevice(requireNotNull(addr), addrType) 282 283 // add source device and set broadcast code 284 var audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE or 285 (if (audioConfigQualityStandard != -1) audioConfigQualityStandard else 0) or 286 (if (audioConfigQualityHigh != -1) audioConfigQualityHigh else 0) 287 288 // process subgroup data 289 // metadata should include at least 1 subgroup for metadata, add a placeholder group if not present 290 numSubgroups = if (numSubgroups > 0) numSubgroups else 1 291 for (i in 0 until numSubgroups) { 292 val bisSync = subgroupBisSyncList.getOrNull(i) 293 val bisNum = subgroupNumOfBisesList.getOrNull(i) 294 val metadata = subgroupMetadataList.getOrNull(i) 295 296 val channels = convertToChannels(bisSync, bisNum) 297 val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder() 298 .setAudioLocation(0).build() 299 val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply { 300 setCodecId(SUBGROUP_LC3_CODEC_ID) 301 setCodecSpecificConfig(audioCodecConfigMetadata) 302 setContentMetadata( 303 BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0))) 304 channels.forEach(::addChannel) 305 }.build() 306 307 Log.d(TAG, "parseQrCodeToMetadata: subgroup $i elements bisSync=$bisSync, " + 308 "bisNum=$bisNum, metadata=${metadata != null}") 309 310 builder.addSubgroup(subgroup) 311 } 312 313 builder.apply { 314 setSourceDevice(device, addrType) 315 setSourceAdvertisingSid(sourceAdvertiserSid) 316 setBroadcastId(broadcastId) 317 setBroadcastName(broadcastName) 318 // QR code should set PBP(public broadcast profile) for auracast 319 setPublicBroadcast(true) 320 setPublicBroadcastMetadata(streamMetadata) 321 setPaSyncInterval(paSyncInterval) 322 setEncrypted(broadcastCode != null) 323 setBroadcastCode(broadcastCode) 324 // Presentation delay is unknown and not useful when adding source 325 // Broadcast sink needs to sync to the Broadcast source to get presentation delay 326 setPresentationDelayMicros(0) 327 setAudioConfigQuality(audioConfigQuality) 328 } 329 return builder.build() 330 } 331 332 private fun getBisSyncFromChannels( 333 channels: List<BluetoothLeBroadcastChannel> 334 ): Pair<UInt, UInt> { 335 var bisSync = 0u 336 var bisCount = 0u 337 // channel index starts from 1 338 channels.forEach { channel -> 339 if (channel.channelIndex > 0) { 340 bisCount++ 341 if (channel.isSelected) { 342 bisSync = bisSync or (1u shl (channel.channelIndex - 1)) 343 } 344 } 345 } 346 // No channel is selected means no preference on Android platform 347 return if (bisSync == 0u) Pair(BIS_SYNC_NO_PREFERENCE, bisCount) 348 else Pair(bisSync, bisCount) 349 } 350 351 private fun convertToChannels( 352 bisSync: UInt?, 353 bisNum: UInt? 354 ): List<BluetoothLeBroadcastChannel> { 355 Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisNum=$bisNum") 356 // if no BIS_SYNC or BIS_NUM available or BIS_SYNC is no preference 357 // return empty channel map with one placeholder channel 358 var selectedChannels = if (bisSync != null && bisNum != null) bisSync else 0u 359 val channels = mutableListOf<BluetoothLeBroadcastChannel>() 360 val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder() 361 .setAudioLocation(0).build() 362 363 if (bisSync == BIS_SYNC_NO_PREFERENCE || selectedChannels == 0u) { 364 // No channel preference means no channel is selected 365 // Generate one placeholder channel for metadata 366 val channel = BluetoothLeBroadcastChannel.Builder().apply { 367 setSelected(false) 368 setChannelIndex(1) 369 setCodecMetadata(audioCodecConfigMetadata) 370 } 371 return listOf(channel.build()) 372 } 373 374 for (i in 0 until BIS_SYNC_MAX_CHANNEL) { 375 val channelMask = 1u shl i 376 if ((selectedChannels and channelMask) != 0u) { 377 val channel = BluetoothLeBroadcastChannel.Builder().apply { 378 setSelected(true) 379 setChannelIndex(i + 1) 380 setCodecMetadata(audioCodecConfigMetadata) 381 } 382 channels.add(channel.build()) 383 } 384 } 385 return channels 386 } 387 }