summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRongxuan Liu <rongxuan@google.com>2024-05-06 05:55:46 +0000
committerAndroid Build Cherrypicker Worker <android-build-cherrypicker-worker@google.com>2024-05-06 05:55:46 +0000
commit8cc1ff3c87446f9aea9810e6d1a211fb882ef85f (patch)
tree3ac738142349985babda475c902136ac2dd4763c
parentad60df611d66c9860b9173a860c34992da82e0ed (diff)
downloadbase-8cc1ff3c87446f9aea9810e6d1a211fb882ef85f.tar.gz
[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
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothBroadcastUtils.java2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExt.kt397
-rw-r--r--packages/SettingsLib/tests/unit/src/com/android/settingslib/bluetooth/BluetoothLeBroadcastMetadataExtTest.kt74
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()
@@ -70,6 +70,37 @@ class BluetoothLeBroadcastMetadataExtTest {
}
@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 {
setCodecId(0x6)
@@ -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