XEP-0080: User Location on Android using PEP with aSmack

by

A 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:

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 */
    }
}