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