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