• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }