How to implement Multi User Chat(Group Chat) in Android using Smack library

We have developed an app, named CAMighty which is a verified community of CA/CS/CMAs. It discusses real life cases & problems and also get opinions from industry stalwarts and best CA/CS/CMAs.

We had a requirement of providing a session of experts on various topics. Where multiple users can ask questions and get replies from experts. So it’s something like multi user chat or group chat. We explored many libraries for single and multi user chat which communicates with XMPP server quite efficiently. And found very good library which fulfills our requirements i.e. SMACK library.

Smack is a library for communicating with XMPP servers to perform real-time communications, including instant messaging and group chat.

You will not get detailed chat related information regarding SMACK library and their functions on internet. So we tried to explain detailed chat integration and its features in this blog. Hope that it will be helpful. ūüôā

In this blog we will explain how to set up smack library for Group Chat, how to connect/login with XMPP server, how to join a room, how to fetch history of messages exchanged, how to send message in a room and how to listen for incoming messages from other users of room.

1. How to setup Smack library?

a. Add following libraries in app’s build.gradle

[java]

dependencies {
implementation "org.igniterealtime.smack:smack-android-extensions:4.3.1 "
implementation "org.igniterealtime.smack:smack-tcp:4.3.1 "
implementation "org.igniterealtime.smack:smack-experimental:4.3.1 "
}

configurations {
all*.exclude group: ‘xpp3’, module: ‘xpp3’
}
[/java]

b. Add following in Project’s build.gradle

[java]

allprojects {
repositories {
google()
jcenter()
maven {
url ‘https://igniterealtime.org/repo/’
}
}
}
[/java]

2. How to connect/login with XMPP server?

¬†For this we will need port number(eg 5222), domainName (eg “localhost”) which is specific to only login with the XMPP server, hostName(eg 192.168.1.155). All this things will be provided by backend.

 There are many servers available to connect with XMPP i.e OpenFire, ejabberd etc. Here in this app we have used ejabberd server.

To connect with XMPP, it requires userName and password. Username can be anything but should be unique (eg Mobile Number) and password will be given by backend which  is generated by our ejabberd server.

[java]
var smackConnection: AbstractXMPPConnection? = null fun attemptLogin(userName: String, password: String): Single<AbstractXMPPConnection> {

    return Single.create<AbstractXMPPConnection> { source ->

configBuilder = getBaseConfig(userName, password)<strong>
        val hostAddress: InetAddress = InetAddress.getByName(hostName)
        val configuration = configBuilder.setHostAddress(hostAddress).build()
        val connection = XMPPTCPConnection(configuration)

connection.connect()
        if (connection.isConnected) {
            Log.v(LOG_TAG, "-> attemptLogin -> connected")
        } else {
            source.onError(Throwable("Unable to connect"))
        }

        connection.login()
        if (connection.isAuthenticated) {
            Log.v(LOG_TAG, "-> attemptLogin -> ${configuration.username} authenticated")
        } else {
            source.onError(Throwable("Unable to login"))
        }

        //send the available status that is "online" over the server
        connection.sendStanza(Presence(Presence.Type.available))
        this. smackConnection = connection

        val roster = Roster.getInstanceFor(connection)        
//accept_all means anyone can initiate chat and message
        roster.subscriptionMode = Roster.SubscriptionMode.accept_all

        source.onSuccess(connection)
    }
}
[/java]

3. Setting up chat room, join the room and fetch messages.

We need some important classes for group chat and they are as follows:

       i. MultiUserChatManager: A manager for Multi-User Chat rooms.

       ii. MultiUserChat: A MultiUserChat is a conversation that takes place among many users in a virtual room.

       iii. MamManager: A Manager for Message Archive Management.

We will use this variables in all below functions:

[java]

private lateinit var mucJid: EntityBareJid
private var multiUserChatManager: MultiUserChatManager? = null
private lateinit var mamManager: MamManager
private var multiUserChat: MultiUserChat? = null
private lateinit var nickName: Resourcepart
var messageList = MutableLiveData<List<Message>>()
private val tempMessageList = ArrayList<Message>()
private var disposableMessages: Disposable? = null
private lateinit var uid: String
private var doMoreLoading: Boolean = false
private lateinit var username: String
private lateinit var groupName: String
[/java]

Now we will setup room, join the room and we will fetch message history, for that you need to write following function, in that we will initialize above classes.

[java]

private fun initGroupChat() {
initGroupChatRoom(userName)
initMam()
}
[/java]

[java]

private fun initGroupChatRoom(userName: String) {
// xmppServiceGroupDomain is specific for group chat for eg ("muc_localhost")
val xmppServiceGroupDomain: DomainBareJid = JidCreate.domainBareFrom(domainNameForGroupChat)
//create muc jid which is room JID
//groupName is the name of the group which we want to join
   mucJid = JidCreate.entityBareFrom(Localpart.from(groupName), xmppServiceGroupDomain)

//userName can be your name by which you want to join the room
//nickName is the name which will be shown on group chat
   nickName = Resourcepart.from(userName)

   // Get the MultiUserChatManager instance
   multiUserChatManager = MultiUserChatManager.getInstanceFor(smackConnection)
   // Get the multiuserchat instance
   multiUserChat = multiUserChatManager?.getMultiUserChat(mucJid)

   val mucEnterConfiguration = multiUserChat?.getEnterConfigurationBuilder(nickName)!!
            .requestNoHistory()
            .build()

   if (!multiUserChat!!.isJoined) {
        multiUserChat?.join(mucEnterConfiguration)
   }

// For listening incoming message
   multiUserChat?.addMessageListener(incomingMessageListener)
// incomingMessageListener is implemented below.
}
[/java]

[java]

