diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md
index b5c279f7d..5043f940d 100644
--- a/documentation/extensions/index.md
+++ b/documentation/extensions/index.md
@@ -56,6 +56,7 @@ Smack Extensions and currently supported XEPs of smack-extensions
| [SI File Transfer](filetransfer.md) | [XEP-0096](https://xmpp.org/extensions/xep-0096.html) | n/a | Transfer files between two users over XMPP. |
| User Mood | [XEP-0107](https://xmpp.org/extensions/xep-0107.html) | 1.2.1 | Communicate the users current mood. |
| [Entity Capabilities](caps.md) | [XEP-0115](https://xmpp.org/extensions/xep-0115.html) | n/a | Broadcasting and dynamic discovery of entity capabilities. |
+| User Tune | [XEP-0118](https://xmpp.org/extensions/xep-0118.html) | n/a | Defines a payload format for communicating information about music to which a user is listening. |
| Data Forms Validation | [XEP-0122](https://xmpp.org/extensions/xep-0122.html) | n/a | Enables an application to specify additional validation guidelines . |
| Service Administration | [XEP-0133](https://xmpp.org/extensions/xep-0133.html) | n/a | Recommended best practices for service-level administration of servers and components using Ad-Hoc Commands. |
| Stream Compression | [XEP-0138](https://xmpp.org/extensions/xep-0138.html) | n/a | Support for optional compression of the XMPP stream.
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java
new file mode 100644
index 000000000..c513579fc
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneListener.java
@@ -0,0 +1,27 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+
+import org.jxmpp.jid.BareJid;
+
+public interface UserTuneListener {
+
+ void onUserTuneUpdated(BareJid jid, Message message, UserTuneElement userTuneElement);
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java
new file mode 100644
index 000000000..9b158627e
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/UserTuneManager.java
@@ -0,0 +1,143 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.AsyncButOrdered;
+import org.jivesoftware.smack.Manager;
+import org.jivesoftware.smack.SmackException.NoResponseException;
+import org.jivesoftware.smack.SmackException.NotConnectedException;
+import org.jivesoftware.smack.SmackException.NotLoggedInException;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException.XMPPErrorException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.pep.PepListener;
+import org.jivesoftware.smackx.pep.PepManager;
+import org.jivesoftware.smackx.pubsub.EventElement;
+import org.jivesoftware.smackx.pubsub.ItemsExtension;
+import org.jivesoftware.smackx.pubsub.PayloadItem;
+import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+
+import org.jxmpp.jid.BareJid;
+import org.jxmpp.jid.EntityBareJid;
+
+/**
+ * Entry point for Smacks API for XEP-0118: User Tune.
+ *
+ * To publish a UserTune, please use {@link #publishUserTune(UserTuneElement)} method. This will publish the node.
+ *
+ * To stop publishing a UserTune, please use {@link #clearUserTune()} method. This will send a disabling publish signal.
+ *
+ * To add a UserTune listener in order to remain updated with other users UserTune, use {@link #addUserTuneListener(UserTuneListener)} method.
+ *
+ * To link a UserTuneElement with {@link Message}, use 'message.addExtension(userTuneElement)'.
+ *
+ * An example to illustrate is provided inside UserTuneElementTest inside the test package.
+ *
+ * @see
+ * XEP-0118: User Tune
+ */
+public final class UserTuneManager extends Manager {
+
+ public static final String USERTUNE_NODE = "http://jabber.org/protocol/tune";
+ public static final String USERTUNE_NOTIFY = USERTUNE_NODE + "+notify";
+
+ private static final Map INSTANCES = new WeakHashMap<>();
+
+ private final Set userTuneListeners = new HashSet<>();
+ private final AsyncButOrdered asyncButOrdered = new AsyncButOrdered<>();
+ private final PepManager pepManager;
+ private boolean ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT = true;
+
+ public static synchronized UserTuneManager getInstanceFor(XMPPConnection connection) throws NotLoggedInException {
+ UserTuneManager manager = INSTANCES.get(connection);
+ if (manager == null) {
+ manager = new UserTuneManager(connection);
+ INSTANCES.put(connection, manager);
+ }
+ return manager;
+ }
+
+ private UserTuneManager(XMPPConnection connection) throws NotLoggedInException {
+ super(connection);
+ pepManager = PepManager.getInstanceFor(connection);
+ pepManager.addPepListener(new PepListener() {
+ @Override
+ public void eventReceived(EntityBareJid from, EventElement event, Message message) {
+ if (!USERTUNE_NODE.equals(event.getEvent().getNode())) {
+ return;
+ }
+
+ final BareJid contact = from.asBareJid();
+ asyncButOrdered.performAsyncButOrdered(contact, () -> {
+ ItemsExtension items = (ItemsExtension) event.getExtensions().get(0);
+ PayloadItem> payload = (PayloadItem) items.getItems().get(0);
+ UserTuneElement tune = (UserTuneElement) payload.getPayload();
+
+ for (UserTuneListener listener : userTuneListeners) {
+ synchronized (userTuneListeners) {
+ listener.onUserTuneUpdated(contact, message, tune);
+ }
+ }
+ });
+ }
+ });
+ if (ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT) {
+ enableUserTuneNotifications();
+ }
+ }
+
+ public void setUserTuneNotificationsEnabledByDefault(boolean bool) {
+ ENABLE_USER_TUNE_NOTIFICATIONS_BY_DEFAULT = bool;
+ }
+
+ public void enableUserTuneNotifications() {
+ ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(USERTUNE_NOTIFY);
+ }
+
+ @SuppressWarnings("unused")
+ private void disableUserTuneNotifications() {
+ ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(USERTUNE_NOTIFY);
+ }
+
+ public void clearUserTune() throws NotLoggedInException, NotALeafNodeException, NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException {
+ publishUserTune(UserTuneElement.EMPTY_USER_TUNE);
+ }
+
+ public void publishUserTune(UserTuneElement userTuneElement) throws NotLoggedInException, NotALeafNodeException, NoResponseException, NotConnectedException, XMPPErrorException, InterruptedException {
+ // TODO: To prevent a large number of updates when a user is skipping through tracks, an implementation SHOULD wait several seconds before publishing new tune information.
+ pepManager.publish(USERTUNE_NODE, new PayloadItem<>(userTuneElement));
+ }
+
+ public void addUserTuneListener(UserTuneListener listener) {
+ synchronized (userTuneListeners) {
+ userTuneListeners.add(listener);
+ }
+ }
+
+ public void removeUserTuneListener(UserTuneListener listener) {
+ synchronized (userTuneListeners) {
+ userTuneListeners.remove(listener);
+ }
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/UserTuneElement.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/UserTuneElement.java
new file mode 100644
index 000000000..6cd1ca271
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/UserTuneElement.java
@@ -0,0 +1,267 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune.element;
+
+import java.net.URI;
+
+import org.jivesoftware.smack.datatypes.UInt16;
+import org.jivesoftware.smack.packet.ExtensionElement;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.util.EqualsUtil;
+import org.jivesoftware.smack.util.HashCode;
+import org.jivesoftware.smack.util.XmlStringBuilder;
+
+/**
+ * {@link ExtensionElement} that contains the UserTune.
+ * Instance of UserTuneElement can be created using {@link Builder#build()}
+ * method.
+ */
+public final class UserTuneElement implements ExtensionElement {
+
+ public static final String NAMESPACE = "http://jabber.org/protocol/tune";
+ public static final String ELEMENT = "tune";
+
+ private final String artist;
+ private final UInt16 length;
+ private final Integer rating;
+ private final String source;
+ private final String title;
+ private final String track;
+ private final URI uri;
+
+ public static final UserTuneElement EMPTY_USER_TUNE = null;
+
+ private UserTuneElement(Builder builder) {
+ this.artist = builder.artist;
+ this.length = builder.length;
+ this.rating = builder.rating;
+ this.source = builder.source;
+ this.title = builder.title;
+ this.track = builder.track;
+ this.uri = builder.uri;
+ }
+
+ @Override
+ public String getNamespace() {
+ return NAMESPACE;
+ }
+
+ @Override
+ public String getElementName() {
+ return ELEMENT;
+ }
+
+ public String getArtist() {
+ return artist;
+ }
+
+ public UInt16 getLength() {
+ return length;
+ }
+
+ public Integer getRating() {
+ return rating;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getTrack() {
+ return track;
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ @Override
+ public XmlStringBuilder toXML(XmlEnvironment xmlEnvironment) {
+ XmlStringBuilder xml = new XmlStringBuilder(this);
+ if (isEmptyUserTune()) {
+ return xml.closeEmptyElement();
+ }
+ xml.rightAngleBracket();
+ xml.optElement("artist", artist);
+ xml.optElement("length", length);
+ xml.optElement("rating", rating);
+ xml.optElement("source", source);
+ xml.optElement("title", title);
+ xml.optElement("track", track);
+ xml.optElement("uri", uri);
+ return xml.closeElement(getElementName());
+ }
+
+ private boolean isEmptyUserTune() {
+ return this.equals(EMPTY_USER_TUNE);
+ }
+
+ public static boolean hasUserTuneElement(Message message) {
+ return message.hasExtension(ELEMENT, NAMESPACE);
+ }
+
+ public static UserTuneElement from(Message message) {
+ return message.getExtension(ELEMENT, NAMESPACE);
+ }
+
+ @Override
+ public int hashCode() {
+ return HashCode.builder()
+ .append(artist)
+ .append(length)
+ .append(rating)
+ .append(source)
+ .append(title)
+ .append(track)
+ .append(uri).build();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return EqualsUtil
+ .equals(this, obj, (equalsBuilder, otherTune) -> equalsBuilder
+ .append(artist, otherTune.artist)
+ .append(length, otherTune.length)
+ .append(rating, otherTune.rating)
+ .append(source, otherTune.source)
+ .append(title, otherTune.source)
+ .append(title, otherTune.title)
+ .append(track, otherTune.track)
+ .append(uri, otherTune.uri));
+ }
+
+ /**
+ * Returns a new instance of {@link Builder}.
+ * @return Builder
+ */
+ public static Builder getBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * This class defines a Builder class for {@link UserTuneElement}.
+ * {@link UserTuneElement} instance can be obtained using the {@link #build()} method as follows.
+ * UserTuneElement.Builder builder = new UserTuneElement.Builder();
+ * builder.setSource("Yessongs"); builder.setTitle("Heart of the Sunrise");
+ * UserTuneElement userTuneElement = builder.build();
+ * Values such as title, source, artist, length, source, track and uri can be set using their respective setters through {@link Builder}
+ */
+ public static final class Builder {
+ private String artist;
+ private UInt16 length;
+ private Integer rating;
+ private String source;
+ private String title;
+ private String track;
+ private URI uri;
+
+ private Builder() {
+ }
+
+ /**
+ * Artist is an optional element in UserTuneElement.
+ * @param artist.
+ * @return builder.
+ */
+ public Builder setArtist(String artist) {
+ this.artist = artist;
+ return this;
+ }
+
+ /**
+ * Length is an optional element in UserTuneElement.
+ * @param length.
+ * @return builder.
+ */
+ public Builder setLength(int length) {
+ return setLength(UInt16.from(length));
+ }
+
+ /**
+ * Length is an optional element in UserTuneElement.
+ * @param length.
+ * @return builder.
+ */
+ public Builder setLength(UInt16 length) {
+ this.length = length;
+ return this;
+ }
+
+ /**
+ * Rating is an optional element in UserTuneElement.
+ * @param rating.
+ * @return builder.
+ */
+ public Builder setRating(int rating) {
+ this.rating = rating;
+ return this;
+ }
+
+ /**
+ * Source is an optional element in UserTuneElement.
+ * @param source.
+ * @return builder.
+ */
+ public Builder setSource(String source) {
+ this.source = source;
+ return this;
+ }
+
+ /**
+ * Title is an optional element in UserTuneElement.
+ * @param title.
+ * @return builder.
+ */
+ public Builder setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Track is an optional element in UserTuneElement.
+ * @param track.
+ * @return builder.
+ */
+ public Builder setTrack(String track) {
+ this.track = track;
+ return this;
+ }
+
+ /**
+ * URI is an optional element in UserTuneElement.
+ * @param uri.
+ * @return builder.
+ */
+ public Builder setUri(URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ /**
+ * This method is called to build a UserTuneElement.
+ * @return UserTuneElement.
+ */
+ public UserTuneElement build() {
+ return new UserTuneElement(this);
+ }
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/package-info.java
new file mode 100644
index 000000000..06849e26a
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/element/package-info.java
@@ -0,0 +1,21 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar
+ *
+ * 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.
+ */
+
+/**
+ * Smack's API for XEP-0118: User Tune.
+ */
+package org.jivesoftware.smackx.usertune.element;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/package-info.java
new file mode 100644
index 000000000..182ee237e
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/package-info.java
@@ -0,0 +1,21 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar
+ *
+ * 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.
+ */
+
+/**
+ * Smack's API for XEP-0118: User Tune.
+ */
+package org.jivesoftware.smackx.usertune;
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/UserTuneProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/UserTuneProvider.java
new file mode 100644
index 000000000..5b1c423ef
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/UserTuneProvider.java
@@ -0,0 +1,89 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune.provider;
+
+import java.io.IOException;
+import java.net.URI;
+
+import org.jivesoftware.smack.packet.XmlEnvironment;
+import org.jivesoftware.smack.parsing.SmackParsingException;
+import org.jivesoftware.smack.provider.ExtensionElementProvider;
+import org.jivesoftware.smack.util.ParserUtils;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+
+/**
+ * This is the Provider Class for {@link UserTuneElement}.
+ */
+public class UserTuneProvider extends ExtensionElementProvider {
+
+ public static final UserTuneProvider INSTANCE = new UserTuneProvider();
+
+ @Override
+ public UserTuneElement parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment)
+ throws XmlPullParserException, IOException, SmackParsingException {
+
+ UserTuneElement.Builder builder = UserTuneElement.getBuilder();
+ XmlPullParser.TagEvent tag = parser.nextTag();
+ outerloop: while (true) {
+ switch (tag) {
+ case START_ELEMENT:
+ String name = parser.getName();
+ String namespace = parser.getNamespace();
+ if (!UserTuneElement.NAMESPACE.equals(namespace)) {
+ continue outerloop;
+ }
+ while (tag == XmlPullParser.TagEvent.START_ELEMENT) {
+ switch (name) {
+ case "artist":
+ builder.setArtist(parser.nextText());
+ break;
+ case "length":
+ builder.setLength(ParserUtils.getIntegerFromNextText(parser));
+ break;
+ case "rating":
+ builder.setRating(ParserUtils.getIntegerFromNextText(parser));
+ break;
+ case "source":
+ builder.setSource(parser.nextText());
+ break;
+ case "title":
+ builder.setTitle(parser.nextText());
+ break;
+ case "track":
+ builder.setTrack(parser.nextText());
+ break;
+ case "uri":
+ URI uri = ParserUtils.getUriFromNextText(parser);
+ builder.setUri(uri);
+ break;
+ }
+ tag = parser.nextTag();
+ name = parser.getName();
+ }
+ break;
+ case END_ELEMENT:
+ if (parser.getDepth() == initialDepth) {
+ break outerloop;
+ }
+ break;
+ }
+ }
+ return builder.build();
+ }
+}
diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/package-info.java
new file mode 100644
index 000000000..09bcf2adc
--- /dev/null
+++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/usertune/provider/package-info.java
@@ -0,0 +1,21 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar
+ *
+ * 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.
+ */
+
+/**
+ * Smack's API for XEP-0118: User Tune.
+ */
+package org.jivesoftware.smackx.usertune.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 a34d5435d..c673a9a2f 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
@@ -589,4 +589,11 @@
org.jivesoftware.smackx.mood.provider.MoodProvider
+
+
+ tune
+ http://jabber.org/protocol/tune
+ org.jivesoftware.smackx.usertune.provider.UserTuneProvider
+
+
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java
new file mode 100644
index 000000000..9aea03774
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneElementTest.java
@@ -0,0 +1,74 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;
+
+import static org.jivesoftware.smack.test.util.XmlUnitUtils.assertXmlSimilar;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.jivesoftware.smack.parsing.SmackParsingException;
+import org.jivesoftware.smack.test.util.SmackTestSuite;
+import org.jivesoftware.smack.test.util.SmackTestUtil;
+import org.jivesoftware.smack.test.util.SmackTestUtil.XmlPullParserKind;
+import org.jivesoftware.smack.xml.XmlPullParser;
+import org.jivesoftware.smack.xml.XmlPullParserException;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+import org.jivesoftware.smackx.usertune.provider.UserTuneProvider;
+
+import org.junit.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+public class UserTuneElementTest extends SmackTestSuite {
+
+ private final String xml = "" +
+ "Yes" +
+ "686" +
+ "8" +
+ "" +
+ "Heart of the Sunrise" +
+ "" +
+ "http://www.yesworld.com/lyrics/Fragile.html#9" +
+ "";
+
+ @Test
+ public void toXmlTest() throws IOException, XmlPullParserException, SmackParsingException, URISyntaxException {
+
+ URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#9");
+
+ UserTuneElement.Builder builder = UserTuneElement.getBuilder();
+ UserTuneElement userTuneElement = builder.setArtist("Yes")
+ .setLength(686)
+ .setRating(8)
+ .setSource("Yessongs")
+ .setTitle("Heart of the Sunrise")
+ .setTrack("3")
+ .setUri(uri)
+ .build();
+ assertXmlSimilar(xml, userTuneElement.toXML().toString());
+ }
+
+ @ParameterizedTest
+ @EnumSource(value = SmackTestUtil.XmlPullParserKind.class)
+ public void userTuneElementProviderTest(XmlPullParserKind parserKind) throws XmlPullParserException, IOException, SmackParsingException {
+ XmlPullParser parser = SmackTestUtil.getParserFor(xml, parserKind);
+ UserTuneElement parsed = UserTuneProvider.INSTANCE.parse(parser);
+ assertXmlSimilar(xml, parsed.toXML().toString());
+ }
+}
diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneManagerTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneManagerTest.java
new file mode 100644
index 000000000..bb21fae84
--- /dev/null
+++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/usertune/UserTuneManagerTest.java
@@ -0,0 +1,58 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.test.util.SmackTestSuite;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+
+import org.junit.Test;
+
+public class UserTuneManagerTest extends SmackTestSuite{
+
+ @Test
+ public void addMessage() throws URISyntaxException {
+
+ UserTuneElement.Builder builder = UserTuneElement.getBuilder();
+ builder.setArtist("Yes");
+ builder.setLength(686);
+ builder.setRating(8);
+ builder.setSource("Yessongs");
+ builder.setTitle("Heart of the Sunrise");
+ builder.setTrack("3");
+ URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#9");
+ builder.setUri(uri);
+ UserTuneElement userTuneElement = builder.build();
+
+ Message message = new Message();
+ message.addExtension(userTuneElement);
+
+ assertTrue(message.hasExtension(UserTuneElement.ELEMENT, UserTuneElement.NAMESPACE));
+ assertTrue(UserTuneElement.hasUserTuneElement(message));
+
+ UserTuneElement element = UserTuneElement.from(message);
+ assertNotNull(element);
+ assertEquals(userTuneElement, element);
+ }
+}
diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java
new file mode 100644
index 000000000..86383a819
--- /dev/null
+++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/UserTuneIntegrationTest.java
@@ -0,0 +1,89 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;
+
+import java.net.URI;
+
+import org.jivesoftware.smack.SmackException;
+import org.jivesoftware.smack.SmackException.NotLoggedInException;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.usertune.element.UserTuneElement;
+
+import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
+import org.igniterealtime.smack.inttest.SmackIntegrationTest;
+import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
+import org.igniterealtime.smack.inttest.util.IntegrationTestRosterUtil;
+import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
+import org.junit.AfterClass;
+
+import org.jxmpp.jid.BareJid;
+
+public class UserTuneIntegrationTest extends AbstractSmackIntegrationTest {
+
+ private final UserTuneManager utm1;
+ private final UserTuneManager utm2;
+
+ public UserTuneIntegrationTest(SmackIntegrationTestEnvironment> environment) throws NotLoggedInException {
+ super(environment);
+ utm1 = UserTuneManager.getInstanceFor(conOne);
+ utm2 = UserTuneManager.getInstanceFor(conTwo);
+ }
+
+ @SmackIntegrationTest
+ public void test() throws Exception {
+ URI uri = new URI("http://www.yesworld.com/lyrics/Fragile.html#9");
+ UserTuneElement.Builder builder = UserTuneElement.getBuilder();
+ UserTuneElement userTuneElement1 = builder.setArtist("Yes")
+ .setLength(686)
+ .setRating(8)
+ .setSource("Yessongs")
+ .setTitle("Heart of the Sunrise")
+ .setTrack("3")
+ .setUri(uri)
+ .build();
+
+ IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout);
+
+ final SimpleResultSyncPoint userTuneReceived = new SimpleResultSyncPoint();
+
+ final UserTuneListener userTuneListener = new UserTuneListener() {
+ @Override
+ public void onUserTuneUpdated(BareJid jid, Message message, UserTuneElement userTuneElement) {
+ if (userTuneElement.equals(userTuneElement1)) {
+ userTuneReceived.signal();
+ }
+ }
+ };
+
+ utm2.addUserTuneListener(userTuneListener);
+
+ try {
+ utm1.publishUserTune(userTuneElement1);
+ userTuneReceived.waitForResult(timeout);
+ } finally {
+ utm2.removeUserTuneListener(userTuneListener);
+ }
+ }
+
+ @AfterClass
+ public void unsubscribe()
+ throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
+ SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
+ IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo);
+ }
+}
diff --git a/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/package-info.java b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/package-info.java
new file mode 100644
index 000000000..b4fb83a44
--- /dev/null
+++ b/smack-integration-test/src/main/java/org/jivesoftware/smackx/usertune/package-info.java
@@ -0,0 +1,17 @@
+/**
+ *
+ * Copyright 2019 Aditya Borikar.
+ *
+ * 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.usertune;