blob: 70d935a4f3b4bb27e8a80fb3e700003125e68daa [file] [log] [blame]
/*
* 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:
* <image-properties version="1.0" handle="123456789">
* <native encoding="JPEG" pixel="1280*1024" size="1048576"/>
* <variant encoding="JPEG" pixel="640*480" />
* <variant encoding="JPEG" pixel="160*120" />
* <variant encoding="GIF" pixel="80*60-640*480" transformation="stretch fill crop"/>
* <attachment content-type="text/plain" name="ABCD1234.txt" size="5120"/>
* <attachment content-type="audio/basic" name="ABCD1234.wav" size="102400"/>
* </image-properties>
*/
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<BipImageFormat> mNativeFormats;
private ArrayList<BipImageFormat> mVariantFormats;
private ArrayList<BipAttachmentFormat> mAttachments;
private BipImageProperties() {
mVersion = sVersion;
mNativeFormats = new ArrayList<BipImageFormat>();
mVariantFormats = new ArrayList<BipImageFormat>();
mAttachments = new ArrayList<BipAttachmentFormat>();
}
public BipImageProperties(InputStream inputStream) {
mNativeFormats = new ArrayList<BipImageFormat>();
mVariantFormats = new ArrayList<BipImageFormat>();
mAttachments = new ArrayList<BipAttachmentFormat>();
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<BipImageFormat> getNativeFormats() {
return mNativeFormats;
}
public ArrayList<BipImageFormat> getVariantFormats() {
return mVariantFormats;
}
public ArrayList<BipAttachmentFormat> 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);
}
}