powered by Jive Software

Help with smack client

Hello smack pros. I’m developing an XMPP android client. I’m using ejabberd as a server for XMPP. I’ve successfully implemented most of the features in my client but there’s still a number left plus my current implementation is not effective in different scenarios. Here I’ll try to explain each bit of my smack implementation.
Sorry for long question. Believe me I didn’t copy paste any word, I’ve typed every single letter in this question and it took me more than two hours to write and minimize the word count.
I’ve posted my questions at the end of the post, please help me. Really need help from smack pros and developers at igniterealtime.

Integration of smack via gradle:

implementation "org.igniterealtime.smack:smack-android-extensions:4.2.3"
implementation "org.igniterealtime.smack:smack-tcp:4.2.3"

In my client I’ve a class named XMPP.java which is responsible for all smack related work.
Application has two basic scenarios: Login & Register

Case Login:
AppStart -> Login
Set: XMPP.username and XMPP.password
Call: XMPP.getInstance(context).init(XMPP.LOGIN);

Case Register:
AppStart -> Register
Set: XMPP.username and XMPP.password
Call: XMPP.getInstance(context).init(XMPP.REGISTER);

In both cases I first get instance of XMPP and then call init(connectionType)
Here’s the code to getInstance() and init()

Required data members:
private static XMPP instance = null;
private static Context mContext;

public static void XMPP getInstance(context){
    if(instance == null){
        mContext = context;
        instance = new XMPP();
    }
    return instance;
}

public void init(connectionType){
    connect(connectionType);
    //initialize other logic
}

Here’s the code to connect()

Required data members:
private static AbstractXMPPConnection connection = null;
private static String host = "host ip";
private static String domain = "domain name";
private static int port = 5222;

private static void connect(connectionType){
    new AsyncTask<Void, Void, Void>{
        @Override
        doInBackground(....){
            try{
                InetAddress addr = InetAddress.getByName(host);
                HostnameVerifier verifier = new HostnameVerifier(){
                    @Override
                    public boolean verify(String hostname, SSLSession session){
                        return false;
                    }
                };

                DomainBareJid serviceName = JidCreate.domanBareJid(domain);
                XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
                        .setPort(port)
                        .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)
                        .setXmppDomain(serviceName)
                        .setHostnameVerifier(verifier)
                        .setHostAddress(addr)
                        .setDebuggerEnabled(true)
                        .build();

                connection = new XMPPTCPConnection(config);
                connection.addConnectionListener(new ConnectionListener(){
                    @Override
                    public void connected(XMPPConnection con){
                        if(!con.isAuthenticated){
                            switch(connectionType){
                                case XMPP.LOGIN:
                                    login();
                                    break;
                                case XMPP.REGISTER:
                                    register();
                                    break;
                            }
                        }
                    }
                    @Override
                    public void authenticated(XMPPConnection con, boolean resumed){
                        saveCredentials();
                        initRoster();
                        initChatManager();
                        initFileTransferManager();
                    }
                    //other override methods from connection listener
                    //left them empty, what logic should I implement there to make the connection persistent??
                });

                connection.connect();
            }
            //any specific instruction to handle exceptions?
            catch(IOException){
                printStackTrace() / toast() / log()
            }
            catch(InterruptedException){
                printStackTrace() / toast() / log()
            }
            catch(SmackException){
                printStackTrace() / toast() / log()
            }
            catch(SmackException){
                printStackTrace() / toast() / log()
            }
            return null;
        }
    }.execute();
}

Here’s the code to disconnect()

Required data members:
private static AbstractXMPPConnection connection = null;
private static Roster roster = null;
private static ChatManager = null;
private static FileTransfermanager = null
//other managers

public static void disconnect(){
    connection.disconnect();
    roster = null;
    username = null;
    password = null;
    chat manager / file transfer manager / etc = null;
}

This is how I login a user

