1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-11-25 21:42:07 +01:00

Make use of pep instead of pubsub in GeoLocationManager

This mini assignment kicked-off with replacing `pubsub` with `pep`,
but later transformed into something more.

The alterations and additions in this commit:
 a) GeoLocation.
    1) Add Documentation.
    2) Add `EMPTY_GEO_LOCATION` to be used while
       `stopPublishingGeoLocation()` is called.
 b) Add GeoLocation IntegrationTest.
 c) Add GeoLocation Listener.
 d) GeoLocationManager.
    1) Add Documentation.
    2) Replace `pubsub` with `pep`.
    3) Add methods to add-and-remove GeoLocationListeners.
    4) Enable GeoLocation by default.
 e) Add `package.info` for GeoLocation Integration Test.
This commit is contained in:
adiaholic 2019-10-20 16:30:38 +05:30
parent 10aee6c787
commit 340e186cf6
5 changed files with 423 additions and 16 deletions

View file

@ -0,0 +1,26 @@
/**
*
* Copyright 2020 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.geoloc;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.geoloc.packet.GeoLocation;
import org.jxmpp.jid.BareJid;
public interface GeoLocationListener {
void onGeoLocationUpdated(BareJid jid, GeoLocation geoLocation, Message message);
}

View file

@ -16,9 +16,13 @@
*/ */
package org.jivesoftware.smackx.geoloc; package org.jivesoftware.smackx.geoloc;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.jivesoftware.smack.AsyncButOrdered;
import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NoResponseException;
@ -26,22 +30,53 @@ import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.geoloc.packet.GeoLocation; import org.jivesoftware.smackx.geoloc.packet.GeoLocation;
import org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider; import org.jivesoftware.smackx.geoloc.provider.GeoLocationProvider;
import org.jivesoftware.smackx.pubsub.LeafNode; 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.PayloadItem;
import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException; import org.jivesoftware.smackx.pubsub.PubSubException.NotALeafNodeException;
import org.jivesoftware.smackx.pubsub.PubSubManager;
import org.jivesoftware.smackx.xdata.provider.FormFieldChildElementProviderManager; import org.jivesoftware.smackx.xdata.provider.FormFieldChildElementProviderManager;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.Jid; import org.jxmpp.jid.Jid;
/**
* Entry point for Smacks API for XEP-0080: User Location.
* <br>
* To publish a UserLocation, please use {@link #sendGeolocation(GeoLocation)} method. This will publish the node.
* <br>
* To stop publishing a UserLocation, please use {@link #stopPublishingGeolocation()} method. This will send a disble publishing signal.
* <br>
* To add a {@link GeoLocationListener} in order to remain updated with other users GeoLocation, use {@link #addGeoLocationListener(GeoLocationListener)} method.
* <br>
* To link a GeoLocation with {@link Message}, use `message.addExtension(geoLocation)`.
* <br>
* An example for illustration is provided inside GeoLocationTest inside the test package.
* <br>
* @see <a href="https://xmpp.org/extensions/xep-0080.html">
* XEP-0080: User Location</a>
*/
public final class GeoLocationManager extends Manager { public final class GeoLocationManager extends Manager {
public static final String GEOLOCATION_NODE = "http://jabber.org/protocol/geoloc";
public static final String GEOLOCATION_NOTIFY = GEOLOCATION_NODE + "+notify";
private static final Map<XMPPConnection, GeoLocationManager> INSTANCES = new WeakHashMap<>(); private static final Map<XMPPConnection, GeoLocationManager> INSTANCES = new WeakHashMap<>();
private static boolean ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT = true;
private final Set<GeoLocationListener> geoLocationListeners = new CopyOnWriteArraySet<>();
private final AsyncButOrdered<BareJid> asyncButOrdered = new AsyncButOrdered<BareJid>();
private final ServiceDiscoveryManager serviceDiscoveryManager;
private final PepManager pepManager;
static { static {
FormFieldChildElementProviderManager.addFormFieldChildElementProvider( FormFieldChildElementProviderManager.addFormFieldChildElementProvider(
GeoLocationProvider.GeoLocationFormFieldChildElementProvider.INSTANCE); GeoLocationProvider.GeoLocationFormFieldChildElementProvider.INSTANCE);
@ -54,11 +89,6 @@ public final class GeoLocationManager extends Manager {
}); });
} }
public GeoLocationManager(XMPPConnection connection) {
super(connection);
}
/** /**
* Retrieves a {@link GeoLocationManager} for the specified {@link XMPPConnection}, creating one if it doesn't * Retrieves a {@link GeoLocationManager} for the specified {@link XMPPConnection}, creating one if it doesn't
* already exist. * already exist.
@ -75,6 +105,36 @@ public final class GeoLocationManager extends Manager {
return geoLocationManager; return geoLocationManager;
} }
private GeoLocationManager(XMPPConnection connection) {
super(connection);
pepManager = PepManager.getInstanceFor(connection);
pepManager.addPepListener(new PepListener() {
@Override
public void eventReceived(EntityBareJid from, EventElement event, Message message) {
if (!GEOLOCATION_NODE.equals(event.getEvent().getNode())) {
return;
}
final BareJid contact = from.asBareJid();
asyncButOrdered.performAsyncButOrdered(contact, () -> {
ItemsExtension itemsExtension = (ItemsExtension) event.getEvent();
List<ExtensionElement> items = itemsExtension.getExtensions();
@SuppressWarnings("unchecked")
PayloadItem<GeoLocation> payload = (PayloadItem<GeoLocation>) items.get(0);
GeoLocation geoLocation = payload.getPayload();
for (GeoLocationListener listener : geoLocationListeners) {
listener.onGeoLocationUpdated(contact, geoLocation, message);
}
});
}
});
serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
if (ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT) {
enableUserLocationNotifications();
}
}
public void sendGeoLocationToJid(GeoLocation geoLocation, Jid jid) throws InterruptedException, public void sendGeoLocationToJid(GeoLocation geoLocation, Jid jid) throws InterruptedException,
NotConnectedException { NotConnectedException {
@ -111,7 +171,7 @@ public final class GeoLocationManager extends Manager {
*/ */
public void sendGeolocation(GeoLocation geoLocation) public void sendGeolocation(GeoLocation geoLocation)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException {
getNode().publish(new PayloadItem<GeoLocation>(geoLocation)); pepManager.publish(GeoLocation.NAMESPACE, new PayloadItem<GeoLocation>(geoLocation));
} }
/** /**
@ -125,13 +185,25 @@ public final class GeoLocationManager extends Manager {
*/ */
public void stopPublishingGeolocation() public void stopPublishingGeolocation()
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException {
GeoLocation emptyGeolocation = new GeoLocation.Builder().build(); pepManager.publish(GeoLocation.NAMESPACE, new PayloadItem<GeoLocation>(GeoLocation.EMPTY_GEO_LOCATION));
getNode().publish(new PayloadItem<GeoLocation>(emptyGeolocation));
} }
private LeafNode getNode() public void setGeoLocationNotificationsEnabledByDefault(boolean bool) {
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, NotALeafNodeException { ENABLE_USER_LOCATION_NOTIFICATIONS_BY_DEFAULT = bool;
return PubSubManager.getInstanceFor(connection()).getOrCreateLeafNode(GeoLocation.NAMESPACE);
} }
public void enableUserLocationNotifications() {
serviceDiscoveryManager.addFeature(GEOLOCATION_NOTIFY);
}
public void disableGeoLocationNotifications() {
serviceDiscoveryManager.removeFeature(GEOLOCATION_NOTIFY);
}
public boolean addGeoLocationListener(GeoLocationListener geoLocationListener) {
return geoLocationListeners.add(geoLocationListener);
}
public boolean removeGeoLocationListener(GeoLocationListener geoLocationListener) {
return geoLocationListeners.remove(geoLocationListener);
}
} }

