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 public static final @NonNull Parcelable.Creator<BluetoothLeAudioContentMetadata> CREATOR = 133 new Parcelable.Creator<BluetoothLeAudioContentMetadata>() { 134 public @NonNull BluetoothLeAudioContentMetadata createFromParcel( 135 @NonNull Parcel in) { 136 final String programInfo = in.readString(); 137 final String language = in.readString(); 138 final int rawMetadataLength = in.readInt(); 139 byte[] rawMetadata = new byte[rawMetadataLength]; 140 in.readByteArray(rawMetadata); 141 return new BluetoothLeAudioContentMetadata(programInfo, language, rawMetadata); 142 } 143 144 public @NonNull BluetoothLeAudioContentMetadata[] newArray(int size) { 145 return new BluetoothLeAudioContentMetadata[size]; 146 } 147 }; 148 149 /** 150 * Construct a {@link BluetoothLeAudioContentMetadata} from raw bytes. 151 * 152 * The byte array will be parsed and values for each getter will be populated 153 * 154 * Raw metadata cannot be set using builder in order to maintain raw bytes and getter value 155 * consistency 156 * 157 * @param rawBytes raw bytes of stream metadata in Bluetooth LTV format 158 * @return parsed {@link BluetoothLeAudioContentMetadata} object 159 * @throws IllegalArgumentException if <var>rawBytes</var> is null or when the raw bytes cannot 160 * be parsed to build the object 161 * @hide 162 */ 163 @SystemApi fromRawBytes(@onNull byte[] rawBytes)164 public static @NonNull BluetoothLeAudioContentMetadata fromRawBytes(@NonNull byte[] rawBytes) { 165 if (rawBytes == null) { 166 throw new IllegalArgumentException("Raw bytes cannot be null"); 167 } 168 List<TypeValueEntry> entries = BluetoothUtils.parseLengthTypeValueBytes(rawBytes); 169 if (rawBytes.length > 0 && rawBytes[0] > 0 && entries.isEmpty()) { 170 throw new IllegalArgumentException("No LTV entries are found from rawBytes of size " 171 + rawBytes.length); 172 } 173 String programInfo = null; 174 String language = null; 175 for (TypeValueEntry entry : entries) { 176 // Only use the first value of each type 177 if (programInfo == null && entry.getType() == PROGRAM_INFO_TYPE) { 178 byte[] bytes = entry.getValue(); 179 programInfo = new String(bytes, StandardCharsets.UTF_8); 180 } else if (language == null && entry.getType() == LANGUAGE_TYPE) { 181 byte[] bytes = entry.getValue(); 182 if (bytes.length != LANGUAGE_LENGTH) { 183 throw new IllegalArgumentException("Language byte size " + bytes.length 184 + " is less than " + LANGUAGE_LENGTH + ", needed for ISO 639-3"); 185 } 186 // Parse 3 bytes ISO 639-3 only 187 language = new String(bytes, 0, LANGUAGE_LENGTH, StandardCharsets.US_ASCII); 188 } 189 } 190 return new BluetoothLeAudioContentMetadata(programInfo, language, rawBytes); 191 } 192 193 /** 194 * Builder for {@link BluetoothLeAudioContentMetadata}. 195 * @hide 196 */ 197 @SystemApi 198 public static final class Builder { 199 private String mProgramInfo = null; 200 private String mLanguage = null; 201 private byte[] mRawMetadata = null; 202 203 /** 204 * Create an empty builder 205 * 206 * @hide 207 */ 208 @SystemApi Builder()209 public Builder() {} 210 211 /** 212 * Create a builder with copies of information from original object. 213 * 214 * @param original original object 215 * @hide 216 */ 217 @SystemApi Builder(@onNull BluetoothLeAudioContentMetadata original)218 public Builder(@NonNull BluetoothLeAudioContentMetadata original) { 219 mProgramInfo = original.getProgramInfo(); 220 mLanguage = original.getLanguage(); 221 mRawMetadata = original.getRawMetadata(); 222 } 223 224 /** 225 * Set the title and/or summary of Audio Stream content in UTF-8 format. 226 * 227 * @param programInfo title and/or summary of Audio Stream content in UTF-8 format, null 228 * if this metadata does not exist 229 * @return this builder 230 * @hide 231 */ 232 @SystemApi setProgramInfo(@ullable String programInfo)233 public @NonNull Builder setProgramInfo(@Nullable String programInfo) { 234 mProgramInfo = programInfo; 235 return this; 236 } 237 238 /** 239 * Set language of the audio stream in 3-byte, lower case language code as defined in 240 * ISO 639-3. 241 * 242 * @return this builder 243 * @hide 244 */ 245 @SystemApi setLanguage(@ullable String language)246 public @NonNull Builder setLanguage(@Nullable String language) { 247 mLanguage = language; 248 return this; 249 } 250 251 /** 252 * Build {@link BluetoothLeAudioContentMetadata}. 253 * 254 * @return constructed {@link BluetoothLeAudioContentMetadata} 255 * @throws IllegalArgumentException if the object cannot be built 256 * @hide 257 */ 258 @SystemApi build()259 public @NonNull BluetoothLeAudioContentMetadata build() { 260 List<TypeValueEntry> entries = new ArrayList<>(); 261 if (mRawMetadata != null) { 262 entries = BluetoothUtils.parseLengthTypeValueBytes(mRawMetadata); 263 if (mRawMetadata.length > 0 && mRawMetadata[0] > 0 && entries.isEmpty()) { 264 throw new IllegalArgumentException("No LTV entries are found from rawBytes of" 265 + " size " + mRawMetadata.length + " please check the original object" 266 + " passed to Builder's copy constructor"); 267 } 268 } 269 if (mProgramInfo != null) { 270 entries.removeIf(entry -> entry.getType() == PROGRAM_INFO_TYPE); 271 entries.add(new TypeValueEntry(PROGRAM_INFO_TYPE, 272 mProgramInfo.getBytes(StandardCharsets.UTF_8))); 273 } 274 if (mLanguage != null) { 275 String cleanedLanguage = mLanguage.toLowerCase().strip(); 276 byte[] languageBytes = cleanedLanguage.getBytes(StandardCharsets.US_ASCII); 277 if (languageBytes.length != LANGUAGE_LENGTH) { 278 throw new IllegalArgumentException("Language byte size " + languageBytes.length 279 + " is less than " + LANGUAGE_LENGTH + ", needed ISO 639-3, to build"); 280 } 281 entries.removeIf(entry -> entry.getType() == LANGUAGE_TYPE); 282 entries.add(new TypeValueEntry(LANGUAGE_TYPE, languageBytes)); 283 } 284 byte[] rawBytes = BluetoothUtils.serializeTypeValue(entries); 285 if (rawBytes == null) { 286 throw new IllegalArgumentException("Failed to serialize entries to bytes"); 287 } 288 return new BluetoothLeAudioContentMetadata(mProgramInfo, mLanguage, rawBytes); 289 } 290 } 291 } 292