Required data members:
public static String username = null;
public static String password = null;
private static AbstractXMPPConnection connection = null;

private static void login(){
    try{
        connection.login(username, password);
    }
    //specific instructions to handle exceptions for better user experience?
    catch(InterruptedException){
        disconnect();
        printStackTrace() / toast() / log()
    }
    catch(IOException){
        disconnect();
        printStackTrace() / toast() / log()
    }
    catch(SmackException){
        disconnect();
        printStackTrace() / toast() / log()
    }
    catch(XMPPException){
        disconnect();
        printStackTrace() / toast("invalid username or password") / log()
    }
}

And this is how I register a user

Required data members:
public static String username = null;
public static String password = null;
private static AbstractXMPPConnection connection = null;

private static void register(){
    AccountManager accountManager = AccountManager.getInstance(connection);
    try{
        if(accountManager.supportsAccountCreation()){
            accountManager.sensitiveOperationOverInsecureConnection(true);
            accountManger.createAccount(Localpart.from(username), password);
            login();
        }
        //specific instructions to handle exceptions for better user experience?
        catch(InterruptedException){
            printStackTrace() / toast() / log()
        }
        catch(XMPPException.XMPPErrorException){
            printStackTrace() / toast() / log()
        }
        catch(XmppStringprepException){
            printStackTrace() / toast() / log()
        }
        catch(SmackException.NotConnectedException){
            printStackTrace() / toast() / log()
        }
        catch(SmackException.NoResponseException){
            printStackTrace() / toast() / log()
        }
    }
}

Code to initialize and handle roster

Required data members:
private static AbstractXMPPConnection connection = null;
private static Roster roster = null;
private static Map<String, RosterEntryModel> presenceMap = new HashMap<>();
private static List<RosterEntryModel> friends = new ArrayList<>();

private static void initRoster(){
    roster = Roster.getInstanceFor(connection);
    roster.addSubscribeListener(new SubscribeListener(){
        @Override
        public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest){
            //auto approve all incoming requests and send a presence request back if needed
            return SubscribeAnswer.ApproveAndAlsoRequestIfRequired;
        }
    });

    roster.addRosterListener(new RosterListener(){
        @Override
        public void entriedAdded() / entriesUpdated() / entriesDeleted(){
            //refresh roster on UI in all cases
            RosterActivity.getInstance().runOnUiThread(new Runnable(){
                public void run(){
                    RosterActivity.getInstance().refreshRoster();
                }
            });
        }
        @Override
        public void presenceChanged(Presence presence){
            String fqdn = presence.getFrom.toString();
            String jid = XmppStringUtils.parseBareJid(fqdn);
            if(presenceMap.containsKey(jid)){
                RosterEntryModel model = presenceMap.get(jid);
                model.setPresence(presence.isAvailable());
                if(presence.isAvailable()){
                    model.setLastSeen();
                    model.setFqdn();
                }
            }
        }
    });

    if(!roster.isLoaded()){
        try{
            roster.reloadAndWait();
        }
        catch(InterruptedException){
            printStackTrace() / toast() / log()
        }
        catch(SmackException.NotLoggedInException){
            printStackTrace() / toast() / log()
        }
        catch(SmackException.NotConnectedException){
            printStackTrace() / toast() / log()
        }

        if(roster != null){
            Collection<RosterEntry> entries = roster.getEntries();
            friends.clear();
            presenceMap.clear();
            for(RosterEntry entry: entries){
                RosterEntryModel model = new RosterEntryModel();
                //set data in custom roster entry model and save model in friends
                friends.add(model);
                //save presence to manipulate later
                presenceMap.put(model.getJid().toString(), model);
            }
        }
    }
}

This is how to get last seen of user, but I always get new year date for all users

Required data members:
private static AbstractXMPPConnection connection = null;
private LastActivtyManager lastActivityManager = null;

