RedundantCoding

Wir entwickeln Web & Mobile Apps.

ASP.NET Core/MVC, MSSQL, JAVA, Android, iOS(Swift), libGDX, XMPP, WebRTC, Smack


Creating custom stanza listener with XMPP, Smack for android

by Kenan Begić


Posted on March 23, 2019 at 20:25


asp.net

This tutorial will explain how to create custom stanza listener for our android client chat application. If you are not familiar with XMPP and its terminology you should visit wiki page and get some insights about what XMPP is. XMPP is a standardized protocol for streaming XML. When client want to start a session with an XMPP server, client open TCP connection and then establish an XML stream to the server. Once client have established an XML stream with XMPP server, client and XMPP server can exchange special XML snippets over the stream. There are three basic XML snippets: <message />, <presence />, and <iq />. Here is the example of one of those XML stanzas.



These snippets, also called XML stanzas, are the basic units of meaning in XMPP, and once you have negotiated an XML stream you can send an unlimited number of stanzas over the stream. This tutorial will explain the basics of creating singleton Smack class and the way you can create custom stanza provider to exchange custom XML stanza formats.

1. Creating model class for user location


First things first, we are going to create simple class that will represent user location. User location class will hold all needed logic for sending user location that will be chosen on our map. Commonly used fields are longitude, latitude, address and name. All those fields will be provided from chosen Place from map that will be presented to user. Our simple class will look like this.

        
    public class UserLocation implements ExtensionElement
    {
                //XML Tags
                public static final String NAMESPACE = "jabber:client";
                public static final String ELEMENT_NAME = "location";
                public static final String TAG_NAME_LATITUDE = "lat";
                public static final String TAG_NAME_LONGITUDE = "lon";
                public static final String TAG_NAME_NAME = "name";
                public static final String TAG_NAME_ADDRESS = "addr";

                //FIelds
                private double latitude;
                private double longitude;
                private String name;
                private String address;

                public UserLocation(Place place) {
                        name = place.getName().toString();
                        address = place.getAddress().toString();
                        latitude = place.getLatLng().latitude;
                        longitude = place.getLatLng().longitude;
                }

                @Override
                public String getElementName() {
                        return ELEMENT_NAME;
                }

                @Override
                public String getNamespace() {
                        return NAMESPACE;
                }

                @SuppressLint("DefaultLocale")
                @Override
                public CharSequence toXML(String enclosingNamespace) {
                        return String.format("<location xmlns='jabber:client'>
                                                                <type>%d</type>
                                                                <lat>%f</lat>
                                                                <lon>%f</lon>
                                                                <name>%s</name>
                                                                <addr>%s</addr>
                                                                </location>",
                                                                ChatMessageTableHelper.TYPE_OUTGOING_LOCATION, 
                                                                latitude, longitude, name, address);
                }
    }
            
        

The way we can ask our user to share location or send pointed location is:

            
    private void sendLocation() {
            // Construct an intent for the place picker
            try {
                    PlacePicker.IntentBuilder intentBuilder = new PlacePicker.IntentBuilder();
                    Intent intent = intentBuilder.build(this);
                    startActivityForResult(intent, REQUEST_PLACE_PICKER);
            } 
            catch (GooglePlayServicesRepairableException | 
                            GooglePlayServicesNotAvailableException e) 
            {
                    Log.e(LOG_TAG, e.toString(), e);
            }
    }
                
            


Later in our onActivityResult method when we get result we create our UserLocation class and send it to our Smack singleton that we will create in next chapter.

            
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (resultCode == Activity.RESULT_OK) {
                    switch (requestCode) {
                            case REQUEST_PLACE_PICKER:
                                    // The user has selected a place. Extract the name and address.
                                    Place place = PlacePicker.getPlace(this, data);
                                    UserLocation location = new UserLocation(place);
                                    //Send Custom stanza to our Smack singleton class
                                    SmackSingelton.getInstance(context).sendChatMessage("buddy@domain.com", "Location message", location) 
                                    break;
                    }
            }
    }
                
            

Now lets explain the UserLocation class a little more. As we can see our UserLocation class extend ExtensionElement interface. As it is self-explanatory, it means that this class will be extension to our standard XML message stanza that will carry custom user XML and will be parsed with custom provider that we will create later. This means that our message will contain custom child element, in our example it is user location with its own child elements that represent basics of location. Additional explanation of extension elements can be found here

Next what we can see is that we must implement three methods of this interface. Those methods are: getElementName(), getNamespace(), toXML(). As we can see first method getElementName(), is returning our static field ELEMENT_NAME that is representing our root element name of our extension. Second method getNamespace(), is returning our static field NAMESPACE what is representing root element XML namespace. And finally third method toXML(), is returning our stanza extension in XML representation.

2. Create user location provider


As we have mentioned before, in order to send our custom stanza as extension element we must create custom provider that will parse our custom stanza XML, and later represent location to our user. So here is our custom provider class for location.

        
     public class LocationMessageProvider extends ExtensionElementProvider {
            @Override
            public Element parse(XmlPullParser parser, int initialDepth) throws Exception {
                UserLocation location = new UserLocation();

                boolean done = false;
                String tag = null;
                while (!done) {
                            int eventType = parser.next();
                            switch (eventType) {
                                    case XmlPullParser.START_TAG:
                                            tag = parser.getName();
                                            break;

                                    case XmlPullParser.TEXT:
                                            if (UserLocation.TAG_NAME_LATITUDE.equals(tag)) {
                                                    try {
                                                            location.setLatitude(Double.parseDouble(parser.getText()));
                                                    } 
                                                    catch (NumberFormatException ignored) {}
                                            } 
                                            else if (UserLocation.TAG_NAME_LONGITUDE.equals(tag)) {
                                                    try {
                                                            location.setLongitude(Double.parseDouble(parser.getText()));
                                                    } 
                                                    catch (NumberFormatException ignored) {}
                                            } 
                                            else if (UserLocation.TAG_NAME_NAME.equals(tag)) {
                                                            location.setName(parser.getText());
                                            } 
                                            else if(UserLocation.TAG_NAME_ADDRESS.equals(tag)) {
                                                            location.setAddress(parser.getText());
                                            }
                                            break;

                                    case XmlPullParser.END_TAG:
                                            // Stop parsing when we hit </addr>
                                            done = UserLocation.TAG_NAME_ADDRESS.equals(parser.getName());
                                            break;
                            }
                }
                return location;
            }
    }
            
        

