Merge "Merge "LE Audio: Implement QR code V2 for LEA broadcast" into udc-dev am: 8da013de74" into udc-dev-plus-aosp
This commit is contained in:
committed by
Android (Google) Code Review
commit
2aa155a458
@@ -43,5 +43,5 @@ public final class BluetoothBroadcastUtils {
|
||||
/**
|
||||
* Bluetooth scheme.
|
||||
*/
|
||||
public static final String SCHEME_BT_BROADCAST_METADATA = "BT:BluetoothLeBroadcastMetadata:";
|
||||
public static final String SCHEME_BT_BROADCAST_METADATA = "BT:";
|
||||
}
|
||||
|
||||
@@ -15,23 +15,92 @@
|
||||
*/
|
||||
package com.android.settingslib.bluetooth
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothLeAudioCodecConfigMetadata
|
||||
import android.bluetooth.BluetoothLeAudioContentMetadata
|
||||
import android.bluetooth.BluetoothLeBroadcastChannel
|
||||
import android.bluetooth.BluetoothLeBroadcastMetadata
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.bluetooth.BluetoothLeBroadcastSubgroup
|
||||
import android.os.Build
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
|
||||
|
||||
object BluetoothLeBroadcastMetadataExt {
|
||||
private const val TAG = "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"
|
||||
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_BTSG_BIS_SYNC = "BS"
|
||||
private const val KEY_BTSG_BIS_MASK = "BM"
|
||||
private const val KEY_BTSG_AUDIO_CONTENT = "AC"
|
||||
|
||||
// Vendor specific data
|
||||
private const val KEY_BTVSD_COMPANY_ID = "VI"
|
||||
private const val KEY_BTVSD_VENDOR_DATA = "VD"
|
||||
|
||||
private const val DELIMITER_KEY_VALUE = ":"
|
||||
private const val DELIMITER_BT_LEVEL_1 = ";"
|
||||
private const val DELIMITER_BT_LEVEL_2 = ","
|
||||
|
||||
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
|
||||
private const val SUBGROUP_LC3_CODEC_ID = 0x6L
|
||||
|
||||
/**
|
||||
* Converts [BluetoothLeBroadcastMetadata] to QR code string.
|
||||
*
|
||||
* QR code string will prefix with "BT:BluetoothLeBroadcastMetadata:".
|
||||
* QR code string will prefix with "BT:".
|
||||
*/
|
||||
fun BluetoothLeBroadcastMetadata.toQrCodeString(): String =
|
||||
SCHEME_BT_BROADCAST_METADATA + Base64.encodeToString(toBytes(this), Base64.NO_WRAP)
|
||||
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()))
|
||||
if (this.broadcastCode != null) {
|
||||
entries.add(Pair(KEY_BT_BROADCAST_CODE,
|
||||
Base64.encodeToString(this.broadcastCode, Base64.NO_WRAP)))
|
||||
}
|
||||
this.subgroups.forEach {
|
||||
subgroup -> entries.add(Pair(KEY_BT_SUBGROUPS, subgroup.toQrCodeString())) }
|
||||
entries.add(Pair(KEY_BT_ANDROID_VERSION, ANDROID_VER))
|
||||
val qrCodeString = SCHEME_BT_BROADCAST_METADATA +
|
||||
entries.toQrCodeString(DELIMITER_BT_LEVEL_1) + SUFFIX_QR_CODE
|
||||
Log.d(TAG, "Generated QR string : $qrCodeString")
|
||||
return qrCodeString
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts QR code string to [BluetoothLeBroadcastMetadata].
|
||||
@@ -39,32 +108,255 @@ object BluetoothLeBroadcastMetadataExt {
|
||||
* QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
|
||||
*/
|
||||
fun convertToBroadcastMetadata(qrCodeString: String): BluetoothLeBroadcastMetadata? {
|
||||
if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) return null
|
||||
if (!qrCodeString.startsWith(SCHEME_BT_BROADCAST_METADATA)) {
|
||||
Log.e(TAG, "String \"$qrCodeString\" does not begin with " +
|
||||
"\"$SCHEME_BT_BROADCAST_METADATA\"")
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
val encodedString = qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
|
||||
val bytes = Base64.decode(encodedString, Base64.NO_WRAP)
|
||||
createFromBytes(BluetoothLeBroadcastMetadata.CREATOR, bytes)
|
||||
Log.d(TAG, "Parsing QR string: $qrCodeString")
|
||||
val strippedString =
|
||||
qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
|
||||
.removeSuffix(SUFFIX_QR_CODE)
|
||||
Log.d(TAG, "Stripped to: $strippedString")
|
||||
parseQrCodeToMetadata(strippedString)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Cannot convert QR code string to BluetoothLeBroadcastMetadata", e)
|
||||
Log.w(TAG, "Cannot parse: $qrCodeString", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun toBytes(parcelable: Parcelable): ByteArray =
|
||||
Parcel.obtain().run {
|
||||
parcelable.writeToParcel(this, 0)
|
||||
setDataPosition(0)
|
||||
val bytes = marshall()
|
||||
recycle()
|
||||
bytes
|
||||
}
|
||||
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 <T> createFromBytes(creator: Parcelable.Creator<T>, bytes: ByteArray): T =
|
||||
Parcel.obtain().run {
|
||||
unmarshall(bytes, 0, bytes.size)
|
||||
setDataPosition(0)
|
||||
val created = creator.createFromParcel(this)
|
||||
recycle()
|
||||
created
|
||||
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)
|
||||
}
|
||||
|
||||
@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)
|
||||
.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 paSyncInterval = -1
|
||||
var broadcastCode: ByteArray? = null
|
||||
// List of VendorID -> Data Pairs
|
||||
var vendorDataList = mutableListOf<Pair<Int, ByteArray?>>()
|
||||
var androidVersion: String? = null
|
||||
val builder = BluetoothLeBroadcastMetadata.Builder()
|
||||
|
||||
for (field: List<String> in level1Fields) {
|
||||
if (field.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
val key = field[0]
|
||||
// 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()
|
||||
}
|
||||
KEY_BT_ADDRESS_TYPE -> {
|
||||
require(sourceAddrType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) {
|
||||
"Duplicate sourceAddrType: $input"
|
||||
}
|
||||
sourceAddrType = value.toInt()
|
||||
}
|
||||
KEY_BT_DEVICE -> {
|
||||
require(sourceAddrString == null) { "Duplicate sourceAddr: $input" }
|
||||
sourceAddrString = value.replace("-", ":")
|
||||
}
|
||||
KEY_BT_ADVERTISING_SID -> {
|
||||
require(sourceAdvertiserSid == -1) { "Duplicate sourceAdvertiserSid: $input" }
|
||||
sourceAdvertiserSid = value.toInt()
|
||||
}
|
||||
KEY_BT_BROADCAST_ID -> {
|
||||
require(broadcastId == -1) { "Duplicate broadcastId: $input" }
|
||||
broadcastId = value.toInt()
|
||||
}
|
||||
KEY_BT_BROADCAST_NAME -> {
|
||||
require(broadcastName == null) { "Duplicate broadcastName: $input" }
|
||||
broadcastName = String(Base64.decode(value, Base64.NO_WRAP))
|
||||
}
|
||||
KEY_BT_PUBLIC_BROADCAST_DATA -> {
|
||||
require(publicBroadcastMetadata == null) {
|
||||
"Duplicate publicBroadcastMetadata $input"
|
||||
}
|
||||
publicBroadcastMetadata = BluetoothLeAudioContentMetadata
|
||||
.fromRawBytes(Base64.decode(value, Base64.NO_WRAP))
|
||||
}
|
||||
KEY_BT_SYNC_INTERVAL -> {
|
||||
require(paSyncInterval == -1) { "Duplicate paSyncInterval: $input" }
|
||||
paSyncInterval = value.toInt()
|
||||
}
|
||||
KEY_BT_BROADCAST_CODE -> {
|
||||
require(broadcastCode == null) { "Duplicate broadcastCode: $input" }
|
||||
broadcastCode = Base64.decode(value, Base64.NO_WRAP)
|
||||
}
|
||||
KEY_BT_ANDROID_VERSION -> {
|
||||
require(androidVersion == null) { "Duplicate androidVersion: $input" }
|
||||
androidVersion = value
|
||||
Log.i(TAG, "QR code Android version: $androidVersion")
|
||||
}
|
||||
// Repeatable
|
||||
KEY_BT_SUBGROUPS -> {
|
||||
builder.addSubgroup(parseSubgroupData(value))
|
||||
}
|
||||
// Repeatable
|
||||
KEY_BT_VENDOR_SPECIFIC -> {
|
||||
vendorDataList.add(parseVendorData(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "parseQrCodeToMetadata: sourceAddrType=$sourceAddrType, " +
|
||||
"sourceAddr=$sourceAddrString, sourceAdvertiserSid=$sourceAdvertiserSid, " +
|
||||
"broadcastId=$broadcastId, broadcastName=$broadcastName, " +
|
||||
"publicBroadcastMetadata=${publicBroadcastMetadata != 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}")
|
||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
||||
// add source device and set broadcast code
|
||||
val device = adapter.getRemoteLeDevice(requireNotNull(sourceAddrString), sourceAddrType)
|
||||
builder.apply {
|
||||
setSourceDevice(device, sourceAddrType)
|
||||
setSourceAdvertisingSid(sourceAdvertiserSid)
|
||||
setBroadcastId(broadcastId)
|
||||
setBroadcastName(broadcastName)
|
||||
setPublicBroadcast(publicBroadcastMetadata != null)
|
||||
setPublicBroadcastMetadata(publicBroadcastMetadata)
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
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
|
||||
// channel index starts from 1
|
||||
channels.forEach { channel ->
|
||||
if (channel.channelIndex > 0) {
|
||||
bisMask = bisMask or (1u shl (channel.channelIndex - 1))
|
||||
}
|
||||
}
|
||||
return bisMask
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
val channels = mutableListOf<BluetoothLeBroadcastChannel>()
|
||||
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder()
|
||||
.setAudioLocation(0).build()
|
||||
for (i in 0 until BIS_SYNC_MAX_CHANNEL) {
|
||||
val channelMask = 1u shl i
|
||||
if ((bisMask and channelMask) != 0u) {
|
||||
val channel = BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected((selectionMask and channelMask) != 0u)
|
||||
setChannelIndex(i + 1)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}
|
||||
channels.add(channel.build())
|
||||
}
|
||||
}
|
||||
return channels
|
||||
}
|
||||
}
|
||||
@@ -34,24 +34,33 @@ class BluetoothLeBroadcastMetadataExtTest {
|
||||
@Test
|
||||
fun toQrCodeString() {
|
||||
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
|
||||
setCodecId(100)
|
||||
setCodecId(0x6)
|
||||
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
|
||||
setCodecSpecificConfig(audioCodecConfigMetadata)
|
||||
setContentMetadata(BluetoothLeAudioContentMetadata.Builder().build())
|
||||
setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("Test").setLanguage("eng").build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setChannelIndex(1000)
|
||||
setSelected(true)
|
||||
setChannelIndex(2)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected(true)
|
||||
setChannelIndex(1)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
}.build()
|
||||
|
||||
val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
|
||||
setSourceDevice(Device, 0)
|
||||
setSourceDevice(Device, BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||
setSourceAdvertisingSid(1)
|
||||
setBroadcastId(2)
|
||||
setPaSyncInterval(3)
|
||||
setBroadcastId(123456)
|
||||
setBroadcastName("Test")
|
||||
setPublicBroadcastMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("pTest").build())
|
||||
setPaSyncInterval(160)
|
||||
setEncrypted(true)
|
||||
setBroadcastCode(byteArrayOf(10, 11, 12, 13))
|
||||
setPresentationDelayMicros(4)
|
||||
setBroadcastCode("TestCode".toByteArray(Charsets.UTF_8))
|
||||
addSubgroup(subgroup)
|
||||
}.build()
|
||||
|
||||
@@ -60,6 +69,108 @@ class BluetoothLeBroadcastMetadataExtTest {
|
||||
assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toQrCodeString_NoChannelSelected() {
|
||||
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
|
||||
setCodecId(0x6)
|
||||
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
|
||||
setCodecSpecificConfig(audioCodecConfigMetadata)
|
||||
setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("Test").setLanguage("eng").build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected(false)
|
||||
setChannelIndex(2)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected(false)
|
||||
setChannelIndex(1)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
}.build()
|
||||
|
||||
val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
|
||||
setSourceDevice(Device, BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||
setSourceAdvertisingSid(1)
|
||||
setBroadcastId(123456)
|
||||
setBroadcastName("Test")
|
||||
setPublicBroadcastMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("pTest").build())
|
||||
setPaSyncInterval(160)
|
||||
setEncrypted(true)
|
||||
setBroadcastCode("TestCode".toByteArray(Charsets.UTF_8))
|
||||
addSubgroup(subgroup)
|
||||
}.build()
|
||||
|
||||
val qrCodeString = metadata.toQrCodeString()
|
||||
|
||||
val parsedMetadata =
|
||||
BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(qrCodeString)!!
|
||||
|
||||
assertThat(parsedMetadata).isNotNull()
|
||||
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].hasChannelPreference()).isFalse()
|
||||
// Input order does not matter due to parsing through bisMask
|
||||
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
|
||||
fun toQrCodeString_OneChannelSelected() {
|
||||
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
|
||||
setCodecId(0x6)
|
||||
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
|
||||
setCodecSpecificConfig(audioCodecConfigMetadata)
|
||||
setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("Test").setLanguage("eng").build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected(false)
|
||||
setChannelIndex(1)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||
setSelected(true)
|
||||
setChannelIndex(2)
|
||||
setCodecMetadata(audioCodecConfigMetadata)
|
||||
}.build())
|
||||
}.build()
|
||||
|
||||
val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
|
||||
setSourceDevice(Device, BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||
setSourceAdvertisingSid(1)
|
||||
setBroadcastId(123456)
|
||||
setBroadcastName("Test")
|
||||
setPublicBroadcastMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||
.setProgramInfo("pTest").build())
|
||||
setPaSyncInterval(160)
|
||||
setEncrypted(true)
|
||||
setBroadcastCode("TestCode".toByteArray(Charsets.UTF_8))
|
||||
addSubgroup(subgroup)
|
||||
}.build()
|
||||
|
||||
val qrCodeString = metadata.toQrCodeString()
|
||||
|
||||
val parsedMetadata =
|
||||
BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(qrCodeString)!!
|
||||
|
||||
assertThat(parsedMetadata).isNotNull()
|
||||
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)
|
||||
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()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeAndEncodeAgain_sameString() {
|
||||
val metadata = BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(QR_CODE_STRING)!!
|
||||
@@ -73,13 +184,12 @@ class BluetoothLeBroadcastMetadataExtTest {
|
||||
const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
|
||||
|
||||
val Device: BluetoothDevice =
|
||||
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(TEST_DEVICE_ADDRESS)
|
||||
BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS,
|
||||
BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||
|
||||
const val QR_CODE_STRING =
|
||||
"BT:BluetoothLeBroadcastMetadata:AAAAAAEAAAABAAAAEQAAADAAMAA6AEEAMQA6AEEAMQA6AEEAMQA6" +
|
||||
"AEEAMQA6AEEAMQAAAAAAAAABAAAAAgAAAAMAAAABAAAABAAAAAQAAAAKCwwNBAAAAAEAAAABAAAAZAAA" +
|
||||
"AAAAAAABAAAAAAAAAAAAAAAGAAAABgAAAAUDAAAAAAAAAAAAAAAAAAAAAAAAAQAAAP//////////AAAA" +
|
||||
"AAAAAAABAAAAAQAAAAAAAADoAwAAAQAAAAAAAAAAAAAABgAAAAYAAAAFAwAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAD/////AAAAAAAAAAA="
|
||||
"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;;"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user