| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| 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.Arrays; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Objects; |
| |
| /** |
| * A class representing the media metadata information defined in the Basic Audio Profile. |
| * |
| * @hide |
| */ |
| @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; |
| |
| private BluetoothLeAudioContentMetadata( |
| String programInfo, String language, byte[] rawMetadata) { |
| mProgramInfo = programInfo; |
| mLanguage = language; |
| mRawMetadata = rawMetadata; |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object o) { |
| if (!(o instanceof BluetoothLeAudioContentMetadata)) { |
| return false; |
| } |
| final BluetoothLeAudioContentMetadata other = (BluetoothLeAudioContentMetadata) o; |
| return Objects.equals(mProgramInfo, other.getProgramInfo()) |
| && Objects.equals(mLanguage, other.getLanguage()) |
| && Arrays.equals(mRawMetadata, other.getRawMetadata()); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(mProgramInfo, mLanguage, Arrays.hashCode(mRawMetadata)); |
| } |
| |
| @Override |
| public String toString() { |
| return "BluetoothLeAudioContentMetadata{" |
| + ("programInfo=" + mProgramInfo) |
| + (", language=" + mLanguage) |
| + (", rawMetadata=" + Arrays.toString(mRawMetadata)) |
| + '}'; |
| } |
| |
| /** |
| * Get the title and/or summary of Audio Stream content in UTF-8 format. |
| * |
| * @return title and/or summary of Audio Stream content in UTF-8 format, null if this metadata |
| * does not exist |
| * @hide |
| */ |
| @SystemApi |
| public @Nullable String getProgramInfo() { |
| return mProgramInfo; |
| } |
| |
| /** |
| * Get 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 |
| * @hide |
| */ |
| @SystemApi |
| public @Nullable String getLanguage() { |
| return mLanguage; |
| } |
| |
| /** |
| * Get the raw bytes of stream metadata in Bluetooth LTV format as defined in the Generic Audio |
| * section of <a href="https://www.bluetooth.com/specifications/assigned-numbers/">Bluetooth |
| * Assigned Numbers</a>, including metadata that was not covered by the getter methods in this |
| * class |
| * |
| * @return raw bytes of stream metadata in Bluetooth LTV format |
| */ |
| public @NonNull byte[] getRawMetadata() { |
| return mRawMetadata; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @hide |
| */ |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @hide |
| */ |
| @Override |
| public void writeToParcel(Parcel out, int flags) { |
| out.writeString(mProgramInfo); |
| out.writeString(mLanguage); |
| out.writeInt(mRawMetadata.length); |
| out.writeByteArray(mRawMetadata); |
| } |
| |
| /** |
| * A {@link Parcelable.Creator} to create {@link BluetoothLeAudioContentMetadata} from parcel. |
| * |
| * @hide |
| */ |
| @SystemApi @NonNull |
| public static final Creator<BluetoothLeAudioContentMetadata> CREATOR = |
| new Creator<>() { |
| public @NonNull BluetoothLeAudioContentMetadata createFromParcel( |
| @NonNull Parcel in) { |
| final String programInfo = in.readString(); |
| final String language = in.readString(); |
| final int rawMetadataLength = in.readInt(); |
| byte[] rawMetadata = new byte[rawMetadataLength]; |
| in.readByteArray(rawMetadata); |
| return new BluetoothLeAudioContentMetadata(programInfo, language, rawMetadata); |
| } |
| |
| public @NonNull BluetoothLeAudioContentMetadata[] newArray(int size) { |
| return new BluetoothLeAudioContentMetadata[size]; |
| } |
| }; |
| |
| /** |
| * Construct a {@link BluetoothLeAudioContentMetadata} from raw bytes. |
| * |
| * <p>The byte array will be parsed and values for each getter will be populated |
| * |
| * <p>Raw metadata cannot be set using builder in order to maintain raw bytes and getter value |
| * consistency |
| * |
| * @param rawBytes raw bytes of stream metadata in Bluetooth LTV format |
| * @return parsed {@link BluetoothLeAudioContentMetadata} object |
| * @throws IllegalArgumentException if <var>rawBytes</var> is null or when the raw bytes cannot |
| * be parsed to build the object |
| * @hide |
| */ |
| @SystemApi |
| public static @NonNull BluetoothLeAudioContentMetadata fromRawBytes(@NonNull byte[] rawBytes) { |
| if (rawBytes == null) { |
| throw new IllegalArgumentException("Raw bytes cannot be 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); |
| } |
| |
| /** |
| * Builder for {@link BluetoothLeAudioContentMetadata}. |
| * |
| * @hide |
| */ |
| @SystemApi |
| public static final class Builder { |
| private String mProgramInfo = null; |
| private String mLanguage = null; |
| private byte[] mRawMetadata = null; |
| |
| /** |
| * Create an empty builder |
| * |
| * @hide |
| */ |
| @SystemApi |
| public Builder() {} |
| |
| /** |
| * Create a builder with copies of information from original object. |
| * |
| * @param original original object |
| * @hide |
| */ |
| @SystemApi |
| public Builder(@NonNull BluetoothLeAudioContentMetadata original) { |
| mProgramInfo = original.getProgramInfo(); |
| mLanguage = original.getLanguage(); |
| mRawMetadata = original.getRawMetadata(); |
| } |
| |
| /** |
| * Set the title and/or summary of Audio Stream content in UTF-8 format. |
| * |
| * @param programInfo title and/or summary of Audio Stream content in UTF-8 format, null if |
| * this metadata does not exist |
| * @return this builder |
| * @hide |
| */ |
| @SystemApi |
| public @NonNull Builder setProgramInfo(@Nullable String programInfo) { |
| mProgramInfo = programInfo; |
| return this; |
| } |
| |
| /** |
| * Set language of the audio stream in 3-byte, lower case language code as defined in ISO |
| * 639-3. |
| * |
| * @return this builder |
| * @hide |
| */ |
| @SystemApi |
| public @NonNull Builder setLanguage(@Nullable String language) { |
| mLanguage = language; |
| return this; |
| } |
| |
| /** |
| * Build {@link BluetoothLeAudioContentMetadata}. |
| * |
| * @return constructed {@link BluetoothLeAudioContentMetadata} |
| * @throws IllegalArgumentException if the object cannot be built |
| * @hide |
| */ |
| @SystemApi |
| public @NonNull BluetoothLeAudioContentMetadata build() { |
| 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(Locale.US).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, rawBytes); |
| } |
| } |
| } |