summaryrefslogtreecommitdiff
path: root/framework/java
diff options
context:
space:
mode:
authorJack He <siyuanh@google.com>2022-03-09 22:16:10 -0800
committerJack He <siyuanh@google.com>2022-03-10 11:30:45 -0800
commit0c6507d57d6141e35a7e4236e266b2dfdde0224c (patch)
tree55354bbe19952aa31fb8d2fb4cce422294235a5e /framework/java
parent7328d681709eb22ba708218dd010d70e4b40556f (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')
-rw-r--r--framework/java/android/bluetooth/BluetoothLeAudioCodecConfigMetadata.java46
-rw-r--r--framework/java/android/bluetooth/BluetoothLeAudioContentMetadata.java67
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcast.java4
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcastChannel.java3
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcastMetadata.java9
-rw-r--r--framework/java/android/bluetooth/BluetoothLeBroadcastSubgroup.java10
-rw-r--r--framework/java/android/bluetooth/BluetoothUtils.java130
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;
+ }
}