1 /* 2 * 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 // BluetoothLeBroadcastMetadata 35 private const val KEY_BT_QR_VER = "R" 36 private const val KEY_BT_ADDRESS_TYPE = "T" 37 private const val KEY_BT_DEVICE = "D" 38 private const val KEY_BT_ADVERTISING_SID = "AS" 39 private const val KEY_BT_BROADCAST_ID = "B" 40 private const val KEY_BT_BROADCAST_NAME = "BN" 41 private const val KEY_BT_PUBLIC_BROADCAST_DATA = "PM" 42 private const val KEY_BT_SYNC_INTERVAL = "SI" 43 private const val KEY_BT_BROADCAST_CODE = "C" 44 private const val KEY_BT_SUBGROUPS = "SG" 45 private const val KEY_BT_VENDOR_SPECIFIC = "V" 46 private const val KEY_BT_ANDROID_VERSION = "VN" 47 48 // Subgroup data 49 private const val KEY_BTSG_BIS_SYNC = "BS" 50 private const val KEY_BTSG_BIS_MASK = "BM" 51 private const val KEY_BTSG_AUDIO_CONTENT = "AC" 52 53 // Vendor specific data 54 private const val KEY_BTVSD_COMPANY_ID = "VI" 55 private const val KEY_BTVSD_VENDOR_DATA = "VD" 56 57 private const val DELIMITER_KEY_VALUE = ":" 58 private const val DELIMITER_BT_LEVEL_1 = ";" 59 private const val DELIMITER_BT_LEVEL_2 = "," 60 61 private const val SUFFIX_QR_CODE = ";;" 62 63 private const val ANDROID_VER = "U" 64 private const val QR_CODE_VER = 0x010000 65 66 // BT constants 67 private const val BIS_SYNC_MAX_CHANNEL = 32 68 private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu 69 private const val SUBGROUP_LC3_CODEC_ID = 0x6L 70 71 /** 72 * Converts [BluetoothLeBroadcastMetadata] to QR code string. 73 * 74 * QR code string will prefix with "BT:". 75 */ 76 fun BluetoothLeBroadcastMetadata.toQrCodeString(): String { 77 val entries = mutableListOf<Pair<String, String>>() 78 entries.add(Pair(KEY_BT_QR_VER, QR_CODE_VER.toString())) 79 entries.add(Pair(KEY_BT_ADDRESS_TYPE, this.sourceAddressType.toString())) 80 entries.add(Pair(KEY_BT_DEVICE, this.sourceDevice.address.replace(":", "-"))) 81 entries.add(Pair(KEY_BT_ADVERTISING_SID, this.sourceAdvertisingSid.toString())) 82 entries.add(Pair(KEY_BT_BROADCAST_ID, this.broadcastId.toString())) 83 if (this.broadcastName != null) { 84 entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString( 85 this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP))) 86 } 87 if (this.publicBroadcastMetadata != null) { 88 entries.add(Pair(KEY_BT_PUBLIC_BROADCAST_DATA, Base64.encodeToString( 89 this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP))) 90 } 91 entries.add(Pair(KEY_BT_SYNC_INTERVAL, this.paSyncInterval.toString())) 92 if (this.broadcastCode != null) { 93 entries.add(Pair(KEY_BT_BROADCAST_CODE, 94 Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP))) 95 } 96 this.subgroups.forEach { 97 subgroup -> entries.add(Pair(KEY_BT_SUBGROUPS, subgroup.toQrCodeString())) } 98 entries.add(Pair(KEY_BT_ANDROID_VERSION, ANDROID_VER)) 99 val qrCodeString = SCHEME_BT_BROADCAST_METADATA + 100 entries.toQrCodeString(DELIMITER_BT_LEVEL_1) + SUFFIX_QR_CODE 101 Log.d(TAG, "Generated QR string : $qrCodeString") 102 return qrCodeString 103 } 104 105 /** 106 * Converts QR code string to [BluetoothLeBroadcastMetadata]. 107 * 108 * QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:". 109 */ 110 fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? { 111 if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) { 112 Log.e(TAG, "String \"$qrCodeString\" does not begin with " + 113 "\"$SCHEME_BT_BROADCAST_METADATA\"") 114 return null 115 } 116 return try { 117 Log.d(TAG, "Parsing QR string: $qrCodeString") 118 val strippedString = 119 qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA) 120 .removeSuffix(SUFFIX_QR_CODE) 121 Log.d(TAG, "Stripped to: $strippedString") 122 parseQrCodeToMetadata(strippedString) 123 } catch (e: Exception) { 124 Log.w(TAG, "Cannot parse: $qrCodeString", e) 125 null 126 } 127 } 128 129 private fun BluetoothLeBroadcastSubgroup.toQrCodeString(): String { 130 val entries = mutableListOf<Pair<String, String>>() 131 entries.add(Pair(KEY_BTSG_BIS_SYNC, getBisSyncFromChannels(this.channels).toString())) 132 entries.add(Pair(KEY_BTSG_BIS_MASK, getBisMaskFromChannels(this.channels).toString())) 133 entries.add(Pair(KEY_BTSG_AUDIO_CONTENT, 134 Base64.encodeToString(this.contentMetadata.rawMetadata, Base64.NO_WRAP))) 135 return entries.toQrCodeString(DELIMITER_BT_LEVEL_2) 136 } 137 138 private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String { 139 val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second } 140 return entryStrings.joinToString(separator = delimiter) 141 } 142 143 @TargetApi(Build.VERSION_CODES.TIRAMISU) 144 private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata { 145 // Split into a list of list 146 val level1Fields = input.split(DELIMITER_BT_LEVEL_1) 147 .map{it.split(DELIMITER_KEY_VALUE, limit = 2)} 148 var qrCodeVersion = -1 149 var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN 150 var sourceAddrString: String? = null 151 var sourceAdvertiserSid = -1 152 var broadcastId = -1 153 var broadcastName: String? = null 154 var publicBroadcastMetadata: BluetoothLeAudioContentMetadata? = null 155 var paSyncInterval = -1 156 var broadcastCode: ByteArray? = null 157 // List of VendorID -> Data Pairs 158 var vendorDataList = mutableListOf<Pair<Int, ByteArray?>>() 159 var androidVersion: String? = null 160 val builder = BluetoothLeBroadcastMetadata.Builder() 161 162 for (field: List<String> in level1Fields) { 163 if (field.isEmpty()) { 164 continue 165 } 166 val key = field[0] 167 // Ignore 3rd value and after 168 val value = if (field.size > 1) field[1] else "" 169 when (key) { 170 KEY_BT_QR_VER -> { 171 require(qrCodeVersion == -1) { "Duplicate qrCodeVersion: $input" } 172 qrCodeVersion = value.toInt() 173 } 174 KEY_BT_ADDRESS_TYPE -> { 175 require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) { 176 "Duplicate sourceAddrType: $input" 177 } 178 sourceAddrType = value.toInt() 179 } 180 KEY_BT_DEVICE -> { 181 require(sourceAddrString == null) { "Duplicate sourceAddr: $input" } 182 sourceAddrString = value.replace("-", ":") 183 } 184 KEY_BT_ADVERTISING_SID -> { 185 require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" } 186 sourceAdvertiserSid = value.toInt() 187 } 188 KEY_BT_BROADCAST_ID -> { 189 require(broadcastId == -1) { "Duplicate broadcastId: $input" } 190 broadcastId = value.toInt() 191 } 192 KEY_BT_BROADCAST_NAME -> { 193 require(broadcastName == null) { "Duplicate broadcastName: $input" } 194 broadcastName = String(Base64.decode(value, Base64.NO_WRAP)) 195 } 196 KEY_BT_PUBLIC_BROADCAST_DATA -> { 197 require(publicBroadcastMetadata == null) { 198 "Duplicate publicBroadcastMetadata $input" 199 } 200 publicBroadcastMetadata = BluetoothLeAudioContentMetadata 201 .fromRawBytes(Base64.decode(value, Base64.NO_WRAP)) 202 } 203 KEY_BT_SYNC_INTERVAL -> { 204 require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" } 205 paSyncInterval = value.toInt() 206 } 207 KEY_BT_BROADCAST_CODE -> { 208 require(broadcastCode == null) { "Duplicate broadcastCode: $input" } 209 broadcastCode = Base64.decode(value, Base64.NO_WRAP) 210 } 211 KEY_BT_ANDROID_VERSION -> { 212 require(androidVersion == null) { "Duplicate androidVersion: $input" } 213 androidVersion = value 214 Log.i(TAG, "QR code Android version: $androidVersion") 215 } 216 // Repeatable 217 KEY_BT_SUBGROUPS -> { 218 builder.addSubgroup(parseSubgroupData(value)) 219 } 220 // Repeatable 221 KEY_BT_VENDOR_SPECIFIC -> { 222 vendorDataList.add(parseVendorData(value)) 223 } 224 } 225 } 226 Log.d(TAG, "parseQrCodeToMetadata: sourceAddrType=$sourceAddrType, " + 227 "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " + 228 "broadcastId=$broadcastId, broadcastName=$broadcastName, " + 229 "publicBroadcastMetadata=${publicBroadcastMetadata != null}, " + 230 "paSyncInterval=$paSyncInterval, " + 231 "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}") 232 Log.d(TAG, "Not used in current code, but part of the specification: " + 233 "qrCodeVersion=$qrCodeVersion, androidVersion=$androidVersion, " + 234 "vendorDataListSize=${vendorDataList.size}") 235 val adapter = BluetoothAdapter.getDefaultAdapter() 236 // add source device and set broadcast code 237 val device = adapter.getRemoteLeDevice(requireNotNull(sourceAddrString), sourceAddrType) 238 builder.apply { 239 setSourceDevice(device, sourceAddrType) 240 setSourceAdvertisingSid(sourceAdvertiserSid) 241 setBroadcastId(broadcastId) 242 setBroadcastName(broadcastName) 243 setPublicBroadcast(publicBroadcastMetadata != null) 244 setPublicBroadcastMetadata(publicBroadcastMetadata) 245 setPaSyncInterval(paSyncInterval) 246 setEncrypted(broadcastCode != null) 247 setBroadcastCode(broadcastCode) 248 // Presentation delay is unknown and not useful when adding source 249 // Broadcast sink needs to sync to the Broadcast source to get presentation delay 250 setPresentationDelayMicros(0) 251 } 252 return builder.build() 253 } 254 255 private fun parseSubgroupData(input: String): BluetoothLeBroadcastSubgroup { 256 Log.d(TAG, "parseSubgroupData: $input") 257 val fields = input.split(DELIMITER_BT_LEVEL_2) 258 var bisSync: UInt? = null 259 var bisMask: UInt? = null 260 var metadata: ByteArray? = null 261 262 fields.forEach { field -> 263 val(key, value) = field.split(DELIMITER_KEY_VALUE) 264 when (key) { 265 KEY_BTSG_BIS_SYNC -> { 266 require(bisSync == null) { "Duplicate bisSync: $input" } 267 bisSync = value.toUInt() 268 } 269 KEY_BTSG_BIS_MASK -> { 270 require(bisMask == null) { "Duplicate bisMask: $input" } 271 bisMask = value.toUInt() 272 } 273 KEY_BTSG_AUDIO_CONTENT -> { 274 require(metadata == null) { "Duplicate metadata: $input" } 275 metadata = Base64.decode(value, Base64.NO_WRAP) 276 } 277 } 278 } 279 val channels = convertToChannels(requireNotNull(bisSync), requireNotNull(bisMask)) 280 val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder() 281 .setAudioLocation(0).build() 282 return BluetoothLeBroadcastSubgroup.Builder().apply { 283 setCodecId(SUBGROUP_LC3_CODEC_ID) 284 setCodecSpecificConfig(audioCodecConfigMetadata) 285 setContentMetadata( 286 BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0))) 287 channels.forEach(::addChannel) 288 }.build() 289 } 290 291 private fun parseVendorData(input: String): Pair<Int, ByteArray?> { 292 var companyId = -1 293 var data: ByteArray? = null 294 val fields = input.split(DELIMITER_BT_LEVEL_2) 295 fields.forEach { field -> 296 val(key, value) = field.split(DELIMITER_KEY_VALUE) 297 when (key) { 298 KEY_BTVSD_COMPANY_ID -> { 299 require(companyId == -1) { "Duplicate companyId: $input" } 300 companyId = value.toInt() 301 } 302 KEY_BTVSD_VENDOR_DATA -> { 303 require(data == null) { "Duplicate data: $input" } 304 data = Base64.decode(value, Base64.NO_WRAP) 305 } 306 } 307 } 308 return Pair(companyId, data) 309 } 310 311 private fun getBisSyncFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt { 312 var bisSync = 0u 313 // channel index starts from 1 314 channels.forEach { channel -> 315 if (channel.isSelected && channel.channelIndex > 0) { 316 bisSync = bisSync or (1u shl (channel.channelIndex - 1)) 317 } 318 } 319 // No channel is selected means no preference on Android platform 320 return if (bisSync == 0u) BIS_SYNC_NO_PREFERENCE else bisSync 321 } 322 323 private fun getBisMaskFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt { 324 var bisMask = 0u 325 // channel index starts from 1 326 channels.forEach { channel -> 327 if (channel.channelIndex > 0) { 328 bisMask = bisMask or (1u shl (channel.channelIndex - 1)) 329 } 330 } 331 return bisMask 332 } 333 334 private fun convertToChannels(bisSync: UInt, bisMask: UInt): 335 List<BluetoothLeBroadcastChannel> { 336 Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisMask=$bisMask") 337 var selectionMask = bisSync 338 if (bisSync != BIS_SYNC_NO_PREFERENCE) { 339 require(bisMask == (bisMask or bisSync)) { 340 "bisSync($bisSync) must select a subset of bisMask($bisMask) if it has preferences" 341 } 342 } else { 343 // No channel preference means no channel is selected 344 selectionMask = 0u 345 } 346 val channels = mutableListOf<BluetoothLeBroadcastChannel>() 347 val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder() 348 .setAudioLocation(0).build() 349 for (i in 0 until BIS_SYNC_MAX_CHANNEL) { 350 val channelMask = 1u shl i 351 if ((bisMask and channelMask) != 0u) { 352 val channel = BluetoothLeBroadcastChannel.Builder().apply { 353 setSelected((selectionMask and channelMask) != 0u) 354 setChannelIndex(i + 1) 355 setCodecMetadata(audioCodecConfigMetadata) 356 } 357 channels.add(channel.build()) 358 } 359 } 360 return channels 361 } 362 }