diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md
index 325fcf30ae..9fbfa7c2b2 100644
--- a/documentation/extensions/index.md
+++ b/documentation/extensions/index.md
@@ -50,6 +50,7 @@ Smack Extensions and currently supported XEPs of smack-extensions
| Advanced Message Processing | [XEP-0079](http://xmpp.org/extensions/xep-0079.html) | Enables entities to request, and servers to perform, advanced processing of XMPP message stanzas. |
| User Location | [XEP-0080](http://xmpp.org/extensions/xep-0080.html) | Enabled communicating information about the current geographical or physical location of an entity. |
| XMPP Date Time Profiles | [XEP-0082](http://xmpp.org/extensions/xep-0082.html) | Standardization of Date and Time representation in XMPP. |
+| User Avatar | [XEP-0084](http://xmpp.org/extensions/xep-0084.html) | Allows to exchange user avatars, which are small images or icons associated with human users. |
| Chat State Notifications | [XEP-0085](http://xmpp.org/extensions/xep-0085.html) | Communicating the status of a user in a chat session. |
| [Time Exchange](time.md) | [XEP-0090](http://xmpp.org/extensions/xep-0090.html) | Allows local time information to be shared between users. |
| Software Version | [XEP-0092](http://xmpp.org/extensions/xep-0092.html) | Retrieve and announce the software application of an XMPP entity. |
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java
new file mode 100644
index 0000000000..cf153ea68a
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataInfo.java
@@ -0,0 +1,108 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar;
+
+/**
+ * User Avatar metadata info model class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataInfo {
+
+ private final String id;
+ private final String url;
+ private final long bytes;
+ private final String type;
+ private final int height;
+ private final int width;
+
+ /**
+ * MetadataInfo constructor.
+ *
+ * @param id
+ * @param url
+ * @param bytes
+ * @param type
+ * @param pixelsHeight
+ * @param pixelsWidth
+ */
+ public MetadataInfo(String id, String url, long bytes, String type, int pixelsHeight, int pixelsWidth) {
+ this.id = id;
+ this.url = url;
+ this.bytes = bytes;
+ this.type = type;
+ this.height = pixelsHeight;
+ this.width = pixelsWidth;
+ }
+
+ /**
+ * Get the id.
+ *
+ * @return the id
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Get the url.
+ *
+ * @return the url
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * Get the amount of bytes.
+ *
+ * @return the amount of bytes
+ */
+ public long getBytes() {
+ return bytes;
+ }
+
+ /**
+ * Get the type.
+ *
+ * @return the type
+ */
+ public String getType() {
+ return type;
+ }
+
+ /**
+ * Get the height in pixels.
+ *
+ * @return the height in pixels
+ */
+ public int getHeight() {
+ return height;
+ }
+
+ /**
+ * Get the width in pixels.
+ *
+ * @return the width in pixels
+ */
+ public int getWidth() {
+ return width;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java
new file mode 100644
index 0000000000..dfffebf720
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/MetadataPointer.java
@@ -0,0 +1,62 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar;
+
+import java.util.HashMap;
+
+/**
+ * User Avatar metadata pointer model class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataPointer {
+
+ private final String namespace;
+ private final HashMap fields;
+
+ /**
+ * Metadata Pointer constructor.
+ *
+ * @param namespace
+ * @param fields
+ */
+ public MetadataPointer(String namespace, HashMap fields) {
+ this.namespace = namespace;
+ this.fields = fields;
+ }
+
+ /**
+ * Get the namespace.
+ *
+ * @return the namespace
+ */
+ public String getNamespace() {
+ return namespace;
+ }
+
+ /**
+ * Get the fields.
+ *
+ * @return the fields
+ */
+ public HashMap getFields() {
+ return fields;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java
new file mode 100644
index 0000000000..27db87813a
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/UserAvatarManager.java
@@ -0,0 +1,312 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.Manager;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPConnectionRegistry;
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.util.SHA1;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.disco.packet.DiscoverItems.Item;
+import org.jivesoftware.smackx.pubsub.LeafNode;
+import org.jivesoftware.smackx.pubsub.PayloadItem;
+import org.jivesoftware.smackx.pubsub.PubSubManager;
+
+/**
+ * User Avatar manager class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public final class UserAvatarManager extends Manager {
+
+ public static final String DATA_NAMESPACE = "urn:xmpp:avatar:data";
+ public static final String METADATA_NAMESPACE = "urn:xmpp:avatar:metadata";
+
+ static {
+ XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
+ @Override
+ public void connectionCreated(XMPPConnection connection) {
+ getInstanceFor(connection);
+ }
+ });
+ }
+
+ private static final Map INSTANCES = new WeakHashMap<>();
+
+ /**
+ * Get the singleton instance of UserAvatarManager.
+ *
+ * @param connection
+ * @return the instance of UserAvatarManager
+ */
+ public static synchronized UserAvatarManager getInstanceFor(XMPPConnection connection) {
+ UserAvatarManager userAvatarManager = INSTANCES.get(connection);
+
+ if (userAvatarManager == null) {
+ userAvatarManager = new UserAvatarManager(connection);
+ INSTANCES.put(connection, userAvatarManager);
+ }
+
+ return userAvatarManager;
+ }
+
+ private UserAvatarManager(XMPPConnection connection) {
+ super(connection);
+ }
+
+ /**
+ * Returns true if User Avatar is supported by the server.
+ *
+ * @return true if User Avatar is supported by the server.
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public boolean isSupportedByServer()
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ return findNodeItem(DATA_NAMESPACE) && findNodeItem(METADATA_NAMESPACE);
+ }
+
+ private boolean findNodeItem(String nodeName)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection());
+ List- items = serviceDiscoveryManager.discoverItems(connection().getUser().asBareJid(), nodeName)
+ .getItems();
+ return items != null && items.size() > 0;
+ }
+
+ /**
+ * Get the data node.
+ *
+ * @return the data node
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws XMPPErrorException
+ */
+ public LeafNode getDataNode()
+ throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException {
+ return PubSubManager.getInstance(connection()).getOrCreateLeafNode(DATA_NAMESPACE);
+ }
+
+ /**
+ * Get the metadata node.
+ *
+ * @return the metadata node
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ * @throws XMPPErrorException
+ */
+ public LeafNode getMetadataNode()
+ throws NoResponseException, NotConnectedException, InterruptedException, XMPPErrorException {
+ return PubSubManager.getInstance(connection()).getOrCreateLeafNode(METADATA_NAMESPACE);
+ }
+
+ /**
+ * Publish avatar data.
+ *
+ * @param data
+ * @param itemId
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws XMPPErrorException
+ * @throws InterruptedException
+ */
+ public void publishAvatarData(byte[] data, String itemId)
+ throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException {
+ DataExtension dataExtension = new DataExtension(data);
+ getDataNode().publish(new PayloadItem(itemId, dataExtension));
+ }
+
+ /**
+ * Publish avatar data.
+ *
+ * @param data
+ * @return created item id
+ * @throws NoResponseException
+ * @throws NotConnectedException
+ * @throws XMPPErrorException
+ * @throws InterruptedException
+ */
+ public String publishAvatarData(byte[] data)
+ throws NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException {
+ String itemId = Base64.encodeToString(SHA1.bytes(data));
+ publishAvatarData(data, itemId);
+ return itemId;
+ }
+
+ /**
+ * Publish avatar metadata.
+ *
+ * @param itemId
+ * @param info
+ * @param pointers
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, MetadataInfo info, List pointers)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ List infos = new ArrayList<>();
+ infos.add(info);
+ publishAvatarMetadata(itemId, infos, pointers);
+ }
+
+ /**
+ * Publish avatar metadata.
+ *
+ * @param itemId
+ * @param infos
+ * @param pointers
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, List infos, List pointers)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
+ getMetadataNode().publish(new PayloadItem(itemId, metadataExtension));
+ }
+
+ /**
+ * Publish HTTP avatar metadata.
+ *
+ * @param itemId
+ * @param id
+ * @param url
+ * @param bytes
+ * @param type
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishHTTPAvatarMetadata(String itemId, String id, String url, long bytes, String type)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ MetadataInfo info = new MetadataInfo(id, url, bytes, type, 0, 0);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Publish HTTP avatar metadata with its size in pixels.
+ *
+ * @param itemId
+ * @param id
+ * @param url
+ * @param bytes
+ * @param type
+ * @param pixelsHeight
+ * @param pixelsWidth
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishHTTPAvatarMetadataWithSize(String itemId, String id, String url, long bytes, String type,
+ int pixelsHeight, int pixelsWidth)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ MetadataInfo info = new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Publish avatar metadata.
+ *
+ * @param itemId
+ * @param id
+ * @param bytes
+ * @param type
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadata(String itemId, String id, long bytes, String type)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ MetadataInfo info = new MetadataInfo(id, null, bytes, type, 0, 0);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Publish avatar metadata with its size in pixels.
+ *
+ * @param itemId
+ * @param id
+ * @param bytes
+ * @param type
+ * @param pixelsHeight
+ * @param pixelsWidth
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void publishAvatarMetadataWithSize(String itemId, String id, long bytes, String type, int pixelsHeight,
+ int pixelsWidth)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ MetadataInfo info = new MetadataInfo(id, null, bytes, type, pixelsHeight, pixelsWidth);
+ publishAvatarMetadata(itemId, info, null);
+ }
+
+ /**
+ * Get last data of an item.
+ *
+ * @param itemId
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void requestLastData(String itemId)
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ List ids = new ArrayList<>();
+ ids.add(itemId);
+ getDataNode().getItems(ids);
+ }
+
+ /**
+ * Disable avatar publishing.
+ *
+ * @throws NoResponseException
+ * @throws XMPPErrorException
+ * @throws NotConnectedException
+ * @throws InterruptedException
+ */
+ public void disableAvatarPublishing()
+ throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
+ getMetadataNode().publish(new PayloadItem(new MetadataExtension(null)));
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java
new file mode 100644
index 0000000000..cc35a96e2c
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/DataExtension.java
@@ -0,0 +1,84 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar.element;
+
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.util.XmlStringBuilder;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.UserAvatarManager;
+
+/**
+ * Data extension element class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class DataExtension implements ExtensionElement {
+
+ public static final String ELEMENT = "data";
+ public static final String NAMESPACE = UserAvatarManager.DATA_NAMESPACE;
+
+ private final byte[] data;
+
+ /**
+ * Data Extension constructor.
+ *
+ * @param data
+ */
+ public DataExtension(byte[] data) {
+ this.data = data;
+ }
+
+ /**
+ * Get data.
+ *
+ * @return a copy of the immutable data
+ */
+ public byte[] getData() {
+ return data.clone();
+ }
+
+ /**
+ * Get data as String.
+ *
+ * @return the data as String
+ */
+ public String getDataAsString() {
+ return Base64.encodeToString(data);
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public CharSequence toXML() {
+ XmlStringBuilder xml = new XmlStringBuilder(this);
+ xml.rightAngleBracket();
+ xml.escape(this.getDataAsString());
+ xml.closeElement(this);
+ return xml;
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java
new file mode 100644
index 0000000000..dcd47c65a0
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/MetadataExtension.java
@@ -0,0 +1,165 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar.element;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.util.XmlStringBuilder;
+import org.jivesoftware.smackx.avatar.MetadataInfo;
+import org.jivesoftware.smackx.avatar.MetadataPointer;
+import org.jivesoftware.smackx.avatar.UserAvatarManager;
+
+/**
+ * Metadata extension element class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataExtension implements ExtensionElement {
+
+ public static final String ELEMENT = "metadata";
+ public static final String NAMESPACE = UserAvatarManager.METADATA_NAMESPACE;
+
+ private final List infos;
+ private final List pointers;
+
+ /**
+ * Metadata Extension constructor.
+ *
+ * @param infos
+ */
+ public MetadataExtension(List infos) {
+ this(infos, null);
+ }
+
+ /**
+ * Metadata Extension constructor.
+ *
+ * @param infos
+ * @param pointers
+ */
+ public MetadataExtension(List infos, List pointers) {
+ this.infos = infos;
+ this.pointers = pointers;
+ }
+
+ /**
+ * Get the info elements list.
+ *
+ * @return the info elements list
+ */
+ public List getInfos() {
+ return Collections.unmodifiableList(infos);
+ }
+
+ /**
+ * Get the pointer elements list.
+ *
+ * @return the pointer elements list
+ */
+ public List getPointers() {
+ return (pointers == null) ? null : Collections.unmodifiableList(pointers);
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public CharSequence toXML() {
+ XmlStringBuilder xml = new XmlStringBuilder(this);
+ parseInfos(xml);
+ parsePointers(xml);
+ closeElement(xml);
+ return xml;
+ }
+
+ private void parseInfos(XmlStringBuilder xml) {
+ if (infos != null) {
+ xml.rightAngleBracket();
+
+ for (MetadataInfo info : infos) {
+ xml.halfOpenElement("info");
+ xml.attribute("id", info.getId());
+ xml.attribute("bytes", String.valueOf(info.getBytes()));
+ xml.attribute("type", info.getType());
+ xml.optAttribute("url", info.getUrl());
+
+ if (info.getHeight() > 0) {
+ xml.attribute("height", info.getHeight());
+ }
+
+ if (info.getWidth() > 0) {
+ xml.attribute("width", info.getWidth());
+ }
+
+ xml.closeEmptyElement();
+ }
+ }
+ }
+
+ private void parsePointers(XmlStringBuilder xml) {
+ if (pointers != null) {
+
+ for (MetadataPointer pointer : pointers) {
+ xml.openElement("pointer");
+ xml.halfOpenElement("x");
+
+ String namespace = pointer.getNamespace();
+ if (namespace != null) {
+ xml.xmlnsAttribute(namespace);
+ }
+
+ xml.rightAngleBracket();
+
+ HashMap fields = pointer.getFields();
+ if (fields != null) {
+ Iterator> it = fields.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry pair = it.next();
+ xml.escapedElement(pair.getKey(), String.valueOf(pair.getValue()));
+ }
+ }
+
+ xml.closeElement("x");
+ xml.closeElement("pointer");
+ }
+
+ }
+ }
+
+ private void closeElement(XmlStringBuilder xml) {
+ if (infos != null || pointers != null) {
+ xml.closeElement(this);
+ } else {
+ xml.closeEmptyElement();
+ }
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java
new file mode 100644
index 0000000000..f6e4ab263e
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/element/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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.
+ */
+/**
+ * User Avatar elements.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar.element;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java
new file mode 100644
index 0000000000..b61f2bf409
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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.
+ */
+/**
+ * Classes and interfaces of User Avatar.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java
new file mode 100644
index 0000000000..1a257ba8c1
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/DataProvider.java
@@ -0,0 +1,39 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar.provider;
+
+import org.jivesoftware.smack.provider.ExtensionElementProvider;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * User Avatar data provider class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class DataProvider extends ExtensionElementProvider {
+
+ @Override
+ public DataExtension parse(XmlPullParser parser, int initialDepth) throws Exception {
+ byte[] data = Base64.decode(parser.nextText());
+ return new DataExtension(data);
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java
new file mode 100644
index 0000000000..d987cd2b9d
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/MetadataProvider.java
@@ -0,0 +1,139 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar.provider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.jivesoftware.smack.provider.ExtensionElementProvider;
+import org.jivesoftware.smackx.avatar.MetadataInfo;
+import org.jivesoftware.smackx.avatar.MetadataPointer;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * User Avatar metedata provider class.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+public class MetadataProvider extends ExtensionElementProvider {
+
+ @Override
+ public MetadataExtension parse(XmlPullParser parser, int initialDepth) throws Exception {
+ List metadataInfos = null;
+ List pointers = null;
+
+ outerloop: while (true) {
+ int eventType = parser.next();
+
+ if (eventType == XmlPullParser.START_TAG) {
+
+ if (parser.getName().equals("info")) {
+ if (metadataInfos == null) {
+ metadataInfos = new ArrayList<>();
+ }
+
+ MetadataInfo info = parseInfo(parser);
+ if (info.getId() != null) {
+ metadataInfos.add(info);
+ }
+ }
+
+ if (parser.getName().equals("pointer")) {
+ if (pointers == null) {
+ pointers = new ArrayList<>();
+ }
+
+ pointers.add(parsePointer(parser));
+ }
+
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getDepth() == initialDepth) {
+ break outerloop;
+ }
+ }
+ }
+
+ return new MetadataExtension(metadataInfos, pointers);
+ }
+
+ private MetadataInfo parseInfo(XmlPullParser parser) {
+ String id = null;
+ String url = null;
+ long bytes = 0;
+ String type = null;
+ int pixelsHeight = 0;
+ int pixelsWidth = 0;
+
+ id = parser.getAttributeValue("", "id");
+ url = parser.getAttributeValue("", "url");
+ type = parser.getAttributeValue("", "type");
+
+ String bytesString = parser.getAttributeValue("", "bytes");
+ if (bytesString != null) {
+ bytes = Long.parseLong(bytesString);
+ }
+
+ String widthString = parser.getAttributeValue("", "width");
+ if (widthString != null) {
+ pixelsWidth = Integer.parseInt(widthString);
+ }
+
+ String heightString = parser.getAttributeValue("", "height");
+ if (heightString != null) {
+ pixelsHeight = Integer.parseInt(heightString);
+ }
+
+ return new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
+ }
+
+ private MetadataPointer parsePointer(XmlPullParser parser) throws XmlPullParserException, IOException {
+ int pointerDepth = parser.getDepth();
+ String namespace = null;
+ HashMap fields = null;
+
+ outerloop2: while (true) {
+ int eventType2 = parser.next();
+
+ if (eventType2 == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("x")) {
+ namespace = parser.getNamespace();
+ } else {
+ if (fields == null) {
+ fields = new HashMap<>();
+ }
+
+ String name = parser.getName();
+ Object value = parser.nextText();
+ fields.put(name, value);
+ }
+ } else if (eventType2 == XmlPullParser.END_TAG) {
+ if (parser.getDepth() == pointerDepth) {
+ break outerloop2;
+ }
+ }
+ }
+
+ return new MetadataPointer(namespace, fields);
+ }
+
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java
new file mode 100644
index 0000000000..6529523cb1
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/avatar/provider/package-info.java
@@ -0,0 +1,24 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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.
+ */
+/**
+ * User Avatar providers.
+ *
+ * @author Fernando Ramirez
+ * @see XEP-0084: User
+ * Avatar
+ */
+package org.jivesoftware.smackx.avatar.provider;
diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
index 41705f0ca0..ce95f05276 100644
--- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
+++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers
@@ -544,4 +544,16 @@
org.jivesoftware.smackx.bob.provider.BoBIQProvider
+
+
+ data
+ urn:xmpp:avatar:data
+ org.jivesoftware.smackx.avatar.provider.DataProvider
+
+
+ metadata
+ urn:xmpp:avatar:metadata
+ org.jivesoftware.smackx.avatar.provider.MetadataProvider
+
+
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java
new file mode 100644
index 0000000000..7db73e7c9c
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/DataExtensionTest.java
@@ -0,0 +1,47 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar;
+
+import org.jivesoftware.smack.test.util.SmackTestSuite;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smack.util.stringencoder.Base64;
+import org.jivesoftware.smackx.avatar.element.DataExtension;
+import org.jivesoftware.smackx.avatar.provider.DataProvider;
+import org.junit.Assert;
+import org.junit.Test;
+import org.xmlpull.v1.XmlPullParser;
+
+public class DataExtensionTest extends SmackTestSuite {
+
+ // @formatter:off
+ String dataExtensionExample = ""
+ + "qANQR1DBwU4DX7jmYZnnfe32"
+ + "";
+ // @formatter:on
+
+ @Test
+ public void checkDataExtensionParse() throws Exception {
+ byte[] data = Base64.decode("qANQR1DBwU4DX7jmYZnnfe32");
+ DataExtension dataExtension = new DataExtension(data);
+ Assert.assertEquals(dataExtensionExample, dataExtension.toXML().toString());
+
+ XmlPullParser parser = PacketParserUtils.getParserFor(dataExtensionExample);
+ DataExtension dataExtensionFromProvider = new DataProvider().parse(parser);
+ Assert.assertEquals(Base64.encodeToString(data), Base64.encodeToString(dataExtensionFromProvider.getData()));
+ }
+
+}
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java
new file mode 100644
index 0000000000..cfeaeb4a82
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/avatar/MetadataExtensionTest.java
@@ -0,0 +1,213 @@
+/**
+ *
+ * Copyright 2017 Fernando Ramirez
+ *
+ * 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 org.jivesoftware.smackx.avatar;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.avatar.element.MetadataExtension;
+import org.jivesoftware.smackx.avatar.provider.MetadataProvider;
+import org.junit.Assert;
+import org.junit.Test;
+import org.xmlpull.v1.XmlPullParser;
+
+public class MetadataExtensionTest {
+
+ // @formatter:off
+ String metadataExtensionExample = ""
+ + ""
+ + "";
+ // @formatter:on
+
+ // @formatter:off
+ String emptyMetadataExtensionExample = "";
+ // @formatter:on
+
+ // @formatter:off
+ String metadataWithSeveralInfos = ""
+ + ""
+ + ""
+ + ""
+ + "";
+ // @formatter:on
+
+ // @formatter:off
+ String metadataWithInfoAndPointers = ""
+ + ""
+ + ""
+ + ""
+ + "Ancapistan"
+ + "Kropotkin"
+ + ""
+ + ""
+ + ""
+ + ""
+ + "hard"
+ + "2"
+ + ""
+ + ""
+ + "";
+ // @formatter:on
+
+ @Test
+ public void checkMetadataExtensionParse() throws Exception {
+ String id = "357a8123a30844a3aa99861b6349264ba67a5694";
+ String url = "http://avatars.example.org/happy.gif";
+ long bytes = 23456;
+ String type = "image/gif";
+ int pixelsHeight = 64;
+ int pixelsWidth = 128;
+
+ MetadataInfo info = new MetadataInfo(id, url, bytes, type, pixelsHeight, pixelsWidth);
+ List infos = new ArrayList<>();
+ infos.add(info);
+
+ MetadataExtension metadataExtension = new MetadataExtension(infos);
+ Assert.assertEquals(metadataExtensionExample, metadataExtension.toXML().toString());
+
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataExtensionExample);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ Assert.assertEquals(id, metadataExtensionFromProvider.getInfos().get(0).getId());
+ Assert.assertEquals(url, metadataExtensionFromProvider.getInfos().get(0).getUrl());
+ Assert.assertEquals(bytes, metadataExtensionFromProvider.getInfos().get(0).getBytes());
+ Assert.assertEquals(type, metadataExtensionFromProvider.getInfos().get(0).getType());
+ Assert.assertEquals(pixelsHeight, metadataExtensionFromProvider.getInfos().get(0).getHeight());
+ Assert.assertEquals(pixelsWidth, metadataExtensionFromProvider.getInfos().get(0).getWidth());
+ }
+
+ @Test
+ public void checkEmptyMetadataExtensionParse() throws Exception {
+ MetadataExtension metadataExtension = new MetadataExtension(null);
+ Assert.assertEquals(emptyMetadataExtensionExample, metadataExtension.toXML().toString());
+ }
+
+ @Test
+ public void checkSeveralInfosInMetadataExtension() throws Exception {
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithSeveralInfos);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ MetadataInfo info1 = metadataExtensionFromProvider.getInfos().get(0);
+ MetadataInfo info2 = metadataExtensionFromProvider.getInfos().get(1);
+ MetadataInfo info3 = metadataExtensionFromProvider.getInfos().get(2);
+
+ Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info1.getId());
+ Assert.assertNull(info1.getUrl());
+ Assert.assertEquals(12345, info1.getBytes());
+ Assert.assertEquals("image/png", info1.getType());
+ Assert.assertEquals(64, info1.getHeight());
+ Assert.assertEquals(64, info1.getWidth());
+
+ Assert.assertEquals("e279f80c38f99c1e7e53e262b440993b2f7eea57", info2.getId());
+ Assert.assertEquals("http://avatars.example.org/happy.png", info2.getUrl());
+ Assert.assertEquals(12345, info2.getBytes());
+ Assert.assertEquals("image/png", info2.getType());
+ Assert.assertEquals(64, info2.getHeight());
+ Assert.assertEquals(128, info2.getWidth());
+
+ Assert.assertEquals("357a8123a30844a3aa99861b6349264ba67a5694", info3.getId());
+ Assert.assertEquals("http://avatars.example.org/happy.gif", info3.getUrl());
+ Assert.assertEquals(23456, info3.getBytes());
+ Assert.assertEquals("image/gif", info3.getType());
+ Assert.assertEquals(64, info3.getHeight());
+ Assert.assertEquals(64, info3.getWidth());
+ }
+
+ @Test
+ public void checkInfosAndPointersParse() throws Exception {
+ XmlPullParser parser = PacketParserUtils.getParserFor(metadataWithInfoAndPointers);
+ MetadataExtension metadataExtensionFromProvider = new MetadataProvider().parse(parser);
+
+ MetadataInfo info = metadataExtensionFromProvider.getInfos().get(0);
+ Assert.assertEquals("111f4b3c50d7b0df729d299bc6f8e9ef9066971f", info.getId());
+ Assert.assertNull(info.getUrl());
+ Assert.assertEquals(12345, info.getBytes());
+ Assert.assertEquals("image/png", info.getType());
+ Assert.assertEquals(64, info.getHeight());
+ Assert.assertEquals(64, info.getWidth());
+
+ MetadataPointer pointer1 = metadataExtensionFromProvider.getPointers().get(0);
+ HashMap fields1 = pointer1.getFields();
+ Assert.assertEquals("http://example.com/virtualworlds", pointer1.getNamespace());
+ Assert.assertEquals("Ancapistan", fields1.get("game"));
+ Assert.assertEquals("Kropotkin", fields1.get("character"));
+
+ MetadataPointer pointer2 = metadataExtensionFromProvider.getPointers().get(1);
+ HashMap fields2 = pointer2.getFields();
+ Assert.assertEquals("http://sample.com/game", pointer2.getNamespace());
+ Assert.assertEquals("hard", fields2.get("level"));
+ Assert.assertEquals("2", fields2.get("players"));
+ }
+
+ @Test
+ public void createMetadataExtensionWithInfoAndPointer() {
+ String id = "111f4b3c50d7b0df729d299bc6f8e9ef9066971f";
+ long bytes = 12345;
+ String type = "image/png";
+ int pixelsHeight = 64;
+ int pixelsWidth = 64;
+ MetadataInfo info = new MetadataInfo(id, null, bytes, type, pixelsHeight, pixelsWidth);
+
+ HashMap fields1 = new HashMap<>();
+ fields1.put("game", "Ancapistan");
+ fields1.put("character", "Kropotkin");
+ MetadataPointer pointer1 = new MetadataPointer("http://example.com/virtualworlds", fields1);
+
+ HashMap fields2 = new HashMap<>();
+ fields2.put("level", "hard");
+ fields2.put("players", 2);
+ MetadataPointer pointer2 = new MetadataPointer("http://sample.com/game", fields2);
+
+ List infos = new ArrayList<>();
+ infos.add(info);
+
+ List pointers = new ArrayList<>();
+ pointers.add(pointer1);
+ pointers.add(pointer2);
+
+ MetadataExtension metadataExtension = new MetadataExtension(infos, pointers);
+ Assert.assertEquals(metadataWithInfoAndPointers, metadataExtension.toXML().toString());
+ }
+
+}