public static String String lastSeen(Jid jid){
    if(lastActivityManager == null){
        lastActivityManager = LastActivityManager.getInstanceFor(connection);
    }
    Long time;
    try{
        time = lastActivityManager.getLastActivity(jid).getIdelTime();
        return getLastSeenFormattedTime(time);
    }
    catch(SmackException.NoResponseException){
        printStackTrace() / log()
        return "";
    }
    catch(SmackException.NotConnectedException){
        printStackTrace() / log()
        return "";
    }
    catch(XMPPException.XMPPErrorException){
        printStackTrace() / log()
        return "";
    }
    catch(InterruptedException){
        printStackTrace() / log()
        return "";
    }
}

This is how I add a new friend in Roster by sending subscription request

Required data members:
private static AbstractXMPPConnection connection = null;

public static void addBuddy(String username){
    Presence subscribe = new Presence(Presence.Type.subcribe);
    //I get deprecated inspection report on setTo(), but it still works
    subscribe.setTo(username+"@"+domain);
    try{
        connection.sendStanza(subscribe);
    }
    catch(SmackException.NotConnectedExcepton){
        printStackTrace() / toast() / log()
    }
    catch(InterruptedExcepton){
        printStackTrace() / toast() / log()
    }
}

Code to initialize chat manager, I fail to get exacet time of incoming/outgoing message

Required data members:
private static AbstractXMPPConnection connection = null;
private static ChatManager = null;
private DelayInformation delayInformation;
private static long timeStamp;

private static void initChatManager(){
    if(chatManager == null){
        chatManager = ChatManager.getInstanceFor(connection);
        chatManager.addIncomingListerner(new IncomingChatMessageListener(){
            @Override
            public void newIncomingMessage(EntityBareJid from, Message message, Chat chat){
                //get messge time, I get null from delayInformation all the time
                delayInformation = (DelayInformation) message.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE);
                if(delayInformation != null){
                    timeStamp = delayInformation.getStamp().getTime();
                }
                else{
                    timeStamp = System.getCurrentTimeMillis();
                }
                //save message in database for history
                //if sent by an active chat user, update chat | else notify user
            }
        });

        chatManager.addOutgoingListener(new OutgoingChatMessageListerner(){
            @Override
            public void newOutgoingMessage(EntityBareJid to, Message message, Chat chat){
                //get messge time, I get null from delayInformation all the time
                delayInformation = (DelayInformation) message.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE);
                if(delayInformation != null){
                    timeStamp = delayInformation.getStamp().getTime();
                }
                else{
                    timeStamp = System.getCurrentTimeMillis();
                }
                //save message in database for history
                //if sent in an active chat, update chat
            }
        });
    }
}

Code to send a message

Required data members:
private static RosterEntryModel currentUser; //custom model for roster entry

public static void sendMessage(String message){
    EntityBareJid jid = null;
    try{
        jid = JidCreate.entityBareFrom(currentUser.getjid());        
    }
    catch(XmppStringprepException){
        printStackTrace() / toast() / log()
    }

    Chat chat = chatManager.chatWith(jid);
    try{
        chat.send(messge);
    }
    catch(SmackException.NotConnectedException){
        printStackTrace() / toast() / log()
    }
    catch(InterruptedException){
        printStackTrace() / toast() / log()
    }
}

Code to initialize file transfer manager

Required data members:
private static ServiceDiscoveryManager = null;
private static FileTransferManager = null;
private static AbstractXMPPConnection connection = null;
private static String pathToDirectory = "path to directory";

private static void initFileTransferManager(){
    if(serviceDiscoveryManager == null)
        serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
    serviceDiscoveryManager.addFeature("http://jabber.org/protoco/disco#info");
    serviceDiscoveryManager.addFeature("jabber:iq:privacy");

    if(fileTransferManager == null){
        fileTransferManager = FileTransferManager.getInstanceFor(connection);
        fileTransferManager.addFileTransferListener(new FileTransferListener(){
            @Override
            public void fileTransferRequest(FileTransferRequest request){
                String fileName = pathToDirectory +"/"+request.getFileName();
                IncomingFileTransfer transfer = request.accept();
                try{
                    transfer.receiveFile(new File(fileName));
                }
                catch(SmackException){
                    printStackTrace() / log()
                }
                catch(IOException){
                    printStackTrace() / log()
                }
            }
        });
    }   
}

