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 }