| /* | |
| * 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.Hide; | |
| import android.annotation.NonNull; | |
| import android.annotation.Nullable; | |
| import android.annotation.RequiresNoPermission; | |
| 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 | |
| @RequiresNoPermission | |
| 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 | |
| @RequiresNoPermission | |
| 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 | |
| */ | |
| @RequiresNoPermission | |
| public @NonNull byte[] getRawMetadata() { | |
| return mRawMetadata; | |
| } | |
| @Override | |
| public int describeContents() { | |
| return 0; | |
| } | |
| @Override | |
| public void writeToParcel(Parcel out, int flags) { | |
| BluetoothUtils.writeStringToParcel(out, mProgramInfo); | |
| BluetoothUtils.writeStringToParcel(out, mLanguage); | |
| 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(); | |
| byte[] rawMetadata = in.createByteArray(); | |
| 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 | |
| @RequiresNoPermission | |
| 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 | |
| @RequiresNoPermission | |
| 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 | |
| @RequiresNoPermission | |
| 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 | |
| @RequiresNoPermission | |
| 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); | |
| } | |
| } | |
| } |