From 8cc1ff3c87446f9aea9810e6d1a211fb882ef85f Mon Sep 17 00:00:00 2001
From: Rongxuan Liu <rongxuan@google.com>
Date: Mon, 6 May 2024 05:55:46 +0000
Subject: [PATCH] [Audiosharing] Update QR code format to follow BT SIG
 standard

Bug: 324655158
Test: atest com.android.settingslib.bluetooth.BluetoothLeBroadcastMetadataExtTest
Test: manually tested with broadcast source and assistant by QR code
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:4abfa5bed1a35a9a9b5ac54951296b3edde4ee9a)
Merged-In: I798666b89c4e3558c7528bace0346caf8218dc53
Change-Id: I798666b89c4e3558c7528bace0346caf8218dc53
---
 .../bluetooth/BluetoothBroadcastUtils.java    |   2 +-
 .../BluetoothLeBroadcastMetadataExt.kt        | 397 ++++++++++--------
 .../BluetoothLeBroadcastMetadataExtTest.kt    |  74 +++-
 3 files changed, 272 insertions(+), 201 deletions(-)

diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
index 1ff2befd19ed..5e3bd9a66e88 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java
@@ -43,5 +43,5 @@ public final class BluetoothBroadcastUtils {
     /**
      * Bluetooth scheme.
      */
-    public static final String SCHEME_BT_BROADCAST_METADATA = "BT:";
+    public static final String SCHEME_BT_BROADCAST_METADATA = "BLUETOOTH:UUID:184F;";
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
index 9bb11f8da645..da1fd55c4ccb 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt
@@ -31,38 +31,34 @@ import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROAD
 object BluetoothLeBroadcastMetadataExt {
     private const val TAG = "BtLeBroadcastMetadataExt"
 
-    // BluetoothLeBroadcastMetadata
-    private const val KEY_BT_QR_VER = "R"
-    private const val KEY_BT_ADDRESS_TYPE = "T"
-    private const val KEY_BT_DEVICE = "D"
-    private const val KEY_BT_ADVERTISING_SID = "AS"
-    private const val KEY_BT_BROADCAST_ID = "B"
+    // Data Elements for directing Broadcast Assistants
     private const val KEY_BT_BROADCAST_NAME = "BN"
-    private const val KEY_BT_PUBLIC_BROADCAST_DATA = "PM"
-    private const val KEY_BT_SYNC_INTERVAL = "SI"
-    private const val KEY_BT_BROADCAST_CODE = "C"
-    private const val KEY_BT_SUBGROUPS = "SG"
-    private const val KEY_BT_VENDOR_SPECIFIC = "V"
-    private const val KEY_BT_ANDROID_VERSION = "VN"
-
-    // Subgroup data
+    private const val KEY_BT_ADVERTISER_ADDRESS_TYPE = "AT"
+    private const val KEY_BT_ADVERTISER_ADDRESS = "AD"
+    private const val KEY_BT_BROADCAST_ID = "BI"
+    private const val KEY_BT_BROADCAST_CODE = "BC"
+    private const val KEY_BT_STREAM_METADATA = "MD"
+    private const val KEY_BT_STANDARD_QUALITY = "SQ"
+    private const val KEY_BT_HIGH_QUALITY = "HQ"
+
+    // Extended Bluetooth URI Data Elements
+    private const val KEY_BT_ADVERTISING_SID = "AS"
+    private const val KEY_BT_PA_INTERVAL = "PI"
+    private const val KEY_BT_NUM_SUBGROUPS = "NS"
+
+    // Subgroup data elements
     private const val KEY_BTSG_BIS_SYNC = "BS"
-    private const val KEY_BTSG_BIS_MASK = "BM"
-    private const val KEY_BTSG_AUDIO_CONTENT = "AC"
+    private const val KEY_BTSG_NUM_BISES = "NB"
+    private const val KEY_BTSG_METADATA = "SM"
 
-    // Vendor specific data
-    private const val KEY_BTVSD_COMPANY_ID = "VI"
-    private const val KEY_BTVSD_VENDOR_DATA = "VD"
+    // Vendor specific data, not being used
+    private const val KEY_BTVSD_VENDOR_DATA = "VS"
 
     private const val DELIMITER_KEY_VALUE = ":"
-    private const val DELIMITER_BT_LEVEL_1 = ";"
-    private const val DELIMITER_BT_LEVEL_2 = ","
+    private const val DELIMITER_ELEMENT = ";"
 
     private const val SUFFIX_QR_CODE = ";;"
 
-    private const val ANDROID_VER = "U"
-    private const val QR_CODE_VER = 0x010000
-
     // BT constants
     private const val BIS_SYNC_MAX_CHANNEL = 32
     private const val BIS_SYNC_NO_PREFERENCE = 0xFFFFFFFFu
@@ -71,33 +67,55 @@ object BluetoothLeBroadcastMetadataExt {
     /**
      * Converts [BluetoothLeBroadcastMetadata] to QR code string.
      *
-     * QR code string will prefix with "BT:".
+     * QR code string will prefix with "BLUETOOTH:UUID:184F".
      */
     fun BluetoothLeBroadcastMetadata.toQrCodeString(): String {
         val entries = mutableListOf<Pair<String, String>>()
-        entries.add(Pair(KEY_BT_QR_VER, QR_CODE_VER.toString()))
-        entries.add(Pair(KEY_BT_ADDRESS_TYPE, this.sourceAddressType.toString()))
-        entries.add(Pair(KEY_BT_DEVICE, this.sourceDevice.address.replace(":", "-")))
-        entries.add(Pair(KEY_BT_ADVERTISING_SID, this.sourceAdvertisingSid.toString()))
-        entries.add(Pair(KEY_BT_BROADCAST_ID, this.broadcastId.toString()))
-        if (this.broadcastName != null) {
-            entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
-                this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
-        }
-        if (this.publicBroadcastMetadata != null) {
-            entries.add(Pair(KEY_BT_PUBLIC_BROADCAST_DATA, Base64.encodeToString(
-                this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
-        }
-        entries.add(Pair(KEY_BT_SYNC_INTERVAL, this.paSyncInterval.toString()))
+        // Generate data elements for directing Broadcast Assistants
+        require(this.broadcastName != null) { "Broadcast name is mandatory for QR code" }
+        entries.add(Pair(KEY_BT_BROADCAST_NAME, Base64.encodeToString(
+            this.broadcastName?.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)))
+        entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS_TYPE, this.sourceAddressType.toString()))
+        entries.add(Pair(KEY_BT_ADVERTISER_ADDRESS, this.sourceDevice.address.replace(":", "")))
+        entries.add(Pair(KEY_BT_BROADCAST_ID, String.format("%X", this.broadcastId.toLong())))
         if (this.broadcastCode != null) {
             entries.add(Pair(KEY_BT_BROADCAST_CODE,
                 Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP)))
         }
+        if (this.publicBroadcastMetadata != null &&
+                this.publicBroadcastMetadata?.rawMetadata?.size != 0) {
+            entries.add(Pair(KEY_BT_STREAM_METADATA, Base64.encodeToString(
+                this.publicBroadcastMetadata?.rawMetadata, Base64.NO_WRAP)))
+        }
+        if ((this.audioConfigQuality and
+                BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD) != 0) {
+            entries.add(Pair(KEY_BT_STANDARD_QUALITY, "1"))
+        }
+        if ((this.audioConfigQuality and
+                BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_HIGH) != 0) {
+            entries.add(Pair(KEY_BT_HIGH_QUALITY, "1"))
+        }
+
+        // Generate extended Bluetooth URI data elements
+        entries.add(Pair(KEY_BT_ADVERTISING_SID,
+                String.format("%X", this.sourceAdvertisingSid.toLong())))
+        entries.add(Pair(KEY_BT_PA_INTERVAL, String.format("%X", this.paSyncInterval.toLong())))
+        entries.add(Pair(KEY_BT_NUM_SUBGROUPS, String.format("%X", this.subgroups.size.toLong())))
+
         this.subgroups.forEach {
-                subgroup -> entries.add(Pair(KEY_BT_SUBGROUPS, subgroup.toQrCodeString())) }
-        entries.add(Pair(KEY_BT_ANDROID_VERSION, ANDROID_VER))
+            val (bisSync, bisCount) = getBisSyncFromChannels(it.channels)
+            entries.add(Pair(KEY_BTSG_BIS_SYNC, String.format("%X", bisSync.toLong())))
+            if (bisCount > 0u) {
+                entries.add(Pair(KEY_BTSG_NUM_BISES, String.format("%X", bisCount.toLong())))
+            }
+            if (it.contentMetadata.rawMetadata.size != 0) {
+                entries.add(Pair(KEY_BTSG_METADATA,
+                    Base64.encodeToString(it.contentMetadata.rawMetadata, Base64.NO_WRAP)))
+            }
+        }
+
         val qrCodeString = SCHEME_BT_BROADCAST_METADATA +
-                entries.toQrCodeString(DELIMITER_BT_LEVEL_1) + SUFFIX_QR_CODE
+                entries.toQrCodeString(DELIMITER_ELEMENT) + SUFFIX_QR_CODE
         Log.d(TAG, "Generated QR string : $qrCodeString")
         return qrCodeString
     }
@@ -105,7 +123,7 @@ object BluetoothLeBroadcastMetadataExt {
     /**
      * Converts QR code string to [BluetoothLeBroadcastMetadata].
      *
-     * QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
+     * QR code string should prefix with "BLUETOOTH:UUID:184F".
      */
     fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
         if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) {
@@ -126,15 +144,6 @@ object BluetoothLeBroadcastMetadataExt {
         }
     }
 
-    private fun BluetoothLeBroadcastSubgroup.toQrCodeString(): String {
-        val entries = mutableListOf<Pair<String, String>>()
-        entries.add(Pair(KEY_BTSG_BIS_SYNC, getBisSyncFromChannels(this.channels).toString()))
-        entries.add(Pair(KEY_BTSG_BIS_MASK, getBisMaskFromChannels(this.channels).toString()))
-        entries.add(Pair(KEY_BTSG_AUDIO_CONTENT,
-            Base64.encodeToString(this.contentMetadata.rawMetadata, Base64.NO_WRAP)))
-        return entries.toQrCodeString(DELIMITER_BT_LEVEL_2)
-    }
-
     private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String {
         val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second }
         return entryStrings.joinToString(separator = delimiter)
@@ -143,23 +152,29 @@ object BluetoothLeBroadcastMetadataExt {
     @TargetApi(Build.VERSION_CODES.TIRAMISU)
     private fun parseQrCodeToMetadata(input: String): BluetoothLeBroadcastMetadata {
         // Split into a list of list
-        val level1Fields = input.split(DELIMITER_BT_LEVEL_1)
+        val elementFields = input.split(DELIMITER_ELEMENT)
             .map{it.split(DELIMITER_KEY_VALUE, limit = 2)}
-        var qrCodeVersion = -1
+
         var sourceAddrType = BluetoothDevice.ADDRESS_TYPE_UNKNOWN
         var sourceAddrString: String? = null
         var sourceAdvertiserSid = -1
         var broadcastId = -1
         var broadcastName: String? = null
-        var publicBroadcastMetadata: BluetoothLeAudioContentMetadata? = null
+        var streamMetadata: BluetoothLeAudioContentMetadata? = null
         var paSyncInterval = -1
         var broadcastCode: ByteArray? = null
-        // List of VendorID -> Data Pairs
-        var vendorDataList = mutableListOf<Pair<Int, ByteArray?>>()
-        var androidVersion: String? = null
+        var audioConfigQualityStandard = -1
+        var audioConfigQualityHigh = -1
+        var numSubgroups = -1
+
+        // List of subgroup data
+        var subgroupBisSyncList = mutableListOf<UInt>()
+        var subgroupNumOfBisesList = mutableListOf<UInt>()
+        var subgroupMetadataList = mutableListOf<ByteArray?>()
+
         val builder = BluetoothLeBroadcastMetadata.Builder()
 
-        for (field: List<String> in level1Fields) {
+        for (field: List<String> in elementFields) {
             if (field.isEmpty()) {
                 continue
             }
@@ -167,190 +182,200 @@ object BluetoothLeBroadcastMetadataExt {
             // Ignore 3rd value and after
             val value = if (field.size > 1) field[1] else ""
             when (key) {
-                KEY_BT_QR_VER -> {
-                    require(qrCodeVersion == -1) { "Duplicate qrCodeVersion: $input" }
-                    qrCodeVersion = value.toInt()
+                // Parse data elements for directing Broadcast Assistants
+                KEY_BT_BROADCAST_NAME -> {
+                    require(broadcastName == null) { "Duplicate broadcastName: $input" }
+                    broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
                 }
-                KEY_BT_ADDRESS_TYPE -> {
+                KEY_BT_ADVERTISER_ADDRESS_TYPE -> {
                     require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
                         "Duplicate sourceAddrType: $input"
                     }
                     sourceAddrType = value.toInt()
                 }
-                KEY_BT_DEVICE -> {
+                KEY_BT_ADVERTISER_ADDRESS -> {
                     require(sourceAddrString == null) { "Duplicate sourceAddr: $input" }
-                    sourceAddrString = value.replace("-", ":")
-                }
-                KEY_BT_ADVERTISING_SID -> {
-                    require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
-                    sourceAdvertiserSid = value.toInt()
+                    sourceAddrString = value.chunked(2).joinToString(":")
                 }
                 KEY_BT_BROADCAST_ID -> {
                     require(broadcastId == -1) { "Duplicate broadcastId: $input" }
-                    broadcastId = value.toInt()
+                    broadcastId = value.toInt(16)
                 }
-                KEY_BT_BROADCAST_NAME -> {
-                    require(broadcastName == null) { "Duplicate broadcastName: $input" }
-                    broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
+                KEY_BT_BROADCAST_CODE -> {
+                    require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
+
+                    broadcastCode = Base64.decode(value.dropLastWhile { it.equals(0.toByte()) }
+                            .toByteArray(), Base64.NO_WRAP)
                 }
-                KEY_BT_PUBLIC_BROADCAST_DATA -> {
-                    require(publicBroadcastMetadata == null) {
-                        "Duplicate publicBroadcastMetadata $input"
+                KEY_BT_STREAM_METADATA -> {
+                    require(streamMetadata == null) {
+                        "Duplicate streamMetadata $input"
                     }
-                    publicBroadcastMetadata = BluetoothLeAudioContentMetadata
+                    streamMetadata = BluetoothLeAudioContentMetadata
                         .fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
                 }
-                KEY_BT_SYNC_INTERVAL -> {
+                KEY_BT_STANDARD_QUALITY -> {
+                    require(audioConfigQualityStandard == -1) {
+                        "Duplicate audioConfigQualityStandard: $input"
+                    }
+                    audioConfigQualityStandard = value.toInt()
+                }
+                KEY_BT_HIGH_QUALITY -> {
+                    require(audioConfigQualityHigh == -1) {
+                        "Duplicate audioConfigQualityHigh: $input"
+                    }
+                    audioConfigQualityHigh = value.toInt()
+                }
+
+                // Parse extended Bluetooth URI data elements
+                KEY_BT_ADVERTISING_SID -> {
+                    require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
+                    sourceAdvertiserSid = value.toInt(16)
+                }
+                KEY_BT_PA_INTERVAL -> {
                     require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
-                    paSyncInterval = value.toInt()
+                    paSyncInterval = value.toInt(16)
                 }
-                KEY_BT_BROADCAST_CODE -> {
-                    require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
-                    broadcastCode = Base64.decode(value, Base64.NO_WRAP)
+                KEY_BT_NUM_SUBGROUPS -> {
+                    require(numSubgroups == -1) { "Duplicate numSubgroups: $input" }
+                    numSubgroups = value.toInt(16)
                 }
-                KEY_BT_ANDROID_VERSION -> {
-                    require(androidVersion == null) { "Duplicate androidVersion: $input" }
-                    androidVersion = value
-                    Log.i(TAG, "QR code Android version: $androidVersion")
+
+                // Repeatable subgroup elements
+                KEY_BTSG_BIS_SYNC -> {
+                    subgroupBisSyncList.add(value.toUInt(16))
                 }
-                // Repeatable
-                KEY_BT_SUBGROUPS -> {
-                    builder.addSubgroup(parseSubgroupData(value))
+                KEY_BTSG_NUM_BISES -> {
+                    subgroupNumOfBisesList.add(value.toUInt(16))
                 }
-                // Repeatable
-                KEY_BT_VENDOR_SPECIFIC -> {
-                    vendorDataList.add(parseVendorData(value))
+                KEY_BTSG_METADATA -> {
+                    subgroupMetadataList.add(Base64.decode(value, Base64.NO_WRAP))
                 }
             }
         }
-        Log.d(TAG, "parseQrCodeToMetadata: sourceAddrType=$sourceAddrType, " +
+        Log.d(TAG, "parseQrCodeToMetadata: main data elements sourceAddrType=$sourceAddrType, " +
                 "sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
                 "broadcastId=$broadcastId, broadcastName=$broadcastName, " +
-                "publicBroadcastMetadata=${publicBroadcastMetadata != null}, " +
+                "streamMetadata=${streamMetadata != null}, " +
                 "paSyncInterval=$paSyncInterval, " +
-                "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}")
-        Log.d(TAG, "Not used in current code, but part of the specification: " +
-                "qrCodeVersion=$qrCodeVersion, androidVersion=$androidVersion, " +
-                "vendorDataListSize=${vendorDataList.size}")
+                "broadcastCode=${broadcastCode?.toString(Charsets.UTF_8)}, " +
+                "audioConfigQualityStandard=$audioConfigQualityStandard, " +
+                "audioConfigQualityHigh=$audioConfigQualityHigh")
+
         val adapter = BluetoothAdapter.getDefaultAdapter()
+        // Check parsed elements data
+        require(broadcastName != null) {
+            "broadcastName($broadcastName) must present in QR code string"
+        }
+        var addr = sourceAddrString
+        var addrType = sourceAddrType
+        if (sourceAddrString != null) {
+            require(sourceAddrType != BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
+                "sourceAddrType($sourceAddrType) must present if address present"
+            }
+        } else {
+            // Use placeholder device if not present
+            addr = "FF:FF:FF:FF:FF:FF"
+            addrType = BluetoothDevice.ADDRESS_TYPE_RANDOM
+        }
+        val device = adapter.getRemoteLeDevice(requireNotNull(addr), addrType)
+
         // add source device and set broadcast code
-        val device = adapter.getRemoteLeDevice(requireNotNull(sourceAddrString), sourceAddrType)
+        var audioConfigQuality = BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_NONE or
+                (if (audioConfigQualityStandard != -1) audioConfigQualityStandard else 0) or
+                (if (audioConfigQualityHigh != -1) audioConfigQualityHigh else 0)
+
+        // process subgroup data
+        // metadata should include at least 1 subgroup for metadata, add a placeholder group if not present
+        numSubgroups = if (numSubgroups > 0) numSubgroups else 1
+        for (i in 0 until numSubgroups) {
+            val bisSync = subgroupBisSyncList.getOrNull(i)
+            val bisNum = subgroupNumOfBisesList.getOrNull(i)
+            val metadata = subgroupMetadataList.getOrNull(i)
+
+            val channels = convertToChannels(bisSync, bisNum)
+            val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
+                    .setAudioLocation(0).build()
+            val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
+                setCodecId(SUBGROUP_LC3_CODEC_ID)
+                setCodecSpecificConfig(audioCodecConfigMetadata)
+                setContentMetadata(
+                        BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
+                channels.forEach(::addChannel)
+            }.build()
+
+            Log.d(TAG, "parseQrCodeToMetadata: subgroup $i elements bisSync=$bisSync, " +
+                    "bisNum=$bisNum, metadata=${metadata != null}")
+
+            builder.addSubgroup(subgroup)
+        }
+
         builder.apply {
             setSourceDevice(device, sourceAddrType)
             setSourceAdvertisingSid(sourceAdvertiserSid)
             setBroadcastId(broadcastId)
             setBroadcastName(broadcastName)
-            setPublicBroadcast(publicBroadcastMetadata != null)
-            setPublicBroadcastMetadata(publicBroadcastMetadata)
+            // QR code should set PBP(public broadcast profile) for auracast
+            setPublicBroadcast(true)
+            setPublicBroadcastMetadata(streamMetadata)
             setPaSyncInterval(paSyncInterval)
             setEncrypted(broadcastCode != null)
             setBroadcastCode(broadcastCode)
             // Presentation delay is unknown and not useful when adding source
             // Broadcast sink needs to sync to the Broadcast source to get presentation delay
             setPresentationDelayMicros(0)
+            setAudioConfigQuality(audioConfigQuality)
         }
         return builder.build()
     }
 
-    private fun parseSubgroupData(input: String): BluetoothLeBroadcastSubgroup {
-        Log.d(TAG, "parseSubgroupData: $input")
-        val fields = input.split(DELIMITER_BT_LEVEL_2)
-        var bisSync: UInt? = null
-        var bisMask: UInt? = null
-        var metadata: ByteArray? = null
-
-        fields.forEach { field ->
-            val(key, value) = field.split(DELIMITER_KEY_VALUE)
-            when (key) {
-                KEY_BTSG_BIS_SYNC -> {
-                    require(bisSync == null) { "Duplicate bisSync: $input" }
-                    bisSync = value.toUInt()
-                }
-                KEY_BTSG_BIS_MASK -> {
-                    require(bisMask == null) { "Duplicate bisMask: $input" }
-                    bisMask = value.toUInt()
-                }
-                KEY_BTSG_AUDIO_CONTENT -> {
-                    require(metadata == null) { "Duplicate metadata: $input" }
-                    metadata = Base64.decode(value, Base64.NO_WRAP)
-                }
-            }
-        }
-        val channels = convertToChannels(requireNotNull(bisSync), requireNotNull(bisMask))
-        val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
-                .setAudioLocation(0).build()
-        return BluetoothLeBroadcastSubgroup.Builder().apply {
-            setCodecId(SUBGROUP_LC3_CODEC_ID)
-            setCodecSpecificConfig(audioCodecConfigMetadata)
-            setContentMetadata(
-                    BluetoothLeAudioContentMetadata.fromRawBytes(metadata ?: ByteArray(0)))
-            channels.forEach(::addChannel)
-        }.build()
-    }
-
-    private fun parseVendorData(input: String): Pair<Int, ByteArray?> {
-        var companyId = -1
-        var data: ByteArray? = null
-        val fields = input.split(DELIMITER_BT_LEVEL_2)
-        fields.forEach { field ->
-            val(key, value) = field.split(DELIMITER_KEY_VALUE)
-            when (key) {
-                KEY_BTVSD_COMPANY_ID -> {
-                    require(companyId == -1) { "Duplicate companyId: $input" }
-                    companyId = value.toInt()
-                }
-                KEY_BTVSD_VENDOR_DATA -> {
-                    require(data == null) { "Duplicate data: $input" }
-                    data = Base64.decode(value, Base64.NO_WRAP)
-                }
-            }
-        }
-        return Pair(companyId, data)
-    }
-
-    private fun getBisSyncFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt {
+    private fun getBisSyncFromChannels(
+        channels: List<BluetoothLeBroadcastChannel>
+    ): Pair<UInt, UInt> {
         var bisSync = 0u
-        // channel index starts from 1
-        channels.forEach { channel ->
-            if (channel.isSelected && channel.channelIndex > 0) {
-                bisSync = bisSync or (1u shl (channel.channelIndex - 1))
-            }
-        }
-        // No channel is selected means no preference on Android platform
-        return if (bisSync == 0u) BIS_SYNC_NO_PREFERENCE else bisSync
-    }
-
-    private fun getBisMaskFromChannels(channels: List<BluetoothLeBroadcastChannel>): UInt {
-        var bisMask = 0u
+        var bisCount = 0u
         // channel index starts from 1
         channels.forEach { channel ->
             if (channel.channelIndex > 0) {
-                bisMask = bisMask or (1u shl (channel.channelIndex - 1))
+                bisCount++
+                if (channel.isSelected) {
+                    bisSync = bisSync or (1u shl (channel.channelIndex - 1))
+                }
             }
         }
-        return bisMask
+        // No channel is selected means no preference on Android platform
+        return if (bisSync == 0u) Pair(BIS_SYNC_NO_PREFERENCE, bisCount)
+                else Pair(bisSync, bisCount)
     }
 
-    private fun convertToChannels(bisSync: UInt, bisMask: UInt):
-            List<BluetoothLeBroadcastChannel> {
-        Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisMask=$bisMask")
-        var selectionMask = bisSync
-        if (bisSync != BIS_SYNC_NO_PREFERENCE) {
-            require(bisMask == (bisMask or bisSync)) {
-                "bisSync($bisSync) must select a subset of bisMask($bisMask) if it has preferences"
-            }
-        } else {
-            // No channel preference means no channel is selected
-            selectionMask = 0u
-        }
+    private fun convertToChannels(
+        bisSync: UInt?,
+        bisNum: UInt?
+    ): List<BluetoothLeBroadcastChannel> {
+        Log.d(TAG, "convertToChannels: bisSync=$bisSync, bisNum=$bisNum")
+        // if no BIS_SYNC or BIS_NUM available or BIS_SYNC is no preference
+        // return empty channel map with one placeholder channel
+        var selectedChannels = if (bisSync != null && bisNum != null) bisSync else 0u
         val channels = mutableListOf<BluetoothLeBroadcastChannel>()
         val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
                 .setAudioLocation(0).build()
+
+        if (bisSync == BIS_SYNC_NO_PREFERENCE || selectedChannels == 0u) {
+            // No channel preference means no channel is selected
+            // Generate one placeholder channel for metadata
+            val channel = BluetoothLeBroadcastChannel.Builder().apply {
+                setSelected(false)
+                setChannelIndex(1)
+                setCodecMetadata(audioCodecConfigMetadata)
+            }
+            return listOf(channel.build())
+        }
+
         for (i in 0 until BIS_SYNC_MAX_CHANNEL) {
             val channelMask = 1u shl i
-            if ((bisMask and channelMask) != 0u) {
+            if ((selectedChannels and channelMask) != 0u) {
                 val channel = BluetoothLeBroadcastChannel.Builder().apply {
-                    setSelected((selectionMask and channelMask) != 0u)
+                    setSelected(true)
                     setChannelIndex(i + 1)
                     setCodecMetadata(audioCodecConfigMetadata)
                 }
diff --git a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
index 27d7078774d5..1ad20dc02042 100644
--- a/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
+++ b/packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt
@@ -32,7 +32,7 @@ import org.junit.runner.RunWith
 class BluetoothLeBroadcastMetadataExtTest {
 
     @Test
-    fun toQrCodeString() {
+    fun toQrCodeString_encrypted() {
         val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
             setCodecId(0x6)
             val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
@@ -69,6 +69,37 @@ class BluetoothLeBroadcastMetadataExtTest {
         assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
     }
 
+    @Test
+    fun toQrCodeString_non_encrypted() {
+        val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
+            setCodecId(0x6)
+            val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
+            setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
+                .build())
+            setCodecSpecificConfig(audioCodecConfigMetadata)
+            addChannel(BluetoothLeBroadcastChannel.Builder().apply {
+                setSelected(true)
+                setChannelIndex(1)
+                setCodecMetadata(audioCodecConfigMetadata)
+            }.build())
+        }.build()
+
+        val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
+            setSourceDevice(DevicePublic, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+            setSourceAdvertisingSid(1)
+            setBroadcastId(0xDE51E9)
+            setBroadcastName("Hockey")
+            setAudioConfigQuality(BluetoothLeBroadcastMetadata.AUDIO_CONFIG_QUALITY_STANDARD)
+            setPaSyncInterval(0xFFFF)
+            setEncrypted(false)
+            addSubgroup(subgroup)
+        }.build()
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING_NON_ENCRYPTED)
+    }
+
     @Test
     fun toQrCodeString_NoChannelSelected() {
         val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
@@ -102,6 +133,7 @@ class BluetoothLeBroadcastMetadataExtTest {
             addSubgroup(subgroup)
         }.build()
 
+        // if no channel is selected, no preference(0xFFFFFFFFu) will be set in BIS
         val qrCodeString = metadata.toQrCodeString()
 
         val parsedMetadata =
@@ -111,13 +143,11 @@ class BluetoothLeBroadcastMetadataExtTest {
         assertThat(parsedMetadata.subgroups).isNotNull()
         assertThat(parsedMetadata.subgroups.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels).isNotNull()
-        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(2)
+        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].hasChannelPreference()).isFalse()
-        // Input order does not matter due to parsing through bisMask
+        // placeholder channel with not selected
         assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isFalse()
-        assertThat(parsedMetadata.subgroups[0].channels[1].channelIndex).isEqualTo(2)
-        assertThat(parsedMetadata.subgroups[0].channels[1].isSelected).isFalse()
     }
 
     @Test
@@ -162,13 +192,11 @@ class BluetoothLeBroadcastMetadataExtTest {
         assertThat(parsedMetadata.subgroups).isNotNull()
         assertThat(parsedMetadata.subgroups.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].channels).isNotNull()
-        // Only selected channel can be recovered
-        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(2)
+        // Only selected channel can be recovered, non-selected ones will be ignored
+        assertThat(parsedMetadata.subgroups[0].channels.size).isEqualTo(1)
         assertThat(parsedMetadata.subgroups[0].hasChannelPreference()).isTrue()
-        assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(1)
-        assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isFalse()
-        assertThat(parsedMetadata.subgroups[0].channels[1].channelIndex).isEqualTo(2)
-        assertThat(parsedMetadata.subgroups[0].channels[1].isSelected).isTrue()
+        assertThat(parsedMetadata.subgroups[0].channels[0].channelIndex).isEqualTo(2)
+        assertThat(parsedMetadata.subgroups[0].channels[0].isSelected).isTrue()
     }
 
     @Test
@@ -180,16 +208,34 @@ class BluetoothLeBroadcastMetadataExtTest {
         assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
     }
 
+    @Test
+    fun decodeAndEncodeAgain_sameString_non_encrypted() {
+        val metadata =
+                BluetoothLeBroadcastMetadataExt
+                        .convertToBroadcastMetadata(QR_CODE_STRING_NON_ENCRYPTED)!!
+
+        val qrCodeString = metadata.toQrCodeString()
+
+        assertThat(qrCodeString).isEqualTo(QR_CODE_STRING_NON_ENCRYPTED)
+    }
+
     private companion object {
         const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
+        const val TEST_DEVICE_ADDRESS_PUBLIC = "AA:BB:CC:00:11:22"
 
         val Device: BluetoothDevice =
             BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS,
                 BluetoothDevice.ADDRESS_TYPE_RANDOM)
 
+        val DevicePublic: BluetoothDevice =
+            BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS_PUBLIC,
+                BluetoothDevice.ADDRESS_TYPE_PUBLIC)
+
         const val QR_CODE_STRING =
-            "BT:R:65536;T:1;D:00-A1-A1-A1-A1-A1;AS:1;B:123456;BN:VGVzdA==;" +
-            "PM:BgNwVGVzdA==;SI:160;C:VGVzdENvZGU=;SG:BS:3,BM:3,AC:BQNUZXN0BARlbmc=;" +
-            "VN:U;;"
+            "BLUETOOTH:UUID:184F;BN:VGVzdA==;AT:1;AD:00A1A1A1A1A1;BI:1E240;BC:VGVzdENvZGU=;" +
+            "MD:BgNwVGVzdA==;AS:1;PI:A0;NS:1;BS:3;NB:2;SM:BQNUZXN0BARlbmc=;;"
+        const val QR_CODE_STRING_NON_ENCRYPTED =
+            "BLUETOOTH:UUID:184F;BN:SG9ja2V5;AT:0;AD:AABBCC001122;BI:DE51E9;SQ:1;AS:1;PI:FFFF;" +
+            "NS:1;BS:1;NB:1;;"
     }
 }
\ No newline at end of file
-- 
GitLab