XEP-0080: User Location on Android using PEP with aSmack
Deon Botha• Nov 2, 2014A project I’m currently working on requires periodically publishing a user’s location to their XMPP roster friends on Android.
My initial feeling given the relative simplicity of the problem and readily available XEP-0080: User Location specification was that I’d have something up and running in a matter of hours. How wrong I was. I ended up spending several days in a state of perpetual frustration just trying to get PEP to play nicely with aSmack.
Given the lack of documented Android examples surrounding PEP and aSmack at the time of writing, arriving upon solution was more luck than anything. Frustratingly it came down to some careful (and not entirely intuitive) ordering of a few key statements.
So that others might not have to go through the same trouble I thought I’d document my approach now that I have something working.
I made the decision to loosely follow the XEP-0080 specification as I’m relatively new to XMPP and figured it would lead me down a “best practice” path. It also seemed to cover my requirements nicely:
This specification defines an XMPP protocol extension for communicating information about the current geographical or physical location of an entity.
As aSmack does not support XEP-0080 out the box we’ll need to write:
- A custom
PEPItem
(effectively aPacketExtension
) to hold the user location data - A custom
PEPEvent
to store thePEPItem
- A custom
PacketExtensionProvider
to parse the custom user locationPEPEvent
’s from incoming packets - Some boilerplate to tie the above together
- A function to publish a user’s location to the XMPP server
In the interest of brevity the only XML child elements from the specification that I’ll cover in this post are those relating to latitude and longitude. Adding support for the remainder of the specification should be a trivial exercise once we’ve got the foundations in place.
We’re going to be making use of the Smack provider architecture to handle parsing parsing of our custom user location packet extensions. I’ll assume that you’re familiar with this architecture and won’t be covering it any any detail.
A PEPItem to store user location
To start we need to define a PEPItem
to hold the user location data that we’ll publish and receive. A PEPItem
is
responsible for transforming the location data into a representative XML element.
public class UserLocation extends PEPItem {
public static final String NODE =
"http://jabber.org/protocol/geoloc";
public final double latitude, longitude;
public UserLocation(double latitude, double longitude) {
this(StringUtils.randomString(16), latitude, longitude);
}
public UserLocation(double latitude, double longitude,
String id) {
super(id);
this.latitude = latitude;
this.longitude = longitude;
}
@Override
java.lang.String getNode() {
return NODE;
}
// return an XML element approximately inline
// with the XEP-0080 spec
@Override
java.lang.String getItemDetailsXML() {
return String.format(
"<geoloc xmlns='%s'><lat>%f</lat>" +
"<lon>%f</lon></geoloc>",
NODE, latitude, longitude);
}
}
A PEPEvent to hold the PEPItem
Next we’ll need a custom PEPEvent
class to hold our UserLocation
item. This one’s mostly boilerplate but is required to play nice with the PEPManager
subsystem.
public class UserLocationEvent extends PEPEvent {
private final UserLocation location;
public UserLocationEvent(UserLocation location) {
this.location = location;
}
public UserLocation getLocation() {
return location;
}
@Override
public String getNamespace() {
return "http://jabber.org/protocol/pubsub#event";
}
@Override
public String toXML() {
return String.format("<event xmlns=" +
"'http://jabber.org/protocol/pubsub#event' >" +
"<items node='%s' >%s</items></event>",
UserLocation.NODE, location.toXML());
}
}
A PacketExtensionProvider to parse incoming friend locations
Whenever a friend on our roster publishes their location it will find it’s way to us nested as an extension deep within an XMPP packet.
Out of the box aSmack will not know what to do with our custom extension that it finds within these packets and will effectively ignore it.
To have them parsed and reported back in a sensible form we need to create and register a custom PacketExtensionProvider
.
public class UserLocationProvider
implements PacketExtensionProvider {
// This method will get called whenever aSmack discovers a
// packet extension containing a publish element with the
// attribute node='http://jabber.org/protocol/geoloc'
@Override
public PacketExtension parseExtension(XmlPullParser parser)
throws Exception {
boolean stop = false;
String id = null;
double latitude = 0;
double longitude = 0;
String openTag = null;
while (!stop) {
int eventType = parser.next();
switch (eventType) {
case XmlPullParser.START_TAG:
openTag = parser.getName();
if ("item".equals(openTag)) {
id = parser.getAttributeValue("", "id");
}
break;
case XmlPullParser.TEXT:
if ("lat".equals(openTag)) {
try {
latitude = Double.parseDouble(
parser.getText());
} catch (NumberFormatException ex) {
/* ignore */
}
} else if ("lon".equals(openTag)) {
try {
longitude = Double.parseDouble(
parser.getText());
} catch (NumberFormatException ex) {
/* ignore */
}
}
break;
case XmlPullParser.END_TAG:
// Stop parsing when we hit </item>
stop = "item".equals(parser.getName());
openTag = null;
break;
}
}
return new UserLocationEvent(
new UserLocation(id, latitude, longitude));
}
}
Tying it all together
And now for the magic that eluded me for so long… and as I found out the hard way – the ordering of the code snippets in this section is crucial.
XMPPTCPConnection connection = new XMPPTCPConnection();
ServiceDiscoveryManager sdm = ServiceDiscoveryManager
.getInstanceFor(connection);
sdm.addFeature("http://jabber.org/protocol/geoloc");
sdm.addFeature("http://jabber.org/protocol/geoloc+notify");
EntityCapsManager capsManager = EntityCapsManager
.getInstanceFor(connection);
capsManager.enableEntityCaps();
In the above code we create a connection and indicate to the ServiceDiscoveryManager
that we support XEP-0080.
The +notify
suffix indicates our additional interest in receiving notifications related to this protocol too, rather than just publishing them.
Reporting of entity capabilities is also enabled. This means publishing and subscribing XEP-0080 capabilities will be reported within presence updates sent by the Android client.
In the code snippet below we register our custom UserLocationProvider
with the ProviderManager
as per the
Smack provider architecture.
aSmack will now know what to do when it finds a user location extension nested within an XMPP packet i.e. a
UserLocationEvent
will ultimately get parsed out of the packet which will then find its way to the PEPManager
and
finally reported to our own listener.
PEPProvider pepProvider = new PEPProvider();
pepProvider.registerPEPParserExtension(
"http://jabber.org/protocol/geoloc",
new UserLocationProvider());
ProviderManager.addExtensionProvider("event",
"http://jabber.org/protocol/pubsub#event", pepProvider);
PEPManager pepManager = new PEPManager(connection);
pepManager.addPEPListener(PEP_LISTENER);
connection.connect();
connection.login(username, password);
And after all that setup we provide a listener to be notified when a friend publishes a UserLocationEvent
PEPListener PEP_LISTENER = new PEPListener() {
@Override
public void eventReceived(String from, PEPEvent event) {
if (event instanceof UserLocationEvent) {
// do something interesting
}
}
};
Publishing a users location with PEP
Finally publishing a user’s location is pretty straightforward in comparison to receiving them and can be accomplished as follows
public void publishLocation(double latitude, double longitude) {
UserLocation userLocation =
new UserLocation(latitude, longitude);
try {
pepManager.publish(userLocation);
} catch (SmackException.NotConnectedException ex) {
/* ignore */
}
}