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