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