/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.avrcpcontroller; import android.util.Log; import com.android.internal.util.FastXmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Objects; /** * Represents the return value of a BIP GetImageProperties request, giving a detailed description of * an image and its available descriptors before download. * * Format is as described by version 1.2.1 of the Basic Image Profile Specification. The * specification describes three types of metadata that can arrive with an image -- native, variant * and attachment. Native describes which native formats a particular image is available in. * Variant describes which other types of encodings/sizes can be created from the native image using * various transformations. Attachments describes other items that can be downloaded that are * associated with the image (text, sounds, etc.) * * Example: * * * * * * * * */ public class BipImageProperties { private static final String TAG = "avrcpcontroller.BipImageProperties"; private static final String sVersion = "1.0"; /** * A Builder for a BipImageProperties object */ public static class Builder { private BipImageProperties mProperties = new BipImageProperties(); /** * Set the image handle field for the object you're building * * @param handle The image handle you want to add to the object * @return The builder object to keep building on top of */ public Builder setImageHandle(String handle) { mProperties.mImageHandle = handle; return this; } /** * Set the FriendlyName field for the object you're building * * @param friendlyName The friendly name you want to add to the object * @return The builder object to keep building on top of */ public Builder setFriendlyName(String friendlyName) { mProperties.mFriendlyName = friendlyName; return this; } /** * Add a native format for the object you're building * * @param format The format you want to add to the object * @return The builder object to keep building on top of */ public Builder addNativeFormat(BipImageFormat format) { mProperties.addNativeFormat(format); return this; } /** * Add a variant format for the object you're building * * @param format The format you want to add to the object * @return The builder object to keep building on top of */ public Builder addVariantFormat(BipImageFormat format) { mProperties.addVariantFormat(format); return this; } /** * Add an attachment entry for the object you're building * * @param format The format you want to add to the object * @return The builder object to keep building on top of */ public Builder addAttachment(BipAttachmentFormat format) { mProperties.addAttachment(format); return this; } /** * Build the object * * @return A BipImageProperties object */ public BipImageProperties build() { return mProperties; } } /** * The image handle associated with this set of properties. */ private String mImageHandle = null; /** * The version of the properties object, used to encode and decode. */ private String mVersion = null; /** * An optional friendly name for the associated image. The specification suggests the file name. */ private String mFriendlyName = null; /** * The various sets of available formats. */ private ArrayList mNativeFormats; private ArrayList mVariantFormats; private ArrayList mAttachments; private BipImageProperties() { mVersion = sVersion; mNativeFormats = new ArrayList(); mVariantFormats = new ArrayList(); mAttachments = new ArrayList(); } public BipImageProperties(InputStream inputStream) { mNativeFormats = new ArrayList(); mVariantFormats = new ArrayList(); mAttachments = new ArrayList(); parse(inputStream); } private void parse(InputStream inputStream) { try { XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); xpp.setInput(inputStream, "utf-8"); int event = xpp.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { switch (event) { case XmlPullParser.START_TAG: String tag = xpp.getName(); if (tag.equals("image-properties")) { mVersion = xpp.getAttributeValue(null, "version"); mImageHandle = xpp.getAttributeValue(null, "handle"); mFriendlyName = xpp.getAttributeValue(null, "friendly-name"); } else if (tag.equals("native")) { String encoding = xpp.getAttributeValue(null, "encoding"); String pixel = xpp.getAttributeValue(null, "pixel"); String size = xpp.getAttributeValue(null, "size"); addNativeFormat(BipImageFormat.parseNative(encoding, pixel, size)); } else if (tag.equals("variant")) { String encoding = xpp.getAttributeValue(null, "encoding"); String pixel = xpp.getAttributeValue(null, "pixel"); String maxSize = xpp.getAttributeValue(null, "maxsize"); String trans = xpp.getAttributeValue(null, "transformation"); addVariantFormat( BipImageFormat.parseVariant(encoding, pixel, maxSize, trans)); } else if (tag.equals("attachment")) { String contentType = xpp.getAttributeValue(null, "content-type"); String name = xpp.getAttributeValue(null, "name"); String charset = xpp.getAttributeValue(null, "charset"); String size = xpp.getAttributeValue(null, "size"); String created = xpp.getAttributeValue(null, "created"); String modified = xpp.getAttributeValue(null, "modified"); addAttachment( new BipAttachmentFormat(contentType, charset, name, size, created, modified)); } else { warn("Unrecognized tag in x-bt/img-properties object: " + tag); } break; case XmlPullParser.END_TAG: break; } event = xpp.next(); } return; } catch (XmlPullParserException e) { error("XML parser error when parsing XML", e); } catch (IOException e) { error("I/O error when parsing XML", e); } throw new ParseException("Failed to parse image-properties from stream"); } public String getImageHandle() { return mImageHandle; } public String getFriendlyName() { return mFriendlyName; } public ArrayList getNativeFormats() { return mNativeFormats; } public ArrayList getVariantFormats() { return mVariantFormats; } public ArrayList getAttachments() { return mAttachments; } private void addNativeFormat(BipImageFormat format) { Objects.requireNonNull(format); if (format.getType() != BipImageFormat.FORMAT_NATIVE) { throw new IllegalArgumentException("Format type '" + format.getType() + "' but expected '" + BipImageFormat.FORMAT_NATIVE + "'"); } mNativeFormats.add(format); } private void addVariantFormat(BipImageFormat format) { Objects.requireNonNull(format); if (format.getType() != BipImageFormat.FORMAT_VARIANT) { throw new IllegalArgumentException("Format type '" + format.getType() + "' but expected '" + BipImageFormat.FORMAT_VARIANT + "'"); } mVariantFormats.add(format); } private void addAttachment(BipAttachmentFormat format) { Objects.requireNonNull(format); mAttachments.add(format); } @Override public String toString() { StringWriter writer = new StringWriter(); XmlSerializer xmlMsgElement = new FastXmlSerializer(); try { xmlMsgElement.setOutput(writer); xmlMsgElement.startDocument("UTF-8", true); xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); xmlMsgElement.startTag(null, "image-properties"); xmlMsgElement.attribute(null, "version", mVersion); xmlMsgElement.attribute(null, "handle", mImageHandle); for (BipImageFormat format : mNativeFormats) { BipEncoding encoding = format.getEncoding(); BipPixel pixel = format.getPixel(); int size = format.getSize(); if (encoding == null || pixel == null) { error("Native format " + format.toString() + " is invalid."); continue; } xmlMsgElement.startTag(null, "native"); xmlMsgElement.attribute(null, "encoding", encoding.toString()); xmlMsgElement.attribute(null, "pixel", pixel.toString()); if (size >= 0) { xmlMsgElement.attribute(null, "size", Integer.toString(size)); } xmlMsgElement.endTag(null, "native"); } for (BipImageFormat format : mVariantFormats) { BipEncoding encoding = format.getEncoding(); BipPixel pixel = format.getPixel(); int maxSize = format.getMaxSize(); BipTransformation trans = format.getTransformation(); if (encoding == null || pixel == null) { error("Variant format " + format.toString() + " is invalid."); continue; } xmlMsgElement.startTag(null, "variant"); xmlMsgElement.attribute(null, "encoding", encoding.toString()); xmlMsgElement.attribute(null, "pixel", pixel.toString()); if (maxSize >= 0) { xmlMsgElement.attribute(null, "maxsize", Integer.toString(maxSize)); } if (trans != null && trans.supportsAny()) { xmlMsgElement.attribute(null, "transformation", trans.toString()); } xmlMsgElement.endTag(null, "variant"); } for (BipAttachmentFormat format : mAttachments) { String contentType = format.getContentType(); String charset = format.getCharset(); String name = format.getName(); int size = format.getSize(); BipDateTime created = format.getCreatedDate(); BipDateTime modified = format.getModifiedDate(); if (contentType == null || name == null) { error("Attachment format " + format.toString() + " is invalid."); continue; } xmlMsgElement.startTag(null, "attachment"); xmlMsgElement.attribute(null, "content-type", contentType.toString()); if (charset != null) { xmlMsgElement.attribute(null, "charset", charset.toString()); } xmlMsgElement.attribute(null, "name", name.toString()); if (size >= 0) { xmlMsgElement.attribute(null, "size", Integer.toString(size)); } if (created != null) { xmlMsgElement.attribute(null, "created", created.toString()); } if (modified != null) { xmlMsgElement.attribute(null, "modified", modified.toString()); } xmlMsgElement.endTag(null, "attachment"); } xmlMsgElement.endTag(null, "image-properties"); xmlMsgElement.endDocument(); return writer.toString(); } catch (IllegalArgumentException e) { error("Falied to serialize ImageProperties", e); } catch (IllegalStateException e) { error("Falied to serialize ImageProperties", e); } catch (IOException e) { error("Falied to serialize ImageProperties", e); } return null; } /** * Serialize this object into a byte array * * @return Byte array representing this object, ready to send over OBEX, or null on error. */ public byte[] serialize() { String s = toString(); try { return s != null ? s.getBytes("UTF-8") : null; } catch (UnsupportedEncodingException e) { return null; } } private static void warn(String msg) { Log.w(TAG, msg); } private static void error(String msg) { Log.e(TAG, msg); } private static void error(String msg, Throwable e) { Log.e(TAG, msg, e); } }