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.
|
* 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
|
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.bluetooth.BluetoothLeBroadcastMetadata
|
||||||
import android.os.Parcel
|
import android.bluetooth.BluetoothLeBroadcastSubgroup
|
||||||
import android.os.Parcelable
|
import android.os.Build
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
|
import com.android.settingslib.bluetooth.BluetoothBroadcastUtils.SCHEME_BT_BROADCAST_METADATA
|
||||||
|
|
||||||
object BluetoothLeBroadcastMetadataExt {
|
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.
|
* 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 =
|
fun BluetoothLeBroadcastMetadata.toQrCodeString(): String {
|
||||||
SCHEME_BT_BROADCAST_METADATA + Base64.encodeToString(toBytes(this), Base64.NO_WRAP)
|
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].
|
* Converts QR code string to [BluetoothLeBroadcastMetadata].
|
||||||
@@ -39,32 +108,255 @@ object BluetoothLeBroadcastMetadataExt {
|
|||||||
* QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
|
* QR code string should prefix with "BT:BluetoothLeBroadcastMetadata:".
|
||||||
*/
|
*/
|
||||||
fun convertToBroadcastMetadata(qrCodeString: String): 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 {
|
return try {
|
||||||
val encodedString = qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
|
Log.d(TAG, "Parsing QR string: $qrCodeString")
|
||||||
val bytes = Base64.decode(encodedString, Base64.NO_WRAP)
|
val strippedString =
|
||||||
createFromBytes(BluetoothLeBroadcastMetadata.CREATOR, bytes)
|
qrCodeString.removePrefix(SCHEME_BT_BROADCAST_METADATA)
|
||||||
|
.removeSuffix(SUFFIX_QR_CODE)
|
||||||
|
Log.d(TAG, "Stripped to: $strippedString")
|
||||||
|
parseQrCodeToMetadata(strippedString)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Cannot convert QR code string to BluetoothLeBroadcastMetadata", e)
|
Log.w(TAG, "Cannot parse: $qrCodeString", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toBytes(parcelable: Parcelable): ByteArray =
|
private fun BluetoothLeBroadcastSubgroup.toQrCodeString(): String {
|
||||||
Parcel.obtain().run {
|
val entries = mutableListOf<Pair<String, String>>()
|
||||||
parcelable.writeToParcel(this, 0)
|
entries.add(Pair(KEY_BTSG_BIS_SYNC, getBisSyncFromChannels(this.channels).toString()))
|
||||||
setDataPosition(0)
|
entries.add(Pair(KEY_BTSG_BIS_MASK, getBisMaskFromChannels(this.channels).toString()))
|
||||||
val bytes = marshall()
|
entries.add(Pair(KEY_BTSG_AUDIO_CONTENT,
|
||||||
recycle()
|
Base64.encodeToString(this.contentMetadata.rawMetadata, Base64.NO_WRAP)))
|
||||||
bytes
|
return entries.toQrCodeString(DELIMITER_BT_LEVEL_2)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T> createFromBytes(creator: Parcelable.Creator<T>, bytes: ByteArray): T =
|
private fun List<Pair<String, String>>.toQrCodeString(delimiter: String): String {
|
||||||
Parcel.obtain().run {
|
val entryStrings = this.map{ it.first + DELIMITER_KEY_VALUE + it.second }
|
||||||
unmarshall(bytes, 0, bytes.size)
|
return entryStrings.joinToString(separator = delimiter)
|
||||||
setDataPosition(0)
|
}
|
||||||
val created = creator.createFromParcel(this)
|
|
||||||
recycle()
|
@TargetApi(Build.VERSION_CODES.TIRAMISU)
|
||||||
created
|
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
|
@Test
|
||||||
fun toQrCodeString() {
|
fun toQrCodeString() {
|
||||||
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
|
val subgroup = BluetoothLeBroadcastSubgroup.Builder().apply {
|
||||||
setCodecId(100)
|
setCodecId(0x6)
|
||||||
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
|
val audioCodecConfigMetadata = BluetoothLeAudioCodecConfigMetadata.Builder().build()
|
||||||
setCodecSpecificConfig(audioCodecConfigMetadata)
|
setCodecSpecificConfig(audioCodecConfigMetadata)
|
||||||
setContentMetadata(BluetoothLeAudioContentMetadata.Builder().build())
|
setContentMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||||
|
.setProgramInfo("Test").setLanguage("eng").build())
|
||||||
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||||
setChannelIndex(1000)
|
setSelected(true)
|
||||||
|
setChannelIndex(2)
|
||||||
|
setCodecMetadata(audioCodecConfigMetadata)
|
||||||
|
}.build())
|
||||||
|
addChannel(BluetoothLeBroadcastChannel.Builder().apply {
|
||||||
|
setSelected(true)
|
||||||
|
setChannelIndex(1)
|
||||||
setCodecMetadata(audioCodecConfigMetadata)
|
setCodecMetadata(audioCodecConfigMetadata)
|
||||||
}.build())
|
}.build())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
|
val metadata = BluetoothLeBroadcastMetadata.Builder().apply {
|
||||||
setSourceDevice(Device, 0)
|
setSourceDevice(Device, BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||||
setSourceAdvertisingSid(1)
|
setSourceAdvertisingSid(1)
|
||||||
setBroadcastId(2)
|
setBroadcastId(123456)
|
||||||
setPaSyncInterval(3)
|
setBroadcastName("Test")
|
||||||
|
setPublicBroadcastMetadata(BluetoothLeAudioContentMetadata.Builder()
|
||||||
|
.setProgramInfo("pTest").build())
|
||||||
|
setPaSyncInterval(160)
|
||||||
setEncrypted(true)
|
setEncrypted(true)
|
||||||
setBroadcastCode(byteArrayOf(10, 11, 12, 13))
|
setBroadcastCode("TestCode".toByteArray(Charsets.UTF_8))
|
||||||
setPresentationDelayMicros(4)
|
|
||||||
addSubgroup(subgroup)
|
addSubgroup(subgroup)
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
@@ -60,6 +69,108 @@ class BluetoothLeBroadcastMetadataExtTest {
|
|||||||
assertThat(qrCodeString).isEqualTo(QR_CODE_STRING)
|
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
|
@Test
|
||||||
fun decodeAndEncodeAgain_sameString() {
|
fun decodeAndEncodeAgain_sameString() {
|
||||||
val metadata = BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(QR_CODE_STRING)!!
|
val metadata = BluetoothLeBroadcastMetadataExt.convertToBroadcastMetadata(QR_CODE_STRING)!!
|
||||||
@@ -73,13 +184,12 @@ class BluetoothLeBroadcastMetadataExtTest {
|
|||||||
const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
|
const val TEST_DEVICE_ADDRESS = "00:A1:A1:A1:A1:A1"
|
||||||
|
|
||||||
val Device: BluetoothDevice =
|
val Device: BluetoothDevice =
|
||||||
BluetoothAdapter.getDefaultAdapter().getRemoteDevice(TEST_DEVICE_ADDRESS)
|
BluetoothAdapter.getDefaultAdapter().getRemoteLeDevice(TEST_DEVICE_ADDRESS,
|
||||||
|
BluetoothDevice.ADDRESS_TYPE_RANDOM)
|
||||||
|
|
||||||
const val QR_CODE_STRING =
|
const val QR_CODE_STRING =
|
||||||
"BT:BluetoothLeBroadcastMetadata:AAAAAAEAAAABAAAAEQAAADAAMAA6AEEAMQA6AEEAMQA6AEEAMQA6" +
|
"BT:R:65536;T:1;D:00-A1-A1-A1-A1-A1;AS:1;B:123456;BN:VGVzdA==;" +
|
||||||
"AEEAMQA6AEEAMQAAAAAAAAABAAAAAgAAAAMAAAABAAAABAAAAAQAAAAKCwwNBAAAAAEAAAABAAAAZAAA" +
|
"PM:BgNwVGVzdA==;SI:160;C:VGVzdENvZGU=;SG:BS:3,BM:3,AC:BQNUZXN0BARlbmc=;" +
|
||||||
"AAAAAAABAAAAAAAAAAAAAAAGAAAABgAAAAUDAAAAAAAAAAAAAAAAAAAAAAAAAQAAAP//////////AAAA" +
|
"VN:U;;"
|
||||||
"AAAAAAABAAAAAQAAAAAAAADoAwAAAQAAAAAAAAAAAAAABgAAAAYAAAAFAwAAAAAAAAAAAAAAAAAAAAAA" +
|
|
||||||
"AAAAAAD/////AAAAAAAAAAA="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user