Code to send a file

Required data members:
private static RosterEntryModel currentUser; //custom model for roster entry
private static AbstractXMPPConnection connection = null;

public static void sendFile(String path, String description){
    String sFqdn = currentUser.getFqdn();
    if(sFqdn.equals(null)) return;
    String node = XmppSringUtils.parseLocalpart(sFqdn);
    String domain = XmppSringUtils.parseDomain(sFqdn);
    String resource = XmppSringUtils.parseResource(sFqdn);

    try{
        EntityFullJid fqdn = entityFullFrom(node, domain, resource);
        OutgoingFileTransfer transfer = FileTransferManager.getInstanceFor(connection).createOutgoingFileTransfer(fqdn);
        FileTransferNegotiator.IBB_ONLY = true;
        transfer.sendFile(new File(path), description);
    }
    catch(SmackException){
        printStackTrace() / log()
    }
    catch(XmppStringprepException){
        printStackTrace() / log()
    }
}

Questions:
Q1 If I logout a user from server by calling disconnect(), I can still see him active on server and other users can also see him active in their roster, which results in loss of incoming messages, because the user is seen active at server so server does not save them as offline messages to send later. There is nothing wrong with server, I’ve checked it by using a client called “Bruno the jabber bear”, it successfully logs the user out. What’s is the proper way to log out?

Q2 Currenty the client registers a new user by username and password only, how can I add more details at registration time like email, phone number etc. I tried using key, value paired attributes but had no success, on searching I found out that vCard can help me. If vCard is the solution, what’s the proper way to implement it? Are there any better solutions?

Q3 How can I check for username availability on server, the methods I found to check username require auth before username search for example this stackoverflow post.

Q4 How can I set user’s profile picture, do I need to implement vCard? related to Q2

Q5 How can I connect user with a static or previously used resource? I’m keeping track of user’s presence change and resource ID, but is there anyway to make it static? because when I send a file to user, most of the time it fails due to resource changed by user.

Q6 Currently I’m sending a file in real time, success rate is below 50% due to resource change and stream issues, I’ve tried forced IBB, but no success. How do I send files offline? Smack-experimental contains HttpFileUpload package, Is it stable? should I use it? Do I have to write a service at server side as well to accomplish it? Are there any better solutions?

Q7 How can I implement TLS mechanism in my client?

Q8 In devices running android 6.0 and above, connection is automatically closed by OS to prevent battery waste when device goes in sleep mode, how can I keep my connection alive? Do I have to use sticky foreground services? Can’t use background services in 6.0 and above, android forces the use of JobScheduler instead, which is not the requirement. So does smack offer something out of the box? or are there any good libraries to do this task? or any other solution?

Q9 Whatsapp, facebook messenger and other social media applications use FCM/GCM to send notifications from server to client, clients receive them even when application is closed, phone is in sleep mode. Do I have to implement FCM as well? Is it secure? are there any tutorials integrate FCM? any better alternatives?

Q10 How do I get last seen of any user? please check my current implementation, had no success.

Q11 How to get exact time of incoming/outgoing message check my current implementation.

Q12 Can I create new account without password? By using only phone number? Like whatsapp?

Q13 How to retrieve the complete list of users on server? Including the ones that are not present in roster.

Q14 How to add VoIP? Which one is better? JitsiLib, Java Bells, Jingle or anyother? Any good tutorials?

Q15 Best way to implement MUC?

Q16 Are there any good books, tutorials, courses out there to get extensive hands on expertise of XMPP client implementation using smack?

