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