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