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