Q17 Is my current approach to learn and implement XMPP client good? suggestions?

I’ve a few more questions in my mind, but they might get automatially answered after getting answers to asked questions.
Please don’t respond with “read documentation first” or learn “XMPP/smack basics” etc. I’ve spent almost two months on understanding XMPP, studying docs, XEPs, smack docs, blogs, articles, tutorials etc. Most of the solutions are outdated or are not effective and efficient enough.

Please help me develop this client. I’d be really glad if somebody could help me develop or guide me via Github.

Thanks alot, and once again sorry for long post.

1 Like

Is sending new Presence(Type.unavaiable); to server is the solution to Q1?

Hi!
That is a nice list of questions you got there. I really appreciate your post, because it shows XMPP development from the client developers view, which library developers lack from time to time. Also it shows, that there is need for more documentation.

Some points I can answer on my way to university.

Q1: yes, sending unavailable presence will most likely fix your issue.

Q2: Yes, typically vCards are used for information about the user. There is a vCard implementation in Smack somewhere (smack-extensions or smack-experimental).

Q3: i would assume that the server will tell you if you try to register with a username that is already taken.

Q4: this can probably also be solved using vCards.

Q5: when you create the XMPPConnection, you can set the resource.

Q6: HTTPUpload is way more stable + the file you sent is stored on the server, so all devices of the recipient and you have access to it, while SI (what is done by the FileTransferManager) only sends the file to a single resource. HttpUpload is recommended for that reason. Ejabberd comes with a module for that.

Q7: I think @Flow can answer that the best.

Q8: i would guess that a sticky foreground service is the way to go. You could ask the Conversations developers how they solved it. Keep in mind that they didnt use Smack though. Smack doesnt offer anything to solve this problem, but its also not Smacks responsibility.

Q9: There is a XEP for push notifications. It is really only needed for iOS though. Also you would have to implement and maintain a push application server if you would go that route.
If you make use of battery preserving tools like CSI and StreamManagement (which are available in Smack) you should be good to go without push.

Q10: define “last seen”. " last online" would be trivial to implement based on the online status and the date of when yoh received a unavailable-presence.

Q11: define date of message. Date of sending/receiving?

Q12: you probably dont want to do that security wise. I’m not sure how whatsapp did it, but they are probably not using no password :slight_smile:

Q13: I’m not sure if that is possible with XMPP. You could look into shared roster groups, maybe that will be helpful.

Q15: The current standard is MUC, which you can use via the MultiUserChatManager. There is also MIX, which will be the future, but Smack doesnt support it yet.

Q17: Please keep asking questions, but don’t forget to try to solve issues yourselves :slight_smile: If you find solutions for your problems, please consider to share them with us!

1 Like

Btw:

Take a look at the Roster class in smack-im, more specifically the createEntry() method, which properly adds a contact to the users roster.

1 Like

FYI:

I just yesterday created a PR to add support for XEP-0319: Last User Interaction in Presence, which could suit your use case :slight_smile:

Although my previous solution worked, but this is the correct way.
It worked like a charm, I don’t know how did I miss it.
Thanks for pointing in right direction.
Did you check the rest of the code? There could be more such mistakes.

Thanks for the PR :slight_smile:
I’ve tried using IdleElement after updating dependency smack 4.2.3 but had no success as I was unable to locate IdleElement.java
I’ve even tried smack 4.3.0-beta1 but still no success.
Is it still under concideration? If not, how can I implement to get last activity of any entry from roster?

The PR has not yet been merged. It’ll eventually make it in the next Smack release.

Ohh then I guess, I’ll have to wait a bit
Still thanks

I’m trying to implement HttpFileUpload but having trouble with TLS. Here’s my code:

