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.