diff options
author | Jack He <siyuanh@google.com> | 2022-03-09 22:16:10 -0800 |
---|---|---|
committer | Jack He <siyuanh@google.com> | 2022-03-10 11:30:45 -0800 |
commit | 0c6507d57d6141e35a7e4236e266b2dfdde0224c (patch) | |
tree | 55354bbe19952aa31fb8d2fb4cce422294235a5e /framework/java | |
parent | 7328d681709eb22ba708218dd010d70e4b40556f (diff) |
Broadcast API adjustment
* Enforce raw bytes vs. structured value consistency in metadata classes
* Enforce required parameters in builder
* Rename getMaximumNumberOfBroadcast to getMaximumNumberOfBroadcasts
* Added utility method to parse and serialize LTV array
Fixes: 218683032
Bug: 218683032
Test: atest BluetoothInstrumentationTests, cts tests
Tag: #feature
Change-Id: Ia10f414bdc958b75e94276d3f645687f8b9635f9
Diffstat (limited to 'framework/java')
7 files changed, 256 insertions, 13 deletions
diff --git a/framework/java/android/bluetooth/BluetoothLeAudioCodecConfigMetadata.java b/framework/java/android/bluetooth/BluetoothLeAudioCodecConfigMetadata.java index 4ca657c449..9881ec2067 100644 --- a/framework/java/android/bluetooth/BluetoothLeAudioCodecConfigMetadata.java +++ b/framework/java/android/bluetooth/BluetoothLeAudioCodecConfigMetadata.java @@ -18,9 +18,14 @@ package android.bluetooth; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.bluetooth.BluetoothUtils.TypeValueEntry; import android.os.Parcel; import android.os.Parcelable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + /** * A class representing the codec specific config metadata information defined in the Basic Audio * Profile. @@ -29,7 +34,7 @@ import android.os.Parcelable; */ @SystemApi public final class BluetoothLeAudioCodecConfigMetadata implements Parcelable { - private static final int UNKNOWN_VALUE_PLACEHOLDER = -1; + private static final int AUDIO_CHANNEL_LOCATION_TYPE = 0x03; private final long mAudioLocation; private final byte[] mRawMetadata; @@ -137,7 +142,21 @@ public final class BluetoothLeAudioCodecConfigMetadata implements Parcelable { if (rawBytes == null) { throw new IllegalArgumentException("Raw bytes cannot be null"); } - return null; + List<TypeValueEntry> entries = BluetoothUtils.parseLengthTypeValueBytes(rawBytes); + if (rawBytes.length > 0 && rawBytes[0] > 0 && entries.isEmpty()) { + throw new IllegalArgumentException("No LTV entries are found from rawBytes of size " + + rawBytes.length); + } + long audioLocation = 0; + for (TypeValueEntry entry : entries) { + if (entry.getType() == AUDIO_CHANNEL_LOCATION_TYPE) { + byte[] bytes = entry.getValue(); + // Get unsigned uint32_t to long + audioLocation = ((bytes[0] & 0xFF) << 0) | ((bytes[1] & 0xFF) << 8) + | ((bytes[2] & 0xFF) << 16) | ((long) (bytes[3] & 0xFF) << 24); + } + } + return new BluetoothLeAudioCodecConfigMetadata(audioLocation, rawBytes); } /** @@ -146,7 +165,7 @@ public final class BluetoothLeAudioCodecConfigMetadata implements Parcelable { */ @SystemApi public static final class Builder { - private long mAudioLocation = UNKNOWN_VALUE_PLACEHOLDER; + private long mAudioLocation = 0; private byte[] mRawMetadata = null; /** @@ -191,10 +210,25 @@ public final class BluetoothLeAudioCodecConfigMetadata implements Parcelable { */ @SystemApi public @NonNull BluetoothLeAudioCodecConfigMetadata build() { - if (mRawMetadata == null) { - mRawMetadata = new byte[0]; + List<TypeValueEntry> entries = new ArrayList<>(); + if (mRawMetadata != null) { + entries = BluetoothUtils.parseLengthTypeValueBytes(mRawMetadata); + if (mRawMetadata.length > 0 && mRawMetadata[0] > 0 && entries.isEmpty()) { + throw new IllegalArgumentException("No LTV entries are found from rawBytes of" + + " size " + mRawMetadata.length + " please check the original object" + + " passed to Builder's copy constructor"); + } + } + if (mAudioLocation != 0) { + entries.removeIf(entry -> entry.getType() == AUDIO_CHANNEL_LOCATION_TYPE); + entries.add(new TypeValueEntry(AUDIO_CHANNEL_LOCATION_TYPE, + ByteBuffer.allocate(Long.BYTES).putLong(mAudioLocation).array())); + } + byte[] rawBytes = BluetoothUtils.serializeTypeValue(entries); + if (rawBytes == null) { + throw new IllegalArgumentException("Failed to serialize entries to bytes"); } - return new BluetoothLeAudioCodecConfigMetadata(mAudioLocation, mRawMetadata); + return new BluetoothLeAudioCodecConfigMetadata(mAudioLocation, rawBytes); } } } diff --git a/framework/java/android/bluetooth/BluetoothLeAudioContentMetadata.java b/framework/java/android/bluetooth/BluetoothLeAudioContentMetadata.java index 47ab6983e0..4f02e89312 100644 --- a/framework/java/android/bluetooth/BluetoothLeAudioContentMetadata.java +++ b/framework/java/android/bluetooth/BluetoothLeAudioContentMetadata.java @@ -19,9 +19,14 @@ package android.bluetooth; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; +import android.bluetooth.BluetoothUtils.TypeValueEntry; import android.os.Parcel; import android.os.Parcelable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + /** * A class representing the media metadata information defined in the Basic Audio Profile. * @@ -29,6 +34,11 @@ import android.os.Parcelable; */ @SystemApi public final class BluetoothLeAudioContentMetadata implements Parcelable { + // From Generic Audio assigned numbers + private static final int PROGRAM_INFO_TYPE = 0x03; + private static final int LANGUAGE_TYPE = 0x04; + private static final int LANGUAGE_LENGTH = 0x03; + private final String mProgramInfo; private final String mLanguage; private final byte[] mRawMetadata; @@ -137,7 +147,29 @@ public final class BluetoothLeAudioContentMetadata implements Parcelable { if (rawBytes == null) { throw new IllegalArgumentException("Raw bytes cannot be null"); } - return null; + List<TypeValueEntry> entries = BluetoothUtils.parseLengthTypeValueBytes(rawBytes); + if (rawBytes.length > 0 && rawBytes[0] > 0 && entries.isEmpty()) { + throw new IllegalArgumentException("No LTV entries are found from rawBytes of size " + + rawBytes.length); + } + String programInfo = null; + String language = null; + for (TypeValueEntry entry : entries) { + // Only use the first value of each type + if (programInfo == null && entry.getType() == PROGRAM_INFO_TYPE) { + byte[] bytes = entry.getValue(); + programInfo = new String(bytes, StandardCharsets.UTF_8); + } else if (language == null && entry.getType() == LANGUAGE_TYPE) { + byte[] bytes = entry.getValue(); + if (bytes.length != LANGUAGE_LENGTH) { + throw new IllegalArgumentException("Language byte size " + bytes.length + + " is less than " + LANGUAGE_LENGTH + ", needed for ISO 639-3"); + } + // Parse 3 bytes ISO 639-3 only + language = new String(bytes, 0, LANGUAGE_LENGTH, StandardCharsets.US_ASCII); + } + } + return new BluetoothLeAudioContentMetadata(programInfo, language, rawBytes); } /** @@ -189,7 +221,7 @@ public final class BluetoothLeAudioContentMetadata implements Parcelable { * Set language of the audio stream in 3-byte, lower case language code as defined in * ISO 639-3. * - * @return ISO 639-3 formatted language code, null if this metadata does not exist + * @return this builder * @hide */ @SystemApi @@ -207,10 +239,35 @@ public final class BluetoothLeAudioContentMetadata implements Parcelable { */ @SystemApi public @NonNull BluetoothLeAudioContentMetadata build() { - if (mRawMetadata == null) { - mRawMetadata = new byte[0]; + List<TypeValueEntry> entries = new ArrayList<>(); + if (mRawMetadata != null) { + entries = BluetoothUtils.parseLengthTypeValueBytes(mRawMetadata); + if (mRawMetadata.length > 0 && mRawMetadata[0] > 0 && entries.isEmpty()) { + throw new IllegalArgumentException("No LTV entries are found from rawBytes of" + + " size " + mRawMetadata.length + " please check the original object" + + " passed to Builder's copy constructor"); + } + } + if (mProgramInfo != null) { + entries.removeIf(entry -> entry.getType() == PROGRAM_INFO_TYPE); + entries.add(new TypeValueEntry(PROGRAM_INFO_TYPE, + mProgramInfo.getBytes(StandardCharsets.UTF_8))); + } + if (mLanguage != null) { + String cleanedLanguage = mLanguage.toLowerCase().strip(); + byte[] languageBytes = cleanedLanguage.getBytes(StandardCharsets.US_ASCII); + if (languageBytes.length != LANGUAGE_LENGTH) { + throw new IllegalArgumentException("Language byte size " + languageBytes.length + + " is less than " + LANGUAGE_LENGTH + ", needed ISO 639-3, to build"); + } + entries.removeIf(entry -> entry.getType() == LANGUAGE_TYPE); + entries.add(new TypeValueEntry(LANGUAGE_TYPE, languageBytes)); + } + byte[] rawBytes = BluetoothUtils.serializeTypeValue(entries); + if (rawBytes == null) { + throw new IllegalArgumentException("Failed to serialize entries to bytes"); } - return new BluetoothLeAudioContentMetadata(mProgramInfo, mLanguage, mRawMetadata); + return new BluetoothLeAudioContentMetadata(mProgramInfo, mLanguage, rawBytes); } } } diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcast.java b/framework/java/android/bluetooth/BluetoothLeBroadcast.java index 5bf6e00fea..fa7f96737d 100644 --- a/framework/java/android/bluetooth/BluetoothLeBroadcast.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcast.java @@ -708,7 +708,7 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi */ @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) - public int getMaximumNumberOfBroadcast() { + public int getMaximumNumberOfBroadcasts() { final IBluetoothLeAudio service = getService(); final int defaultValue = 1; if (service == null) { @@ -717,7 +717,7 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi } else if (isEnabled()) { try { final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); - service.getMaximumNumberOfBroadcast(mAttributionSource, recv); + service.getMaximumNumberOfBroadcasts(mAttributionSource, recv); return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); } catch (TimeoutException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastChannel.java b/framework/java/android/bluetooth/BluetoothLeBroadcastChannel.java index 6addc062e3..676692ae4a 100644 --- a/framework/java/android/bluetooth/BluetoothLeBroadcastChannel.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcastChannel.java @@ -200,6 +200,9 @@ public final class BluetoothLeBroadcastChannel implements Parcelable { */ @SystemApi public @NonNull BluetoothLeBroadcastChannel build() { + if (mCodecMetadata == null) { + throw new IllegalArgumentException("codec metadata cannot be null"); + } return new BluetoothLeBroadcastChannel(mIsSelected, mChannelIndex, mCodecMetadata); } } diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastMetadata.java b/framework/java/android/bluetooth/BluetoothLeBroadcastMetadata.java index 1810818c98..2e748b9619 100644 --- a/framework/java/android/bluetooth/BluetoothLeBroadcastMetadata.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcastMetadata.java @@ -478,6 +478,15 @@ public final class BluetoothLeBroadcastMetadata implements Parcelable { */ @SystemApi public @NonNull BluetoothLeBroadcastMetadata build() { + if (mSourceAddressType == BluetoothDevice.ADDRESS_TYPE_UNKNOWN) { + throw new IllegalArgumentException("SourceAddressTyp cannot be unknown"); + } + if (mSourceDevice == null) { + throw new IllegalArgumentException("SourceDevice cannot be null"); + } + if (mSubgroups.isEmpty()) { + throw new IllegalArgumentException("Must contain at least one subgroup"); + } return new BluetoothLeBroadcastMetadata(mSourceAddressType, mSourceDevice, mSourceAdvertisingSid, mBroadcastId, mPaSyncInterval, mIsEncrypted, mBroadcastCode, mPresentationDelayMicros, mSubgroups); diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcastSubgroup.java b/framework/java/android/bluetooth/BluetoothLeBroadcastSubgroup.java index 273ac43c37..f8490c661d 100644 --- a/framework/java/android/bluetooth/BluetoothLeBroadcastSubgroup.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcastSubgroup.java @@ -275,6 +275,7 @@ public final class BluetoothLeBroadcastSubgroup implements Parcelable { * A Broadcast subgroup should contain at least 1 Broadcast Channel * * @param channel a Broadcast Channel to be added to this Broadcast subgroup + * @return this builder * @hide */ @SystemApi @@ -305,6 +306,15 @@ public final class BluetoothLeBroadcastSubgroup implements Parcelable { */ @SystemApi public @NonNull BluetoothLeBroadcastSubgroup build() { + if (mCodecSpecificConfig == null) { + throw new IllegalArgumentException("CodecSpecificConfig is null"); + } + if (mContentMetadata == null) { + throw new IllegalArgumentException("ContentMetadata is null"); + } + if (mChannels.isEmpty()) { + throw new IllegalArgumentException("Must have at least one channel"); + } return new BluetoothLeBroadcastSubgroup(mCodecId, mCodecSpecificConfig, mContentMetadata, mNoChannelPreference, mChannels); } diff --git a/framework/java/android/bluetooth/BluetoothUtils.java b/framework/java/android/bluetooth/BluetoothUtils.java index 705732f526..c1d66c5a8f 100644 --- a/framework/java/android/bluetooth/BluetoothUtils.java +++ b/framework/java/android/bluetooth/BluetoothUtils.java @@ -17,13 +17,19 @@ package android.bluetooth; import android.os.UserHandle; +import android.util.Log; import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * {@hide} */ public final class BluetoothUtils { + private static final String TAG = "BluetoothUtils"; + /** * This utility class cannot be instantiated */ @@ -45,4 +51,128 @@ public final class BluetoothUtils { * Match with UserHandl.NULL but accessible inside bluetooth package */ public static final UserHandle USER_HANDLE_NULL = UserHandle.of(-10000); + + static class TypeValueEntry { + private final int mType; + private final byte[] mValue; + + TypeValueEntry(int type, byte[] value) { + mType = type; + mValue = value; + } + + public int getType() { + return mType; + } + + public byte[] getValue() { + return mValue; + } + } + + // Helper method to extract bytes from byte array. + private static byte[] extractBytes(byte[] rawBytes, int start, int length) { + int remainingLength = rawBytes.length - start; + if (remainingLength < length) { + Log.w(TAG, "extractBytes() remaining length " + remainingLength + + " is less than copying length " + length + ", array length is " + + rawBytes.length + " start is " + start); + return null; + } + byte[] bytes = new byte[length]; + System.arraycopy(rawBytes, start, bytes, 0, length); + return bytes; + } + + /** + * Parse Length Value Entry from raw bytes + * + * The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18. + * + * @param rawBytes raw bytes of Length-Value-Entry array + * @hide + */ + public static List<TypeValueEntry> parseLengthTypeValueBytes(byte[] rawBytes) { + if (rawBytes == null) { + return Collections.emptyList(); + } + if (rawBytes.length == 0) { + return Collections.emptyList(); + } + + int currentPos = 0; + List<TypeValueEntry> result = new ArrayList<>(); + while (currentPos < rawBytes.length) { + // length is unsigned int. + int length = rawBytes[currentPos] & 0xFF; + if (length == 0) { + break; + } + currentPos++; + if (currentPos >= rawBytes.length) { + Log.w(TAG, "parseLtv() no type and value after length, rawBytes length = " + + rawBytes.length + ", currentPost = " + currentPos); + break; + } + // Note the length includes the length of the field type itself. + int dataLength = length - 1; + // fieldType is unsigned int. + int type = rawBytes[currentPos] & 0xFF; + currentPos++; + if (currentPos >= rawBytes.length) { + Log.w(TAG, "parseLtv() no value after length, rawBytes length = " + + rawBytes.length + ", currentPost = " + currentPos); + break; + } + byte[] value = extractBytes(rawBytes, currentPos, dataLength); + if (value == null) { + Log.w(TAG, "failed to extract bytes, currentPost = " + currentPos); + break; + } + result.add(new TypeValueEntry(type, value)); + currentPos += dataLength; + } + return result; + } + + /** + * Serialize type value entries to bytes + * @param typeValueEntries type value entries + * @return serialized type value entries on success, null on failure + */ + public static byte[] serializeTypeValue(List<TypeValueEntry> typeValueEntries) { + // Calculate length + int length = 0; + for (TypeValueEntry entry : typeValueEntries) { + // 1 for length and 1 for type + length += 2; + if ((entry.getType() - (entry.getType() & 0xFF)) != 0) { + Log.w(TAG, "serializeTypeValue() type " + entry.getType() + + " is out of range of 0-0xFF"); + return null; + } + if (entry.getValue() == null) { + Log.w(TAG, "serializeTypeValue() value is null"); + return null; + } + int lengthValue = entry.getValue().length + 1; + if ((lengthValue - (lengthValue & 0xFF)) != 0) { + Log.w(TAG, "serializeTypeValue() entry length " + entry.getValue().length + + " is not in range of 0 to 254"); + return null; + } + length += entry.getValue().length; + } + byte[] result = new byte[length]; + int currentPos = 0; + for (TypeValueEntry entry : typeValueEntries) { + result[currentPos] = (byte) ((entry.getValue().length + 1) & 0xFF); + currentPos++; + result[currentPos] = (byte) (entry.getType() & 0xFF); + currentPos++; + System.arraycopy(entry.getValue(), 0, result, currentPos, entry.getValue().length); + currentPos += entry.getValue().length; + } + return result; + } } |