View file

@ -48,6 +48,8 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi
public static final QName QNAME = new QName(NAMESPACE, ELEMENT); public static final QName QNAME = new QName(NAMESPACE, ELEMENT);
public static final GeoLocation EMPTY_GEO_LOCATION = GeoLocation.builder().build();
private static final Logger LOGGER = Logger.getLogger(GeoLocation.class.getName()); private static final Logger LOGGER = Logger.getLogger(GeoLocation.class.getName());
private final Double accuracy; private final Double accuracy;
@ -264,6 +266,10 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi
return NAMESPACE; return NAMESPACE;
} }
/**
* Returns a new instance of {@link Builder}.
* @return Builder
*/
public static Builder builder() { public static Builder builder() {
return new GeoLocation.Builder(); return new GeoLocation.Builder();
} }
@ -273,14 +279,35 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi
return true; return true;
} }
/**
* Returns the first GeoLocation, or <code>null</code> if it doesn't exist in {@link Message}.
* <br>
* @param message The Message stanza containing GeoLocation
* @return GeoLocation
*/
public static GeoLocation from(Message message) { public static GeoLocation from(Message message) {
return message.getExtension(GeoLocation.class); return message.getExtension(GeoLocation.class);
} }
/**
* Returns the first GeoLocation, or <code>null</code> if it doesn't exist in {@link FormField}.
* <br>
* @param formField the Formfield containing GeoLocation
* @return GeoLocation
*/
public static GeoLocation from(FormField formField) { public static GeoLocation from(FormField formField) {
return (GeoLocation) formField.getFormFieldChildElement(QNAME); return (GeoLocation) formField.getFormFieldChildElement(QNAME);
} }
/**
* This class defines a builder class for {@link GeoLocation}.
* <br>
* {@link GeoLocation} instance can be obtained using {@link #build()} method as follows.<br><br>
* <code>GeoLocation.Builder builder = GeoLocation.builder(); <br>
* GeoLocation geoLocation = builder.build();</code>
* <br><br>
* To set GeoLocation parameters, use their respective setters.
*/
public static class Builder { public static class Builder {
private Double accuracy; private Double accuracy;
@ -308,133 +335,285 @@ public final class GeoLocation implements Serializable, ExtensionElement, FormFi
private String tzo; private String tzo;
private URI uri; private URI uri;
/**
* Sets accuracy of horizontal GPS error in meters.
*
* @param accuracy accuracy in meters
* @return Builder
*/
public Builder setAccuracy(Double accuracy) { public Builder setAccuracy(Double accuracy) {
this.accuracy = accuracy; this.accuracy = accuracy;
return this; return this;
} }
/**
* Sets Altitude in meters above or below sea level.
*
* @param alt altitude in meters
* @return Builder
*/
public Builder setAlt(Double alt) { public Builder setAlt(Double alt) {
this.alt = alt; this.alt = alt;
return this; return this;
} }
/**
* Sets Vertical GPS error in meters.
*
* @param altAccuracy altAccuracy in meters
* @return Builder
*/
public Builder setAltAccuracy(Double altAccuracy) { public Builder setAltAccuracy(Double altAccuracy) {
this.altAccuracy = altAccuracy; this.altAccuracy = altAccuracy;
return this; return this;
} }
/**
* Sets a named area such as a campus or neighborhood.
*
* @param area the named area
* @return Builder
*/
public Builder setArea(String area) { public Builder setArea(String area) {
this.area = area; this.area = area;
return this; return this;
} }
/**
* Sets GPS bearing (direction in which the entity is heading<br>
* to reach its next waypoint), measured in decimal degrees,<br>
* relative to true north.
*
* @param bearing bearing in decimal degrees
* @return Builder
*/
public Builder setBearing(Double bearing) { public Builder setBearing(Double bearing) {
this.bearing = bearing; this.bearing = bearing;
return this; return this;
} }
/**
* Sets a specific building on a street or in an area.
*
* @param building name of the building
* @return Builder
*/
public Builder setBuilding(String building) { public Builder setBuilding(String building) {
this.building = building; this.building = building;
return this; return this;
} }
/**
* Sets the nation where the user is located.
*
* @param country user's country of location
* @return Builder
*/
public Builder setCountry(String country) { public Builder setCountry(String country) {
this.country = country; this.country = country;
return this; return this;
} }
/**
* Sets The ISO 3166 two-letter country code.
*
* @param countryCode two-letter country code
* @return Builder
*/
public Builder setCountryCode(String countryCode) { public Builder setCountryCode(String countryCode) {
this.countryCode = countryCode; this.countryCode = countryCode;
return this; return this;
} }
/**
* Sets GPS Datum.
*
* @param datum GPS datum
* @return Builder
*/
public Builder setDatum(String datum) { public Builder setDatum(String datum) {
this.datum = datum; this.datum = datum;
return this; return this;
} }
/**
* Sets A natural-language name for or description of the location.
*
* @param description description of the location
* @return Builder
*/
public Builder setDescription(String description) { public Builder setDescription(String description) {
this.description = description; this.description = description;
return this; return this;
} }
/**
* Sets Horizontal GPS error in arc minutes;<br>
* this element is deprecated in favor of accuracy.
*
* @param error error in arc minutes
* @return Builder
*/
public Builder setError(Double error) { public Builder setError(Double error) {
this.error = error; this.error = error;
return this; return this;
} }
/**
* Sets a particular floor in a building.
*
* @param floor floor in a building
* @return Builder
*/
public Builder setFloor(String floor) { public Builder setFloor(String floor) {
this.floor = floor; this.floor = floor;
return this; return this;
} }
/**
* Sets Latitude in decimal degrees North.
*
* @param lat latitude in decimal degrees
* @return Builder
*/
public Builder setLat(Double lat) { public Builder setLat(Double lat) {
this.lat = lat; this.lat = lat;
return this; return this;
} }
/**
* Sets Locality within the administrative region,<br>
* such as a town or city.
*
* @param locality locality in a region
* @return Builder
*/
public Builder setLocality(String locality) { public Builder setLocality(String locality) {
this.locality = locality; this.locality = locality;
return this; return this;
} }
/**
* Sets Longitude in decimal degrees East.
*
* @param lon longitude in decimal degrees
* @return Builder
*/
public Builder setLon(Double lon) { public Builder setLon(Double lon) {
this.lon = lon; this.lon = lon;
return this; return this;
} }
/**
* Sets PostalCode used for postal delivery.
*
* @param postalcode code for postal delivery
* @return Builder
*/
public Builder setPostalcode(String postalcode) { public Builder setPostalcode(String postalcode) {
this.postalcode = postalcode; this.postalcode = postalcode;
return this; return this;
} }
/**
* Sets an administrative region of the nation,<br>
* such as a state or province.
*
* @param region an administrative region
* @return Builder
*/
public Builder setRegion(String region) { public Builder setRegion(String region) {
this.region = region; this.region = region;
return this; return this;
} }
/**
* Sets a particular room in a building.
*
* @param room room inside a building
* @return Builder
*/
public Builder setRoom(String room) { public Builder setRoom(String room) {
this.room = room; this.room = room;
return this; return this;
} }
/**
* Sets Speed at which the entity is moving, in meters per second.
*
* @param speed speed in meters per second
* @return Builder
*/
public Builder setSpeed(Double speed) { public Builder setSpeed(Double speed) {
this.speed = speed; this.speed = speed;
return this; return this;
} }
/**
* Sets a thoroughfare within the locality, or a crossing of two thoroughfares.
*
* @param street name of the street
* @return Builder
*/
public Builder setStreet(String street) { public Builder setStreet(String street) {
this.street = street; this.street = street;
return this; return this;
} }
/**
* Sets a catch-all element that captures any other information about the location.
*
* @param text distinctive feature about the location
* @return Builder
*/
public Builder setText(String text) { public Builder setText(String text) {
this.text = text; this.text = text;
return this; return this;
} }
/**
* Sets UTC timestamp specifying the moment when the reading was taken.
*
* @param timestamp timestamp of the reading
* @return Builder
*/
public Builder setTimestamp(Date timestamp) { public Builder setTimestamp(Date timestamp) {
this.timestamp = timestamp; this.timestamp = timestamp;
return this; return this;
} }
/**
* Sets the time zone offset from UTC for the current location.
*
* @param tzo time zone offset
* @return Builder
*/
public Builder setTzo(String tzo) { public Builder setTzo(String tzo) {
this.tzo = tzo; this.tzo = tzo;
return this; return this;
} }
/**
* Sets URI or URL pointing to information about the location.
*
* @param uri uri to the location
* @return Builder
*/
public Builder setUri(URI uri) { public Builder setUri(URI uri) {
this.uri = uri; this.uri = uri;
return this; return this;
} }
/**
* This method is called to build {@link GeoLocation} from the Builder.
*
* @return GeoLocation
*/
public GeoLocation build() { public GeoLocation build() {
return new GeoLocation(accuracy, alt, altAccuracy, area, bearing, building, country, countryCode, datum, description, return new GeoLocation(accuracy, alt, altAccuracy, area, bearing, building, country, countryCode, datum, description,
error, floor, lat, locality, lon, postalcode, region, room, speed, street, text, timestamp, error, floor, lat, locality, lon, postalcode, region, room, speed, street, text, timestamp,
tzo, uri); tzo, uri);
} }
} }
} }