As we can see, our LocationMessageProvider extend ExtensionElementProvider abstract class, and it must implement parse() method that will parse our custom stanza if our message stanza contain ExtensionElements. This way we are deserializing raw XML into our UserLocation class. Next what we will need is to register our provider so it can parse our custom stanza that we will send. For this we will create our Smack singleton class.

3. Creating Smack singleton class


Smack singleton class encapsulate all functionalities needed for our client to establish XMPP connection with server. This means that user must be registered and logged in. We will not cover those functionalities as they are not the subject for this post. Our simplified Smack singleton class will look as this.

            
        
 public class SmackSingleton {

        private static final String LOG_TAG = "SmackSingleton";
        private MessagePacketListener messagePacketListener;
        private static SmackSingleton instance;
        private XMPPTCPConnection con;

        private SmackSingleton(Context context) {
                this.context = context;
                messagePacketListener = new MessagePacketListener(context);
                ProviderManager.addExtensionProvider(
                                UserLocation.ELEMENT_NAME, 
                                UserLocation.NAMESPACE, 
                                new LocationMessageProvider()
                );
        }

        public static synchronized SmackSingleton getInstance(Context context) {
                if (instance == null) {
                        instance = new SmackSingleton(context.getApplicationContext());
                }
                return instance;
        }

        //....Login and connection functionalities..//

        public void sendChatMessage(String to, String body, ExtensionElement extension) 
            throws SmackInvocationException 
            {
                try {
                        jid = JidCreate.entityBareFrom(to);
                        Message msg = new Message();

                        //For our message body can be empty
                        msg.setBody(body);

                        //Add our extension to message that will be sent to user
                        msg.addExtension(extension);

                        //Add delivery request if needed to msg which is again another extension to  
                        //message stanza as our location extension as it extend ExtensionElement
                        //interface
                        DeliveryReceiptRequest.addTo(msg);

                        //Send chat message
                        ChatManager.getInstanceFor(connection).chatWith(jid).send(msg);
                } 
                catch (NotConnectedException e) {
                        throw new SmackInvocationException(e);
                } 
                catch (InterruptedException | 
                        XmppStringprepException | 
                        SmackException.NoResponseException e) 
                {
                        e.printStackTrace();
                }
        }
 }
            
        

If we take a look at our class, we have three important things. First, when we create our singleton class, we are adding our LocationMessageProvider to ProviderManager. This means that we are registering our custom provider with provider that will be responsible for parsing custom XML stanzas. Second, is MessagePacketListener. This listener extension of StanzaListener interface, that listen to packets sent to our client. In our case, we listen for only specific packet that need to be processed. This concept is also known as event-style programming. Finally, third thing is that we must send our message. For that is responsible our sendChatMessage(String to, String body, ExtensionElement extension) method. This method sends message to our buddy with body which represent text that user will receive and custom extension. This extension will be UserLocation.

4. Creating MessagePacketListener


When we send our message to our buddy, there must be a way to receive that message. We have mentioned in earlier chapter that MessagePacketListener is responsible for such functionalities. So this is our listener class that we register in our SmackSingleton class.


        
 public class MessagePacketListener implements StanzaListener{
    private Context context;

    MessagePacketListener(Context context) {
                    this.context = context;
    }

    private void processPacketExtension(Intent intent, Message msg) {
                    Collection<ExtensionElement> extensions = msg.getExtensions();
                    if (extensions != null) {
                            Iterator<ExtensionElement> iterator = extensions.iterator();
                                    if (iterator.hasNext()) {
                                            ExtensionElement extension = iterator.next();
                                    if (extension instanceof UserLocation) {
                                            //...We have received UserLcoation...//
                                            UserLocation loc = (UserLocation)extension;
                                    }
                                    if(extension instanceof DeliveryReceipt){
                                            //...Our buddy has set request for DeliveryReceipt extension...//
                                            //...so we will send DeliveryReceipt extension to our buddy...//
                                            DeliveryReceipt delivery = (DeliveryReceipt) extension;
                                    }
                            }
                    }
    }

    @Override
    public void processStanza(Stanza packet) {
                    Message msg = (Message)packet;
                    processPacketExtension(intent, msg);
                    //...Do the sth with message...//
    }
 }
            
        

Our stanza listener must extend interface method processStanza. This method process our message that was sent from our buddy. Additionally we call processPacketExtension method that, process our message and iterate over all extension that it has. If we find any of extensions that are important to us like our user location or delivery receipt we are deserializing raw XML into class we need, so it become usable to our application.

So there it is, our custom extension received by our buddy.

Viola.

5. References


xmpp.org
Extensible Messaging and Presence Protocol (XMPP): Core
Smack docs

6. Contact


Conact me at:

begic_kenan@hotmail.com
info@redundantcoding.com
LinkedIn