1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.bluetooth; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SystemApi; 22 import android.bluetooth.BluetoothUtils.TypeValueEntry; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 26 import java.nio.charset.StandardCharsets; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.List; 30 import java.util.Objects; 31 32 /** 33 * A class representing the media metadata information defined in the Basic Audio Profile. 34 * 35 * @hide 36 */ 37 @SystemApi 38 public final class BluetoothLeAudioContentMetadata implements Parcelable { 39 // From Generic Audio assigned numbers 40 private static final int PROGRAM_INFO_TYPE = 0x03; 41 private static final int LANGUAGE_TYPE = 0x04; 42 private static final int LANGUAGE_LENGTH = 0x03; 43 44 private final String mProgramInfo; 45 private final String mLanguage; 46 private final byte[] mRawMetadata; 47 BluetoothLeAudioContentMetadata(String programInfo, String language, byte[] rawMetadata)48 private BluetoothLeAudioContentMetadata(String programInfo, String language, 49 byte[] rawMetadata) { 50 mProgramInfo = programInfo; 51 mLanguage = language; 52 mRawMetadata = rawMetadata; 53 } 54 55 @Override equals(@ullable Object o)56 public boolean equals(@Nullable Object o) { 57 if (!(o instanceof BluetoothLeAudioContentMetadata)) { 58 return false; 59 } 60 final BluetoothLeAudioContentMetadata other = (BluetoothLeAudioContentMetadata) o; 61 return Objects.equals(mProgramInfo, other.getProgramInfo()) 62 && Objects.equals(mLanguage, other.getLanguage()) 63 && Arrays.equals(mRawMetadata, other.getRawMetadata()); 64 } 65 66 @Override hashCode()67 public int hashCode() { 68 return Objects.hash(mProgramInfo, mLanguage, Arrays.hashCode(mRawMetadata)); 69 } 70 71 /** 72 * Get the title and/or summary of Audio Stream content in UTF-8 format. 73 * 74 * @return title and/or summary of Audio Stream content in UTF-8 format, null if this metadata 75 * does not exist 76 * @hide 77 */ 78 @SystemApi getProgramInfo()79 public @Nullable String getProgramInfo() { 80 return mProgramInfo; 81 } 82 83 /** 84 * Get language of the audio stream in 3-byte, lower case language code as defined in ISO 639-3. 85 * 86 * @return ISO 639-3 formatted language code, null if this metadata does not exist 87 * @hide 88 */ 89 @SystemApi getLanguage()90 public @Nullable String getLanguage() { 91 return mLanguage; 92 } 93 94 /** 95 * Get the raw bytes of stream metadata in Bluetooth LTV format as defined in the Generic Audio 96 * section of <a href="https://www.bluetooth.com/specifications/assigned-numbers/">Bluetooth Assigned Numbers</a>, 97 * including metadata that was not covered by the getter methods in this class 98 * 99 * @return raw bytes of stream metadata in Bluetooth LTV format 100 */ getRawMetadata()101 public @NonNull byte[] getRawMetadata() { 102 return mRawMetadata; 103 } 104 105 106 /** 107 * {@inheritDoc} 108 * @hide 109 */ 110 @Override describeContents()111 public int describeContents() { 112 return 0; 113 } 114 115 /** 116 * {@inheritDoc} 117 * @hide 118 */ 119 @Override writeToParcel(Parcel out, int flags)120 public void writeToParcel(Parcel out, int flags) { 121 out.writeString(mProgramInfo); 122 out.writeString(mLanguage); 123 out.writeInt(mRawMetadata.length); 124 out.writeByteArray(mRawMetadata); 125 } 126 127 /** 128 * A {@link Parcelable.Creator} to create {@link BluetoothLeAudioContentMetadata} from parcel. 129 * @hide 130 */ 131 @SystemApi 132 @NonNull 133 public static final Creator<BluetoothLeAudioContentMetadata> CREATOR = new Creator<>() { 134 public @NonNull BluetoothLeAudioContentMetadata createFromParcel(@NonNull Parcel in) { 135 final String programInfo = in.readString(); 136 final String language = in.readString(); 137 final int rawMetadataLength = in.readInt(); 138 byte[] rawMetadata = new byte[rawMetadataLength]; 139 in.readByteArray(rawMetadata); 140 return new BluetoothLeAudioContentMetadata(programInfo, language, rawMetadata); 141 } 142 143 public @NonNull BluetoothLeAudioContentMetadata[] newArray(int size) { 144 return new BluetoothLeAudioContentMetadata[size]; 145 } 146 }; 147 148 /** 149 * Construct a {@link BluetoothLeAudioContentMetadata} from raw bytes. 150 * 151 * The byte array will be parsed and values for each getter will be populated 152 * 153 * Raw metadata cannot be set using builder in order to maintain raw bytes and getter value 154 * consistency 155 * 156 * @param rawBytes raw bytes of stream metadata in Bluetooth LTV format 157 * @return parsed {@link BluetoothLeAudioContentMetadata} object 158 * @throws IllegalArgumentException if <var>rawBytes</var> is null or when the raw bytes cannot 159 * be parsed to build the object 160 * @hide 161 */ 162 @SystemApi fromRawBytes(@onNull byte[] rawBytes)163 public static @NonNull BluetoothLeAudioContentMetadata fromRawBytes(@NonNull byte[] rawBytes) { 164 if (rawBytes == null) { 165 throw new IllegalArgumentException("Raw bytes cannot be null"); 166 } 167 List<TypeValueEntry> entries = BluetoothUtils.parseLengthTypeValueBytes(rawBytes); 168 if (rawBytes.length > 0 && rawBytes[0] > 0 && entries.isEmpty()) { 169 throw new IllegalArgumentException("No LTV entries are found from rawBytes of size " 170 + rawBytes.length); 171 } 172 String programInfo = null; 173 String language = null; 174 for (TypeValueEntry entry : entries) { 175 // Only use the first value of each type 176 if (programInfo == null && entry.getType() == PROGRAM_INFO_TYPE) { 177 byte[] bytes = entry.getValue(); 178 programInfo = new String(bytes, StandardCharsets.UTF_8); 179 } else if (language == null && entry.getType() == LANGUAGE_TYPE) { 180 byte[] bytes = entry.getValue(); 181 if (bytes.length != LANGUAGE_LENGTH) { 182 throw new IllegalArgumentException("Language byte size " + bytes.length 183 + " is less than " + LANGUAGE_LENGTH + ", needed for ISO 639-3"); 184 } 185 // Parse 3 bytes ISO 639-3 only 186 language = new String(bytes, 0, LANGUAGE_LENGTH, StandardCharsets.US_ASCII); 187 } 188 } 189 return new BluetoothLeAudioContentMetadata(programInfo, language, rawBytes); 190 } 191 192 /** 193 * Builder for {@link BluetoothLeAudioContentMetadata}. 194 * @hide 195 */ 196 @SystemApi 197 public static final class Builder { 198 private String mProgramInfo = null; 199 private String mLanguage = null; 200 private byte[] mRawMetadata = null; 201 202 /** 203 * Create an empty builder 204 * 205 * @hide 206 */ 207 @SystemApi Builder()208 public Builder() {} 209 210 /** 211 * Create a builder with copies of information from original object. 212 * 213 * @param original original object 214 * @hide 215 */ 216 @SystemApi Builder(@onNull BluetoothLeAudioContentMetadata original)217 public Builder(@NonNull BluetoothLeAudioContentMetadata original) { 218 mProgramInfo = original.getProgramInfo(); 219 mLanguage = original.getLanguage(); 220 mRawMetadata = original.getRawMetadata(); 221 } 222 223 /** 224 * Set the title and/or summary of Audio Stream content in UTF-8 format. 225 * 226 * @param programInfo title and/or summary of Audio Stream content in UTF-8 format, null 227 * if this metadata does not exist 228 * @return this builder 229 * @hide 230 */ 231 @SystemApi setProgramInfo(@ullable String programInfo)232 public @NonNull Builder setProgramInfo(@Nullable String programInfo) { 233 mProgramInfo = programInfo; 234 return this; 235 } 236 237 /** 238 * Set language of the audio stream in 3-byte, lower case language code as defined in 239 * ISO 639-3. 240 * 241 * @return this builder 242 * @hide 243 */ 244 @SystemApi setLanguage(@ullable String language)245 public @NonNull Builder setLanguage(@Nullable String language) { 246 mLanguage = language; 247 return this; 248 } 249 250 /** 251 * Build {@link BluetoothLeAudioContentMetadata}. 252 * 253 * @return constructed {@link BluetoothLeAudioContentMetadata} 254 * @throws IllegalArgumentException if the object cannot be built 255 * @hide 256 */ 257 @SystemApi build()258 public @NonNull BluetoothLeAudioContentMetadata build() { 259 List<TypeValueEntry> entries = new ArrayList<>(); 260 if (mRawMetadata != null) { 261 entries = BluetoothUtils.parseLengthTypeValueBytes(mRawMetadata); 262 if (mRawMetadata.length > 0 && mRawMetadata[0] > 0 && entries.isEmpty()) { 263 throw new IllegalArgumentException("No LTV entries are found from rawBytes of" 264 + " size " + mRawMetadata.length + " please check the original object" 265 + " passed to Builder's copy constructor"); 266 } 267 } 268 if (mProgramInfo != null) { 269 entries.removeIf(entry -> entry.getType() == PROGRAM_INFO_TYPE); 270 entries.add(new TypeValueEntry(PROGRAM_INFO_TYPE, 271 mProgramInfo.getBytes(StandardCharsets.UTF_8))); 272 } 273 if (mLanguage != null) { 274 String cleanedLanguage = mLanguage.toLowerCase().strip(); 275 byte[] languageBytes = cleanedLanguage.getBytes(StandardCharsets.US_ASCII); 276 if (languageBytes.length != LANGUAGE_LENGTH) { 277 throw new IllegalArgumentException("Language byte size " + languageBytes.length 278 + " is less than " + LANGUAGE_LENGTH + ", needed ISO 639-3, to build"); 279 } 280 entries.removeIf(entry -> entry.getType() == LANGUAGE_TYPE); 281 entries.add(new TypeValueEntry(LANGUAGE_TYPE, languageBytes)); 282 } 283 byte[] rawBytes = BluetoothUtils.serializeTypeValue(entries); 284 if (rawBytes == null) { 285 throw new IllegalArgumentException("Failed to serialize entries to bytes"); 286 } 287 return new BluetoothLeAudioContentMetadata(mProgramInfo, mLanguage, rawBytes); 288 } 289 } 290 } 291