private val incomingMessageListener = object : MessageListener {
    override fun processMessage(message: Message?) {
        if (!TextUtils.isEmpty(message?.body)) {
            addIncomingMessageInRecycler(message!!)
        }
    }
}
[/java]

//There are 3 ways to join the room

  1. Join using nickname
  2. If room is password protected, then join using nickname and provided password.
  3. Join using mucEnterConfiguration.

First two ways provides chat history by default while joining.

All this messages will be fetched one by one in incomingMessageListener which is message listener means if we set the message history limit to 1000, it will trigger incomingMessageListener 1000 times. This is a major drawback of first 2 points this may result in app performance. Because of this one cannot achieve pagination if they want to.

To overcome this we have a 3rd point i.e. Join using mucEnterConfiguration. In this we can set requestNoHistory() to mucEnterConfiguration object, which will prevent triggering of event of messages ‘n’ number of times in incomingMessageListener. (code is written above)

Instead we can query message history with the help of MAM Manager and its methods. Also we can achieve pagination easily.

Following function is used for fetching chat history. Here, we will fetch last 20 messages of chat history.

[java]

private fun initMam() {
// check the connection object
   if (smackConnection != null) {
//get the instance of MamManager
   mamManager = MamManager.getInstanceFor(multiUserChat)
//enable it for fetching messages
   mamManager.enableMamForAllMessages()
// Function for fetching messages
   disposableMessages = getObservableMessages()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ listOfMessages ->
                    tempMessageList = listOfMessages
                }, { t ->
                    Log.e(LOG_TAG, "-> initMam -> onError ->", t)
                }, {
messageList.value = tempMessageList
                    tempList.clear()
                })
    }
}

// number_of_messages_to_fetch it’s a limit of messages to be fetched eg. 20.
private fun getObservableMessages(): Observable<List<Message>> {
    return Observable.create<List<Message>> { source ->
        try {
            val mamQuery = mamManager.queryMostRecentPage(mucJid, number_of_messages_to_fetch)
            if (mamQuery.messageCount == 0 || mamQuery.messageCount < number_of_messages_to_fetch) {
                uid = ""
                doMoreLoading = false
            } else {
                uid = mamQuery.mamResultExtensions[0].id
                doMoreLoading = true
            }
            source.onNext(mamQuery.messages)

        } catch (e: Exception) {
            if (smackConnection?.isConnected == false) {
                source.onError(e)
            } else {
                Log.e("ChatDetail", "Connection closed")
            }
        }
        source.onComplete()
    }
}
[/java]

Here, uid is a message UID. By using this piece of information we can achieve pagination.

MAMManager can be queried by two ways.

  1. mamManager.queryMostRecentPage(Jid, no_of_messages_to_be_fetched)
  2. mamManager.queryArchive(mamQueryArgs).

First method is pretty straight forward here Jid is mucJid.

By using second method we will achieve pagination. At one time we can fetch max 50 messages per page. In this method QueryArgs is required. We will see this in detail below.

[java]

val mamQueryArgs = MamManager.MamQueryArgs.builder()
.limitResultsToJid(mucJid)
.beforeUid(uid)
.build()
[/java]

So it will fetch messages before provided uid.

Now, we will see how to implement pagination in detail.

We are using Recycler View for displaying list of messages.

[java]

private var isUserScrolling = false
private var isListGoingUp = true

recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
if (isUserScrolling) {
isListGoingUp = dy <= 0
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int)  {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
isUserScrolling = true
if (isListGoingUp) {
if (layoutManager.findLastCompletelyVisibleItemPosition() + 1 == recyclerView?.adapter?.itemCount) {
val handler = Handler()
handler.postDelayed({getMoreMessages() }, 50)
}
}
}
}
})
[/java]

getMoreMessages() method will fetch previous 50 messages in each page till the messages exists.

[java]

fun getMoreMessages() {
if (doMoreLoading) {
val mamQueryArgs = MamManager.MamQueryArgs.builder()
.limitResultsToJid(mucJid)
.beforeUid(uid)
.build()

disposableMessages = getObservableMoreMessages(mamQueryArgs)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({listOfMessages ->
tempMessageList = listOfMessages
}, {t ->
Log.v(LOG_TAG, "FailinitmamError")
Log.e(LOG_TAG, "-> initMam -> onError ->", t)
}, {
messageList.value = tempMessageList
tempList.clear()
})
}
}

private fun getObservableMoreMessages(mamQueryArgs: MamManager.MamQueryArgs) : Observable<List<Message>> {
return Observable.create<List<Message>> {source ->
try {
val mamQuery = mamManager.queryArchive(mamQueryArgs)
if (mamQuery.messageCount == 0 || mamQuery.messageCount < 50) {
uid = ""
doMoreLoading = false
} else {
         uid = mamQuery.mamResultExtensions[0].id
doMoreLoading = true
}
source.onNext(mamQuery.messages)
} catch (e: Exception) {
val connection = smackConnection.connection
if (connection?.isConnected == false) {
source.onError(e)
} else {
Log.e("ChatDetail", "Connection closed")
}
}
source.onComplete()
}
}
[/java]

4. Send Message in chat room

[java]

fun sendMessage(messageBody: String) {
val message = Message()
message.body = messageBody
multiUserChat?.sendMessage(message)
}
[/java]

In this way we have completed basic chat functionalities i.e.

  1. Integrating Smack
  2. Login with XMPP server
  3. Setting up chat room
  4. Join room
  5. Fetching messages and implement pagination
  6. Send Message
  7. Listen for live incoming message

If you have any further issues or confusion regarding this, please feel free to comment.

If you have idea of app which require Group chat, Feel free to contact us. We’ll help you translate them into reality.