View file

@ -0,0 +1,107 @@
/**
*
* Copyright 2020 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.geolocation;
import java.net.URI;
import java.util.concurrent.TimeoutException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smackx.geoloc.GeoLocationListener;
import org.jivesoftware.smackx.geoloc.GeoLocationManager;
import org.jivesoftware.smackx.geoloc.packet.GeoLocation;
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;
import org.jxmpp.util.XmppDateTime;
public class GeolocationIntegrationTest extends AbstractSmackIntegrationTest {
private final GeoLocationManager glm1;
private final GeoLocationManager glm2;
public GeolocationIntegrationTest(SmackIntegrationTestEnvironment environment) {
super(environment);
glm1 = GeoLocationManager.getInstanceFor(conOne);
glm2 = GeoLocationManager.getInstanceFor(conTwo);
}
@SmackIntegrationTest
public void test() throws TimeoutException, Exception {
GeoLocation.Builder builder = GeoLocation.builder();
GeoLocation geoLocation1 = builder.setAccuracy(23d)
.setAlt(1000d)
.setAltAccuracy(10d)
.setArea("Delhi")
.setBearing(10d)
.setBuilding("Small Building")
.setCountry("India")
.setCountryCode("IN")
.setDescription("My Description")
.setError(90d)
.setFloor("top")
.setLat(25.098345d)
.setLocality("awesome")
.setLon(77.992034)
.setPostalcode("110085")
.setRegion("North")
.setRoom("small")
.setSpeed(250.0d)
.setStreet("Wall Street")
.setText("Unit Testing GeoLocation")
.setTimestamp(XmppDateTime.parseDate("2004-02-19"))
.setTzo("+5:30")
.setUri(new URI("http://xmpp.org"))
.build();
IntegrationTestRosterUtil.ensureBothAccountsAreSubscribedToEachOther(conOne, conTwo, timeout);
final SimpleResultSyncPoint geoLocationReceived = new SimpleResultSyncPoint();
final GeoLocationListener geoLocationListener = new GeoLocationListener() {
@Override
public void onGeoLocationUpdated(BareJid jid, GeoLocation geoLocation, Message message) {
if (geoLocation.equals(geoLocation1)) {
geoLocationReceived.signal();
}
}
};
glm2.addGeoLocationListener(geoLocationListener);
try {
glm1.sendGeolocation(geoLocation1);
geoLocationReceived.waitForResult(timeout);
} finally {
glm2.removeGeoLocationListener(geoLocationListener);
}
}
@AfterClass
public void unsubscribe() throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
IntegrationTestRosterUtil.ensureBothAccountsAreNotInEachOthersRoster(conOne, conTwo);
}
}

View file

@ -0,0 +1,23 @@
/**
*
* Copyright 2020 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.
*/
/**
* Integration Tests for Smacks support for XEP-0080: User Location.
*
* @see <a href="https://xmpp.org/extensions/xep-0080.html">
* XEP-0080: User Location</a>
*/
package org.jivesoftware.smackx.geolocation;