public static void sendFile(final String path){
    if(httpFileUploadManager.isUploadServiceDiscovered()){

        new AsyncTask<Void, Void, Void>{

            @Override
            protected Void doInBackground(Void... voids){
                try{
                    final String url = httpFileUploadManager.uploadFile(new File(path).toString());
                    //Send the URL as text message to receiver
                    sendMessage(url);
                }
                catch(InterruptedException | XMPPErrorException | SmackException | IOException){
                    printStackTrace() / log()
                }

                return null;
            }
        }.execute();
    }
}

And here’s my server-side (ejabberd) configuration for HttpFileUpload:

listen:
    -
        port:5443
        module: ejabberd_http
        tls: true
        request_handlers:
            "upload": mod_http_upload
    -

modules:
    -
        mod_http_upload:
            docroot: "@HOST@/upload"
            put_url: "https://@HOST@:5443"
            thumbnail: false
    -

# where @HOST@ is the IPv4 address of host

With above code and configuration, I got this exception:

javax.net.ssl.SSLHandshakeException: Connection closed by peer
...

Then I made following config changes on server-side:

listen:
    -
        ...
        tls: false
        ...
    -

modules:
    -
        ... 
        put_url: "http://@HOST@:5443" # replaced https with http
        ...
    -

after these changes when I upload a file, I get this debugger log:

<!--Debugger log-->

D/SMACK: SENT (0): <iq to='upload.domain' id='BFyPq-44' type='get'>
                      <request xmlns='urn:xmpp:http:upload:0' filename='image.png' size='43871' content-type='application/octet-stream'/>
                   </iq>
D/SMACK: RECV (0): <iq xml:lang='en' to='user@domain/resource' from='upload.domain' type='result' id='BFyPq-44'>
                      <slot xmlns='urn:xmpp:http:upload:0;>
                        <get url='url_to_file'/>
                      </slot>
                   </iq>
D/SMACK: RECV (0): <r xmlns='urn:xmpp:sm:3'/>
D/SMACK: SENT (0): <a xmlns='urn:xmpp:sm:3' h='17'/>


and here’s the thrown exception:

//Exception
java.io.IOException: Error response 404 from server during file upload; Not Found, file size: 43871, put URL: Http://@HOST@:5443/file_name
        at org.jivesoftware.smackx.httpfileupload.HttpFileUploadManager.uploadFile(HttpFileUploadManager.java:464)
        ...

what should I do now?
Is this server-side issue? or am I missing something at client side? Please help.

Q13: you can do it using search.

UserSearchManager manager = new UserSearchManager(connection);
        String searchFormString = "search." + connection.getServiceName();

        try {
            DomainBareJid domainBareJid = JidCreate.domainBareFrom(searchFormString);
            Form searchForm = manager.getSearchForm(domainBareJid);
            Form answerForm = searchForm.createAnswerForm();

            UserSearch userSearch = new UserSearch();
            answerForm.setAnswer("Username", true);
            //answerForm.setAnswer("search", "*");//this one is going to fetch all users in OpenFire
            answerForm.setAnswer("search", query);

            ReportedData reportedData = userSearch.sendSearchForm(connection, answerForm, domainBareJid);

            if (reportedData == null) return Resource.success(null);
            List<ReportedData.Row> rows = reportedData.getRows();
            if (rows.isEmpty()) return Resource.success(null);

            List<String> result = new ArrayList<>();
            for (ReportedData.Row row : rows) {
                result.add(getUsername(row));
            }
            return Resource.success(result);
        }
        catch (SmackException.NoResponseException |
                XMPPException.XMPPErrorException |
                SmackException.NotConnectedException |
                InterruptedException |
                XmppStringprepException e) {
            return Resource.error(e.getMessage(), null);
        }

I haven’t implemented a client on my own yet, so unfortunately I’m not sure how to use most extensions :smiley:.

Regarding HttpFileUpload:
I think there is something wrong in your ejabberd config. I believe you mixed upload.domain and domain/upload.

One more thing I learned is disabling all ejabberd’s tls (config changes to all tls: true to false) when working form Local machine