• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 com.android.bluetooth.avrcpcontroller;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.annotation.SuppressLint;
22 import android.util.Log;
23 import android.util.Xml;
24 
25 import org.xmlpull.v1.XmlPullParser;
26 import org.xmlpull.v1.XmlPullParserException;
27 import org.xmlpull.v1.XmlPullParserFactory;
28 import org.xmlpull.v1.XmlSerializer;
29 
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.StringWriter;
33 import java.nio.charset.StandardCharsets;
34 import java.util.ArrayList;
35 import java.util.List;
36 
37 /**
38  * Represents the return value of a BIP GetImageProperties request, giving a detailed description of
39  * an image and its available descriptors before download.
40  *
41  * <p>Format is as described by version 1.2.1 of the Basic Image Profile Specification. The
42  * specification describes three types of metadata that can arrive with an image -- native, variant
43  * and attachment. Native describes which native formats a particular image is available in. Variant
44  * describes which other types of encodings/sizes can be created from the native image using various
45  * transformations. Attachments describes other items that can be downloaded that are associated
46  * with the image (text, sounds, etc.)
47  *
48  * <p>The specification requires that 1. The fixed version string of "1.0" is used 2. There is an
49  * image handle 3. The "imaging thumbnail format" is included. This is defined for BIP in section
50  * 4.4.3 (160x120 JPEG) and redefined for AVRCP in section 5.14.2.2.1 I (200x200 JPEG). It can be
51  * either a native or variant format.
52  *
53  * <p>Example: <image-properties version="1.0" handle="123456789"> <native encoding="JPEG"
54  * pixel="1280*1024" size="1048576"/> <variant encoding="JPEG" pixel="640*480" /> <variant
55  * encoding="JPEG" pixel="160*120" /> <variant encoding="GIF" pixel="80*60-640*480"
56  * transformation="stretch fill crop"/> <attachment content-type="text/plain" name="ABCD1234.txt"
57  * size="5120"/> <attachment content-type="audio/basic" name="ABCD1234.wav" size="102400"/>
58  * </image-properties>
59  */
60 public class BipImageProperties {
61     private static final String TAG =
62             AvrcpControllerUtils.TAG_PREFIX_AVRCP_CONTROLLER
63                     + BipImageProperties.class.getSimpleName();
64 
65     private static final String sVersion = "1.0";
66 
67     /** A Builder for a BipImageProperties object */
68     public static class Builder {
69         private final BipImageProperties mProperties = new BipImageProperties();
70 
71         /**
72          * Set the image handle field for the object you're building
73          *
74          * @param handle The image handle you want to add to the object
75          * @return The builder object to keep building on top of
76          */
setImageHandle(String handle)77         public Builder setImageHandle(String handle) {
78             mProperties.mImageHandle = handle;
79             return this;
80         }
81 
82         /**
83          * Set the FriendlyName field for the object you're building
84          *
85          * @param friendlyName The friendly name you want to add to the object
86          * @return The builder object to keep building on top of
87          */
setFriendlyName(String friendlyName)88         public Builder setFriendlyName(String friendlyName) {
89             mProperties.mFriendlyName = friendlyName;
90             return this;
91         }
92 
93         /**
94          * Add a native format for the object you're building
95          *
96          * @param format The format you want to add to the object
97          * @return The builder object to keep building on top of
98          */
addNativeFormat(BipImageFormat format)99         public Builder addNativeFormat(BipImageFormat format) {
100             mProperties.addNativeFormat(format);
101             return this;
102         }
103 
104         /**
105          * Add a variant format for the object you're building
106          *
107          * @param format The format you want to add to the object
108          * @return The builder object to keep building on top of
109          */
addVariantFormat(BipImageFormat format)110         public Builder addVariantFormat(BipImageFormat format) {
111             mProperties.addVariantFormat(format);
112             return this;
113         }
114 
115         /**
116          * Add an attachment entry for the object you're building
117          *
118          * @param format The format you want to add to the object
119          * @return The builder object to keep building on top of
120          */
addAttachment(BipAttachmentFormat format)121         public Builder addAttachment(BipAttachmentFormat format) {
122             mProperties.addAttachment(format);
123             return this;
124         }
125 
126         /**
127          * Build the object
128          *
129          * @return A BipImageProperties object
130          */
build()131         public BipImageProperties build() {
132             return mProperties;
133         }
134     }
135 
136     /** The image handle associated with this set of properties. */
137     private String mImageHandle = null;
138 
139     /** The version of the properties object, used to encode and decode. */
140     private String mVersion = null;
141 
142     /**
143      * An optional friendly name for the associated image. The specification suggests the file name.
144      */
145     private String mFriendlyName = null;
146 
147     /** Whether we have the required imaging thumbnail format */
148     private boolean mHasThumbnailFormat = false;
149 
150     /** The various sets of available formats. */
151     private final List<BipImageFormat> mNativeFormats = new ArrayList<>();
152 
153     private final List<BipImageFormat> mVariantFormats = new ArrayList<>();
154     private final List<BipAttachmentFormat> mAttachments = new ArrayList<>();
155 
BipImageProperties()156     private BipImageProperties() {
157         mVersion = sVersion;
158     }
159 
BipImageProperties(InputStream inputStream)160     public BipImageProperties(InputStream inputStream) {
161         parse(inputStream);
162     }
163 
parse(InputStream inputStream)164     private void parse(InputStream inputStream) {
165         try {
166             XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
167             xpp.setInput(inputStream, "utf-8");
168             int event = xpp.getEventType();
169             while (event != XmlPullParser.END_DOCUMENT) {
170                 switch (event) {
171                     case XmlPullParser.START_TAG:
172                         String tag = xpp.getName();
173                         if (tag.equals("image-properties")) {
174                             mVersion = xpp.getAttributeValue(null, "version");
175                             mImageHandle = xpp.getAttributeValue(null, "handle");
176                             mFriendlyName = xpp.getAttributeValue(null, "friendly-name");
177                         } else if (tag.equals("native")) {
178                             String encoding = xpp.getAttributeValue(null, "encoding");
179                             String pixel = xpp.getAttributeValue(null, "pixel");
180                             String size = xpp.getAttributeValue(null, "size");
181                             addNativeFormat(BipImageFormat.parseNative(encoding, pixel, size));
182                         } else if (tag.equals("variant")) {
183                             String encoding = xpp.getAttributeValue(null, "encoding");
184                             String pixel = xpp.getAttributeValue(null, "pixel");
185                             String maxSize = xpp.getAttributeValue(null, "maxsize");
186                             String trans = xpp.getAttributeValue(null, "transformation");
187                             addVariantFormat(
188                                     BipImageFormat.parseVariant(encoding, pixel, maxSize, trans));
189                         } else if (tag.equals("attachment")) {
190                             String contentType = xpp.getAttributeValue(null, "content-type");
191                             String name = xpp.getAttributeValue(null, "name");
192                             String charset = xpp.getAttributeValue(null, "charset");
193                             String size = xpp.getAttributeValue(null, "size");
194                             String created = xpp.getAttributeValue(null, "created");
195                             String modified = xpp.getAttributeValue(null, "modified");
196                             addAttachment(
197                                     new BipAttachmentFormat(
198                                             contentType, charset, name, size, created, modified));
199                         } else {
200                             Log.w(TAG, "Unrecognized tag in x-bt/img-properties object: " + tag);
201                         }
202                         break;
203                     case XmlPullParser.END_TAG:
204                         break;
205                 }
206                 event = xpp.next();
207             }
208             return;
209         } catch (XmlPullParserException e) {
210             Log.e(TAG, "XML parser error when parsing XML", e);
211         } catch (IOException e) {
212             Log.e(TAG, "I/O error when parsing XML", e);
213         }
214         throw new ParseException("Failed to parse image-properties from stream");
215     }
216 
getImageHandle()217     public String getImageHandle() {
218         return mImageHandle;
219     }
220 
getVersion()221     public String getVersion() {
222         return mVersion;
223     }
224 
getFriendlyName()225     public String getFriendlyName() {
226         return mFriendlyName;
227     }
228 
getNativeFormats()229     public List<BipImageFormat> getNativeFormats() {
230         return mNativeFormats;
231     }
232 
getVariantFormats()233     public List<BipImageFormat> getVariantFormats() {
234         return mVariantFormats;
235     }
236 
getAttachments()237     public List<BipAttachmentFormat> getAttachments() {
238         return mAttachments;
239     }
240 
addNativeFormat(BipImageFormat format)241     private void addNativeFormat(BipImageFormat format) {
242         requireNonNull(format);
243         if (format.getType() != BipImageFormat.FORMAT_NATIVE) {
244             throw new IllegalArgumentException(
245                     "Format type '"
246                             + format.getType()
247                             + "' but expected '"
248                             + BipImageFormat.FORMAT_NATIVE
249                             + "'");
250         }
251         mNativeFormats.add(format);
252 
253         if (!mHasThumbnailFormat && isThumbnailFormat(format)) {
254             mHasThumbnailFormat = true;
255         }
256     }
257 
addVariantFormat(BipImageFormat format)258     private void addVariantFormat(BipImageFormat format) {
259         requireNonNull(format);
260         if (format.getType() != BipImageFormat.FORMAT_VARIANT) {
261             throw new IllegalArgumentException(
262                     "Format type '"
263                             + format.getType()
264                             + "' but expected '"
265                             + BipImageFormat.FORMAT_VARIANT
266                             + "'");
267         }
268         mVariantFormats.add(format);
269 
270         if (!mHasThumbnailFormat && isThumbnailFormat(format)) {
271             mHasThumbnailFormat = true;
272         }
273     }
274 
isThumbnailFormat(BipImageFormat format)275     private static boolean isThumbnailFormat(BipImageFormat format) {
276         if (format == null) return false;
277 
278         BipEncoding encoding = format.getEncoding();
279         if (encoding == null || encoding.getType() != BipEncoding.JPEG) return false;
280 
281         BipPixel pixel = format.getPixel();
282         if (pixel == null) return false;
283         switch (pixel.getType()) {
284             case BipPixel.TYPE_FIXED:
285                 return pixel.getMaxWidth() == 200 && pixel.getMaxHeight() == 200;
286             case BipPixel.TYPE_RESIZE_MODIFIED_ASPECT_RATIO:
287                 return pixel.getMaxWidth() >= 200 && pixel.getMaxHeight() >= 200;
288             case BipPixel.TYPE_RESIZE_FIXED_ASPECT_RATIO:
289                 return pixel.getMaxWidth() == pixel.getMaxHeight() && pixel.getMaxWidth() >= 200;
290         }
291         return false;
292     }
293 
addAttachment(BipAttachmentFormat format)294     private void addAttachment(BipAttachmentFormat format) {
295         requireNonNull(format);
296         mAttachments.add(format);
297     }
298 
299     @Override
300     @SuppressLint("ToStringReturnsNull") // Since this is used for encoding to xml
toString()301     public String toString() {
302         StringWriter writer = new StringWriter();
303         XmlSerializer xmlMsgElement = Xml.newSerializer();
304         try {
305             xmlMsgElement.setOutput(writer);
306             xmlMsgElement.startDocument("UTF-8", true);
307             xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
308             xmlMsgElement.startTag(null, "image-properties");
309             if (mVersion != null) xmlMsgElement.attribute(null, "version", mVersion);
310             if (mImageHandle != null) xmlMsgElement.attribute(null, "handle", mImageHandle);
311             if (mFriendlyName != null) {
312                 xmlMsgElement.attribute(null, "friendly-name", mFriendlyName);
313             }
314 
315             for (BipImageFormat format : mNativeFormats) {
316                 BipEncoding encoding = format.getEncoding();
317                 BipPixel pixel = format.getPixel();
318                 int size = format.getSize();
319                 if (encoding == null || pixel == null) {
320                     Log.e(TAG, "Native format " + format.toString() + " is invalid.");
321                     continue;
322                 }
323                 xmlMsgElement.startTag(null, "native");
324                 xmlMsgElement.attribute(null, "encoding", encoding.toString());
325                 xmlMsgElement.attribute(null, "pixel", pixel.toString());
326                 if (size >= 0) {
327                     xmlMsgElement.attribute(null, "size", Integer.toString(size));
328                 }
329                 xmlMsgElement.endTag(null, "native");
330             }
331 
332             for (BipImageFormat format : mVariantFormats) {
333                 BipEncoding encoding = format.getEncoding();
334                 BipPixel pixel = format.getPixel();
335                 int maxSize = format.getMaxSize();
336                 BipTransformation trans = format.getTransformation();
337                 if (encoding == null || pixel == null) {
338                     Log.e(TAG, "Variant format " + format.toString() + " is invalid.");
339                     continue;
340                 }
341                 xmlMsgElement.startTag(null, "variant");
342                 xmlMsgElement.attribute(null, "encoding", encoding.toString());
343                 xmlMsgElement.attribute(null, "pixel", pixel.toString());
344                 if (maxSize >= 0) {
345                     xmlMsgElement.attribute(null, "maxsize", Integer.toString(maxSize));
346                 }
347                 if (trans != null && trans.supportsAny()) {
348                     xmlMsgElement.attribute(null, "transformation", trans.toString());
349                 }
350                 xmlMsgElement.endTag(null, "variant");
351             }
352 
353             for (BipAttachmentFormat format : mAttachments) {
354                 String contentType = format.getContentType();
355                 String charset = format.getCharset();
356                 String name = format.getName();
357                 int size = format.getSize();
358                 BipDateTime created = format.getCreatedDate();
359                 BipDateTime modified = format.getModifiedDate();
360                 if (contentType == null || name == null) {
361                     Log.e(TAG, "Attachment format " + format.toString() + " is invalid.");
362                     continue;
363                 }
364                 xmlMsgElement.startTag(null, "attachment");
365                 xmlMsgElement.attribute(null, "content-type", contentType.toString());
366                 if (charset != null) {
367                     xmlMsgElement.attribute(null, "charset", charset.toString());
368                 }
369                 xmlMsgElement.attribute(null, "name", name.toString());
370                 if (size >= 0) {
371                     xmlMsgElement.attribute(null, "size", Integer.toString(size));
372                 }
373                 if (created != null) {
374                     xmlMsgElement.attribute(null, "created", created.toString());
375                 }
376                 if (modified != null) {
377                     xmlMsgElement.attribute(null, "modified", modified.toString());
378                 }
379                 xmlMsgElement.endTag(null, "attachment");
380             }
381 
382             xmlMsgElement.endTag(null, "image-properties");
383             xmlMsgElement.endDocument();
384             return writer.toString();
385         } catch (IllegalArgumentException e) {
386             Log.e(TAG, "Failed to serialize ImageProperties", e);
387         } catch (IllegalStateException e) {
388             Log.e(TAG, "Failed to serialize ImageProperties", e);
389         } catch (IOException e) {
390             Log.e(TAG, "Failed to serialize ImageProperties", e);
391         }
392         return null;
393     }
394 
395     /**
396      * Serialize this object into a byte array
397      *
398      * <p>Objects that are not valid will fail to serialize and return null.
399      *
400      * @return Byte array representing this object, ready to send over OBEX, or null on error.
401      */
serialize()402     public byte[] serialize() {
403         if (!isValid()) return null;
404         String s = toString();
405         return s != null ? s.getBytes(StandardCharsets.UTF_8) : null;
406     }
407 
408     /**
409      * Determine if the contents of this BipImageProperties object are valid and meet the
410      * specification requirements: 1. Include the fixed 1.0 version 2. Include an image handle 3.
411      * Have the thumbnail format as either the native or variant
412      *
413      * @return True if our contents are valid, false otherwise
414      */
isValid()415     public boolean isValid() {
416         return sVersion.equals(mVersion) && mImageHandle != null && mHasThumbnailFormat;
417     }
418 }
419