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 
33 /**
34  * Contains the metadata that describes either (1) the desired size of a image to be downloaded or
35  * (2) the extact size of an image to be uploaded.
36  *
37  * When using this to assert the size of an image to download/pull, it's best to derive this
38  * specific descriptor from any of the available BipImageFormat options returned from a
39  * RequestGetImageProperties. Note that if a BipImageFormat is not of a fixed size type then you
40  * must arrive on a desired fixed size for this descriptor.
41  *
42  * When using this to denote the size of an image when pushing an image for transfer this descriptor
43  * must match the metadata of the image being sent.
44  *
45  * Note, the encoding and pixel values are mandatory by specification. The version number is fixed.
46  * All other values are optional. The transformation field is to have *one* selected option in it.
47  *
48  * Example:
49  *   < image-descriptor version=“1.0” >
50  *   < image encoding=“JPEG” pixel=“1280*960” size=“500000”/>
51  *   < /image-descriptor >
52  */
53 public class BipImageDescriptor {
54     private static final String TAG = "avrcpcontroller.BipImageDescriptor";
55     private static final String sVersion = "1.0";
56 
57     /**
58      * A Builder for an ImageDescriptor object
59      */
60     public static class Builder {
61         private BipImageDescriptor mImageDescriptor = new BipImageDescriptor();
62         /**
63          * Set the encoding for the descriptor you're building using a BipEncoding object
64          *
65          * @param encoding The encoding you would like to set
66          * @return This object so you can continue building
67          */
setEncoding(BipEncoding encoding)68         public Builder setEncoding(BipEncoding encoding) {
69             mImageDescriptor.mEncoding = encoding;
70             return this;
71         }
72 
73         /**
74          * Set the encoding for the descriptor you're building using a BipEncoding.* type value
75          *
76          * @param encoding The encoding you would like to set as a BipEncoding.* type value
77          * @return This object so you can continue building
78          */
setEncoding(int encoding)79         public Builder setEncoding(int encoding) {
80             mImageDescriptor.mEncoding = new BipEncoding(encoding, null);
81             return this;
82         }
83 
84         /**
85          * Set the encoding for the descriptor you're building using a BIP defined string name of
86          * the encoding you want
87          *
88          * @param encoding The encoding you would like to set as a BIP spec defined string
89          * @return This object so you can continue building
90          */
setPropietaryEncoding(String encoding)91         public Builder setPropietaryEncoding(String encoding) {
92             mImageDescriptor.mEncoding = new BipEncoding(BipEncoding.USR_XXX, encoding);
93             return this;
94         }
95 
96         /**
97          * Set the fixed X by Y image dimensions for the descriptor you're building
98          *
99          * @param width The number of pixels in width of the image
100          * @param height The number of pixels in height of the image
101          * @return This object so you can continue building
102          */
setFixedDimensions(int width, int height)103         public Builder setFixedDimensions(int width, int height) {
104             mImageDescriptor.mPixel = BipPixel.createFixed(width, height);
105             return this;
106         }
107 
108         /**
109          * Set the transformation used for the descriptor you're building
110          *
111          * @param transformation The BipTransformation.* type value of the used transformation
112          * @return This object so you can continue building
113          */
setTransformation(int transformation)114         public Builder setTransformation(int transformation) {
115             mImageDescriptor.mTransformation = new BipTransformation(transformation);
116             return this;
117         }
118 
119         /**
120          * Set the image file size for the descriptor you're building
121          *
122          * @param size The image size in bytes
123          * @return This object so you can continue building
124          */
setFileSize(int size)125         public Builder setFileSize(int size) {
126             mImageDescriptor.mSize = size;
127             return this;
128         }
129 
130         /**
131          * Set the max file size of the image for the descriptor you're building
132          *
133          * @param size The maxe image size in bytes
134          * @return This object so you can continue building
135          */
setMaxFileSize(int size)136         public Builder setMaxFileSize(int size) {
137             mImageDescriptor.mMaxSize = size;
138             return this;
139         }
140 
141         /**
142          * Build the object
143          *
144          * @return A BipImageDescriptor object
145          */
build()146         public BipImageDescriptor build() {
147             return mImageDescriptor;
148         }
149     }
150 
151     /**
152      * The version of the image-descriptor XML string
153      */
154     private String mVersion = null;
155 
156     /**
157      * The encoding of the image, required by the specification
158      */
159     private BipEncoding mEncoding = null;
160 
161     /**
162      * The width and height of the image, required by the specification
163      */
164     private BipPixel mPixel = null;
165 
166     /**
167      * The transformation to be applied to the image, *one* of BipTransformation.STRETCH,
168      * BipTransformation.CROP, or BipTransformation.FILL placed into a BipTransformation object
169      */
170     private BipTransformation mTransformation = null;
171 
172     /**
173      * The size in bytes of the image
174      */
175     private int mSize = -1;
176 
177     /**
178      * The max size in bytes of the image.
179      *
180      * Optional, used only when describing an image to pull
181      */
182     private int mMaxSize = -1;
183 
BipImageDescriptor()184     private BipImageDescriptor() {
185         mVersion = sVersion;
186     }
187 
BipImageDescriptor(InputStream inputStream)188     public BipImageDescriptor(InputStream inputStream) {
189         parse(inputStream);
190     }
191 
parse(InputStream inputStream)192     private void parse(InputStream inputStream) {
193         try {
194             XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
195             xpp.setInput(inputStream, "utf-8");
196             int event = xpp.getEventType();
197             while (event != XmlPullParser.END_DOCUMENT) {
198                 switch (event) {
199                     case XmlPullParser.START_TAG:
200                         String tag = xpp.getName();
201                         if (tag.equals("image-descriptor")) {
202                             mVersion = xpp.getAttributeValue(null, "version");
203                         } else if (tag.equals("image")) {
204                             mEncoding = new BipEncoding(xpp.getAttributeValue(null, "encoding"));
205                             mPixel = new BipPixel(xpp.getAttributeValue(null, "pixel"));
206                             mSize = parseInt(xpp.getAttributeValue(null, "size"));
207                             mMaxSize = parseInt(xpp.getAttributeValue(null, "maxsize"));
208                             mTransformation = new BipTransformation(
209                                         xpp.getAttributeValue(null, "transformation"));
210                         } else {
211                             Log.w(TAG, "Unrecognized tag in x-bt/img-Description object: " + tag);
212                         }
213                         break;
214                     case XmlPullParser.END_TAG:
215                         break;
216                 }
217                 event = xpp.next();
218             }
219             return;
220         } catch (XmlPullParserException e) {
221             Log.e(TAG, "XML parser error when parsing XML", e);
222         } catch (IOException e) {
223             Log.e(TAG, "I/O error when parsing XML", e);
224         }
225         throw new ParseException("Failed to parse image-descriptor from stream");
226     }
227 
parseInt(String s)228     private static int parseInt(String s) {
229         if (s == null) return -1;
230         try {
231             return Integer.parseInt(s);
232         } catch (NumberFormatException e) {
233             error("Failed to parse '" + s + "'");
234         }
235         return -1;
236     }
237 
getEncoding()238     public BipEncoding getEncoding() {
239         return mEncoding;
240     }
241 
getPixel()242     public BipPixel getPixel() {
243         return mPixel;
244     }
245 
getTransformation()246     public BipTransformation getTransformation() {
247         return mTransformation;
248     }
249 
getSize()250     public int getSize() {
251         return mSize;
252     }
253 
getMaxSize()254     public int getMaxSize() {
255         return mMaxSize;
256     }
257 
258     /**
259      * Serialize this object into a byte array ready for transfer overOBEX
260      *
261      * @return A byte array containing this object's info, or null on error.
262      */
serialize()263     public byte[] serialize() {
264         String s = toString();
265         try {
266             return s != null ? s.getBytes("UTF-8") : null;
267         } catch (UnsupportedEncodingException e) {
268             return null;
269         }
270     }
271 
272     @Override
equals(Object o)273     public boolean equals(Object o) {
274         if (o == this) return true;
275         if (!(o instanceof BipImageDescriptor)) return false;
276 
277         BipImageDescriptor d = (BipImageDescriptor) o;
278         return d.getEncoding() == getEncoding()
279                 && d.getPixel() == getPixel()
280                 && d.getTransformation() == getTransformation()
281                 && d.getSize() == getSize()
282                 && d.getMaxSize() == getMaxSize();
283     }
284 
285     @Override
toString()286     public String toString() {
287         if (mEncoding == null || mPixel == null) {
288             error("Missing required fields [ " + (mEncoding == null ? "encoding " : "")
289                     + (mPixel == null ? "pixel " : ""));
290             return null;
291         }
292         StringWriter writer = new StringWriter();
293         XmlSerializer xmlMsgElement = new FastXmlSerializer();
294         try {
295             xmlMsgElement.setOutput(writer);
296             xmlMsgElement.startDocument("UTF-8", true);
297             xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
298             xmlMsgElement.startTag(null, "image-descriptor");
299             xmlMsgElement.attribute(null, "version", sVersion);
300             xmlMsgElement.startTag(null, "image");
301             xmlMsgElement.attribute(null, "encoding", mEncoding.toString());
302             xmlMsgElement.attribute(null, "pixel", mPixel.toString());
303             if (mSize != -1) {
304                 xmlMsgElement.attribute(null, "size", Integer.toString(mSize));
305             }
306             if (mMaxSize != -1) {
307                 xmlMsgElement.attribute(null, "maxsize", Integer.toString(mMaxSize));
308             }
309             if (mTransformation != null && mTransformation.supportsAny()) {
310                 xmlMsgElement.attribute(null, "transformation", mTransformation.toString());
311             }
312             xmlMsgElement.endTag(null, "image");
313             xmlMsgElement.endTag(null, "image-descriptor");
314             xmlMsgElement.endDocument();
315             return writer.toString();
316         } catch (IllegalArgumentException e) {
317             error(e.toString());
318         } catch (IllegalStateException e) {
319             error(e.toString());
320         } catch (IOException e) {
321             error(e.toString());
322         }
323         return null;
324     }
325 
error(String msg)326     private static void error(String msg) {
327         Log.e(TAG, msg);
328     }
329 }
330