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  * Example:
47  *     <image-properties version="1.0" handle="123456789">
48  *     <native encoding="JPEG" pixel="1280*1024" size="1048576"/>
49  *     <variant encoding="JPEG" pixel="640*480" />
50  *     <variant encoding="JPEG" pixel="160*120" />
51  *     <variant encoding="GIF" pixel="80*60-640*480" transformation="stretch fill crop"/>
52  *     <attachment content-type="text/plain" name="ABCD1234.txt" size="5120"/>
53  *     <attachment content-type="audio/basic" name="ABCD1234.wav" size="102400"/>
54  *     </image-properties>
55  */
56 public class BipImageProperties {
57     private static final String TAG = "avrcpcontroller.BipImageProperties";
58     private static final String sVersion = "1.0";
59 
60     /**
61      * A Builder for a BipImageProperties object
62      */
63     public static class Builder {
64         private BipImageProperties mProperties = new BipImageProperties();
65         /**
66          * Set the image handle field for the object you're building
67          *
68          * @param handle The image handle you want to add to the object
69          * @return The builder object to keep building on top of
70          */
setImageHandle(String handle)71         public Builder setImageHandle(String handle) {
72             mProperties.mImageHandle = handle;
73             return this;
74         }
75 
76         /**
77          * Set the FriendlyName field for the object you're building
78          *
79          * @param friendlyName The friendly name you want to add to the object
80          * @return The builder object to keep building on top of
81          */
setFriendlyName(String friendlyName)82         public Builder setFriendlyName(String friendlyName) {
83             mProperties.mFriendlyName = friendlyName;
84             return this;
85         }
86 
87         /**
88          * Add a native format for the object you're building
89          *
90          * @param format The format you want to add to the object
91          * @return The builder object to keep building on top of
92          */
addNativeFormat(BipImageFormat format)93         public Builder addNativeFormat(BipImageFormat format) {
94             mProperties.addNativeFormat(format);
95             return this;
96         }
97 
98         /**
99          * Add a variant format for the object you're building
100          *
101          * @param format The format you want to add to the object
102          * @return The builder object to keep building on top of
103          */
addVariantFormat(BipImageFormat format)104         public Builder addVariantFormat(BipImageFormat format) {
105             mProperties.addVariantFormat(format);
106             return this;
107         }
108 
109         /**
110          * Add an attachment entry for the object you're building
111          *
112          * @param format The format you want to add to the object
113          * @return The builder object to keep building on top of
114          */
addAttachment(BipAttachmentFormat format)115         public Builder addAttachment(BipAttachmentFormat format) {
116             mProperties.addAttachment(format);
117             return this;
118         }
119 
120         /**
121          * Build the object
122          *
123          * @return A BipImageProperties object
124          */
build()125         public BipImageProperties build() {
126             return mProperties;
127         }
128     }
129 
130     /**
131      * The image handle associated with this set of properties.
132      */
133     private String mImageHandle = null;
134 
135     /**
136      * The version of the properties object, used to encode and decode.
137      */
138     private String mVersion = null;
139 
140     /**
141      * An optional friendly name for the associated image. The specification suggests the file name.
142      */
143     private String mFriendlyName = null;
144 
145     /**
146      * The various sets of available formats.
147      */
148     private ArrayList<BipImageFormat> mNativeFormats;
149     private ArrayList<BipImageFormat> mVariantFormats;
150     private ArrayList<BipAttachmentFormat> mAttachments;
151 
BipImageProperties()152     private BipImageProperties() {
153         mVersion = sVersion;
154         mNativeFormats = new ArrayList<BipImageFormat>();
155         mVariantFormats = new ArrayList<BipImageFormat>();
156         mAttachments = new ArrayList<BipAttachmentFormat>();
157     }
158 
BipImageProperties(InputStream inputStream)159     public BipImageProperties(InputStream inputStream) {
160         mNativeFormats = new ArrayList<BipImageFormat>();
161         mVariantFormats = new ArrayList<BipImageFormat>();
162         mAttachments = new ArrayList<BipAttachmentFormat>();
163         parse(inputStream);
164     }
165 
parse(InputStream inputStream)166     private void parse(InputStream inputStream) {
167         try {
168             XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
169             xpp.setInput(inputStream, "utf-8");
170             int event = xpp.getEventType();
171             while (event != XmlPullParser.END_DOCUMENT) {
172                 switch (event) {
173                     case XmlPullParser.START_TAG:
174                         String tag = xpp.getName();
175                         if (tag.equals("image-properties")) {
176                             mVersion = xpp.getAttributeValue(null, "version");
177                             mImageHandle = xpp.getAttributeValue(null, "handle");
178                             mFriendlyName = xpp.getAttributeValue(null, "friendly-name");
179                         } else if (tag.equals("native")) {
180                             String encoding = xpp.getAttributeValue(null, "encoding");
181                             String pixel = xpp.getAttributeValue(null, "pixel");
182                             String size = xpp.getAttributeValue(null, "size");
183                             addNativeFormat(BipImageFormat.parseNative(encoding, pixel, size));
184                         } else if (tag.equals("variant")) {
185                             String encoding = xpp.getAttributeValue(null, "encoding");
186                             String pixel = xpp.getAttributeValue(null, "pixel");
187                             String maxSize = xpp.getAttributeValue(null, "maxsize");
188                             String trans = xpp.getAttributeValue(null, "transformation");
189                             addVariantFormat(
190                                     BipImageFormat.parseVariant(encoding, pixel, maxSize, trans));
191                         } else if (tag.equals("attachment")) {
192                             String contentType = xpp.getAttributeValue(null, "content-type");
193                             String name = xpp.getAttributeValue(null, "name");
194                             String charset = xpp.getAttributeValue(null, "charset");
195                             String size = xpp.getAttributeValue(null, "size");
196                             String created = xpp.getAttributeValue(null, "created");
197                             String modified = xpp.getAttributeValue(null, "modified");
198                             addAttachment(
199                                     new BipAttachmentFormat(contentType, charset, name, size,
200                                             created, modified));
201                         } else {
202                             warn("Unrecognized tag in x-bt/img-properties object: " + tag);
203                         }
204                         break;
205                     case XmlPullParser.END_TAG:
206                         break;
207                 }
208                 event = xpp.next();
209             }
210             return;
211         } catch (XmlPullParserException e) {
212             error("XML parser error when parsing XML", e);
213         } catch (IOException e) {
214             error("I/O error when parsing XML", e);
215         }
216         throw new ParseException("Failed to parse image-properties from stream");
217     }
218 
getImageHandle()219     public String getImageHandle() {
220         return mImageHandle;
221     }
222 
getFriendlyName()223     public String getFriendlyName() {
224         return mFriendlyName;
225     }
226 
getNativeFormats()227     public ArrayList<BipImageFormat> getNativeFormats() {
228         return mNativeFormats;
229     }
230 
getVariantFormats()231     public ArrayList<BipImageFormat> getVariantFormats() {
232         return mVariantFormats;
233     }
234 
getAttachments()235     public ArrayList<BipAttachmentFormat> getAttachments() {
236         return mAttachments;
237     }
238 
addNativeFormat(BipImageFormat format)239     private void addNativeFormat(BipImageFormat format) {
240         Objects.requireNonNull(format);
241         if (format.getType() != BipImageFormat.FORMAT_NATIVE) {
242             throw new IllegalArgumentException("Format type '" + format.getType()
243                     + "' but expected '" + BipImageFormat.FORMAT_NATIVE + "'");
244         }
245         mNativeFormats.add(format);
246     }
247 
addVariantFormat(BipImageFormat format)248     private void addVariantFormat(BipImageFormat format) {
249         Objects.requireNonNull(format);
250         if (format.getType() != BipImageFormat.FORMAT_VARIANT) {
251             throw new IllegalArgumentException("Format type '" + format.getType()
252                     + "' but expected '" + BipImageFormat.FORMAT_VARIANT + "'");
253         }
254         mVariantFormats.add(format);
255     }
256 
addAttachment(BipAttachmentFormat format)257     private void addAttachment(BipAttachmentFormat format) {
258         Objects.requireNonNull(format);
259         mAttachments.add(format);
260     }
261 
262     @Override
toString()263     public String toString() {
264         StringWriter writer = new StringWriter();
265         XmlSerializer xmlMsgElement = new FastXmlSerializer();
266         try {
267             xmlMsgElement.setOutput(writer);
268             xmlMsgElement.startDocument("UTF-8", true);
269             xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
270             xmlMsgElement.startTag(null, "image-properties");
271             xmlMsgElement.attribute(null, "version", mVersion);
272             xmlMsgElement.attribute(null, "handle", mImageHandle);
273 
274             for (BipImageFormat format : mNativeFormats) {
275                 BipEncoding encoding = format.getEncoding();
276                 BipPixel pixel = format.getPixel();
277                 int size = format.getSize();
278                 if (encoding == null || pixel == null) {
279                     error("Native format " + format.toString() + " is invalid.");
280                     continue;
281                 }
282                 xmlMsgElement.startTag(null, "native");
283                 xmlMsgElement.attribute(null, "encoding", encoding.toString());
284                 xmlMsgElement.attribute(null, "pixel", pixel.toString());
285                 if (size >= 0) {
286                     xmlMsgElement.attribute(null, "size", Integer.toString(size));
287                 }
288                 xmlMsgElement.endTag(null, "native");
289             }
290 
291             for (BipImageFormat format : mVariantFormats) {
292                 BipEncoding encoding = format.getEncoding();
293                 BipPixel pixel = format.getPixel();
294                 int maxSize = format.getMaxSize();
295                 BipTransformation trans = format.getTransformation();
296                 if (encoding == null || pixel == null) {
297                     error("Variant format " + format.toString() + " is invalid.");
298                     continue;
299                 }
300                 xmlMsgElement.startTag(null, "variant");
301                 xmlMsgElement.attribute(null, "encoding", encoding.toString());
302                 xmlMsgElement.attribute(null, "pixel", pixel.toString());
303                 if (maxSize >= 0) {
304                     xmlMsgElement.attribute(null, "maxsize", Integer.toString(maxSize));
305                 }
306                 if (trans != null && trans.supportsAny()) {
307                     xmlMsgElement.attribute(null, "transformation", trans.toString());
308                 }
309                 xmlMsgElement.endTag(null, "variant");
310             }
311 
312             for (BipAttachmentFormat format : mAttachments) {
313                 String contentType = format.getContentType();
314                 String charset = format.getCharset();
315                 String name = format.getName();
316                 int size = format.getSize();
317                 BipDateTime created = format.getCreatedDate();
318                 BipDateTime modified = format.getModifiedDate();
319                 if (contentType == null || name == null) {
320                     error("Attachment format " + format.toString() + " is invalid.");
321                     continue;
322                 }
323                 xmlMsgElement.startTag(null, "attachment");
324                 xmlMsgElement.attribute(null, "content-type", contentType.toString());
325                 if (charset != null) {
326                     xmlMsgElement.attribute(null, "charset", charset.toString());
327                 }
328                 xmlMsgElement.attribute(null, "name", name.toString());
329                 if (size >= 0) {
330                     xmlMsgElement.attribute(null, "size", Integer.toString(size));
331                 }
332                 if (created != null) {
333                     xmlMsgElement.attribute(null, "created", created.toString());
334                 }
335                 if (modified != null) {
336                     xmlMsgElement.attribute(null, "modified", modified.toString());
337                 }
338                 xmlMsgElement.endTag(null, "attachment");
339             }
340 
341             xmlMsgElement.endTag(null, "image-properties");
342             xmlMsgElement.endDocument();
343             return writer.toString();
344         } catch (IllegalArgumentException e) {
345             error("Falied to serialize ImageProperties", e);
346         } catch (IllegalStateException e) {
347             error("Falied to serialize ImageProperties", e);
348         } catch (IOException e) {
349             error("Falied to serialize ImageProperties", e);
350         }
351         return null;
352     }
353 
354     /**
355      * Serialize this object into a byte array
356      *
357      * @return Byte array representing this object, ready to send over OBEX, or null on error.
358      */
serialize()359     public byte[] serialize() {
360         String s = toString();
361         try {
362             return s != null ? s.getBytes("UTF-8") : null;
363         } catch (UnsupportedEncodingException e) {
364             return null;
365         }
366     }
367 
warn(String msg)368     private static void warn(String msg) {
369         Log.w(TAG, msg);
370     }
371 
error(String msg)372     private static void error(String msg) {
373         Log.e(TAG, msg);
374     }
375 
error(String msg, Throwable e)376     private static void error(String msg, Throwable e) {
377         Log.e(TAG, msg, e);
378     }
379 }
380