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