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