Android Apps

How to Create a Chat App in Android Studio

Build a working Android chat app with one-to-one chat, group chat, file sharing and video calls. Uses a free open-source app and your own Openfire server, so you own the data.

Android chat app connected to an Openfire XMPP server

You can build a working chat app for Android without writing a messaging engine from scratch. This guide shows you how: you start from a free open-source app, connect it to your own chat server, and end up with something that sends messages, files and video calls.

Everything here is free except the server, which costs about $5 a month. You will need some patience — this is not a 20-minute project — but every step is written out.

What you will build

By the end you will have an Android app that does all of this:

  • One-to-one chat — like WhatsApp
  • Group chat — create groups and invite people
  • Voice and video calls
  • File and image sending
  • Online status and presence — see who is available
  • Block and ban users — by account or by IP address

You will also get an admin panel in your browser where you can see every user, read delivery stats and ban troublemakers.

Before you start

Three things need to be ready. Do not skip this part — most people who get stuck later are missing one of them.

What you needWhyCost
Android StudioTo open, edit and build the appFree
A VPS running UbuntuYour chat server lives hereAbout $5/month
A domain nameUsers connect to it, and calls need SSLAbout $10/year

You also need to be comfortable typing commands into a black terminal window. If you have never done that, read how to install a LAMP server on Ubuntu first — it is a gentler start.

Why not just use Firebase?

This is the question almost everyone asks, so let us answer it before you spend a weekend on the wrong path.

Firebase is easier. Google runs the server for you, and you can have a basic chat working in an afternoon. If you only want one-to-one text messages and you are learning, start with Firebase.

XMPP is what we use here, and it is harder to set up. It is worth it when you want three things Firebase makes difficult: you own the data and it sits on your server, voice and video calls are part of the standard, and your costs do not climb as users grow. A $5 server handles thousands of users.

Pick based on what you actually need, not on which sounds more impressive.

Step 1: Pick a base app

You do not need to write the app yourself. Several excellent XMPP chat apps are open source under the GNU General Public License v3.0, which means you can change them and use them commercially.

One thing to understand about the licence: GPL v3 lets you sell your app, but if you distribute it you must also make your changed source code available. If that does not suit your plan, check the licence before you build a business on it.

Step 2: Open the project in Android Studio

Copy the clone link from the GitHub page. In Android Studio choose File → New → Project from Version Control, paste the link, and let it download.

The first build takes a while. Gradle downloads a lot on the first run — leave it alone until it finishes.

Then change two things:

  1. The package name. Right-click the package in the project tree, choose Refactor → Rename, and give it your own name, for example com.yourname.chat. Two apps cannot share a package name on the Play Store.
  2. The server address. Open Config.java and replace the default domain with your own. If you skip this, your app connects to somebody else's server.

Step 3: Install Openfire on your server

Openfire is the chat server. It handles accounts, delivers messages and connects calls. Without it the app has nothing to talk to.

Follow how to set up an Openfire XMPP server on Ubuntu — it covers the install, the database and the first login. Come back here when the admin panel opens in your browser.

Step 4: Install the Openfire plugins

A fresh Openfire only sends plain text. Features like file sending and video calls come from plugins.

Open the admin panel, go to the Plugins tab, and install these:

PluginWhat it gives you
RegistrationLets users sign up from the app
HTTP File UploadSending images and files
Jingle NodesVoice and video calls
MUC ServiceGroup chat
Monitoring ServiceMessage history and stats
Avatar ResizerProfile pictures that do not break layouts
BookmarksAuto-join a group for every new user
Presence ServiceOnline and offline status
STUN ServerHelps calls connect through routers
User StatusRecords user IP addresses
Content FilterBlocks bad words

Install them one at a time and wait for each to finish. Installing several at once sometimes leaves one half-registered.

Test your server with a one-page chat app

Before you open the big open-source project, it helps to see a chat app that you understand line by line. Here is a complete one in a single Kotlin file: you log in, type the username of a second account, and send messages both ways. It uses Smack, the most used Java library for XMPP, and talks to the Openfire server you just installed.

  1. In the Openfire admin panel open Users/Groups → Create New User and make two accounts, for example ali and sara.
  2. In Android Studio choose New Project → Empty Activity, language Kotlin, minimum SDK API 24.
  3. Add Smack to app/build.gradle.kts and click Sync Now, and add the internet permission to the manifest.
  4. Replace everything in MainActivity.kt with the code below and change chat.yourdomain.com to your server.
  5. Run it on two phones (or a phone and an emulator). Log in as ali on one and sara on the other, put the other person's username in “Chat with”, and send.
dependencies {
    implementation("org.igniterealtime.smack:smack-android:4.4.8")
    implementation("org.igniterealtime.smack:smack-tcp:4.4.8")
    implementation("org.igniterealtime.smack:smack-extensions:4.4.8")
}

// Android already has an XML parser; these old copies clash with it
configurations.all {
    exclude(group = "xpp3", module = "xpp3")
    exclude(group = "xpp3", module = "xpp3_min")
}
<!-- app/src/main/AndroidManifest.xml, above <application> -->
<uses-permission android:name="android.permission.INTERNET" />
package com.example.mychat   // keep the package line Android Studio made for you

import android.os.Bundle
import android.text.InputType
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import org.jivesoftware.smack.ConnectionConfiguration
import org.jivesoftware.smack.chat2.ChatManager
import org.jivesoftware.smack.tcp.XMPPTCPConnection
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration
import org.jxmpp.jid.impl.JidCreate
import kotlin.concurrent.thread

class MainActivity : ComponentActivity() {

    private val serverDomain = "chat.yourdomain.com"   // your Openfire server
    private var connection: XMPPTCPConnection? = null
    private lateinit var log: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val username = EditText(this).apply { hint = "Your username" }
        val password = EditText(this).apply {
            hint = "Password"
            inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
        }
        val loginButton = Button(this).apply { text = "Log in" }
        val chatWith = EditText(this).apply { hint = "Chat with (their username)" }
        val message = EditText(this).apply { hint = "Message" }
        val sendButton = Button(this).apply { text = "Send" }
        log = TextView(this).apply { textSize = 16f }

        val screen = LinearLayout(this).apply {
            orientation = LinearLayout.VERTICAL
            listOf(username, password, loginButton, chatWith, message, sendButton).forEach { addView(it) }
            addView(ScrollView(context).apply { addView(log) })
        }
        setContentView(screen)

        // Keep everything clear of the status bar and the navigation bar (Android 15 and newer)
        ViewCompat.setOnApplyWindowInsetsListener(screen) { view, insets ->
            val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            view.setPadding(32, bars.top + 32, 32, bars.bottom + 32)
            insets
        }

        loginButton.setOnClickListener { logIn(username.text.toString().trim(), password.text.toString()) }
        sendButton.setOnClickListener {
            val text = message.text.toString()
            if (text.isNotBlank()) {
                send(chatWith.text.toString().trim(), text)
                message.setText("")
            }
        }
    }

    private fun logIn(user: String, pass: String) {
        show("Connecting ...")
        thread {   // network work never runs on the main thread
            try {
                val config = XMPPTCPConnectionConfiguration.builder()
                    .setXmppDomain(serverDomain)
                    .setHost(serverDomain)
                    .setPort(5222)
                    .setUsernameAndPassword(user, pass)
                    // First test only: a fresh Openfire has a self-signed certificate.
                    // Switch to SecurityMode.required once the server has a real SSL certificate.
                    .setSecurityMode(ConnectionConfiguration.SecurityMode.disabled)
                    .build()
                val conn = XMPPTCPConnection(config)
                conn.connect().login()
                connection = conn

                // Every message that arrives for this user
                ChatManager.getInstanceFor(conn).addIncomingListener { from, msg, _ ->
                    show("${from.localpart}: ${msg.body}")
                }
                show("Logged in as $user")
            } catch (e: Exception) {
                show("Could not log in: ${e.message}")
            }
        }
    }

    private fun send(toUser: String, text: String) {
        val conn = connection ?: return show("Log in first")
        thread {
            try {
                val jid = JidCreate.entityBareFrom("$toUser@$serverDomain")
                ChatManager.getInstanceFor(conn).chatWith(jid).send(text)
                show("me: $text")
            } catch (e: Exception) {
                show("Not sent: ${e.message}")
            }
        }
    }

    private fun show(line: String) = runOnUiThread { log.append(line + "\n") }

    override fun onDestroy() {
        connection?.disconnect()
        super.onDestroy()
    }
}
PartWhat it does
XMPPTCPConnectionConfigurationYour server's address, the port (5222) and the login details
SecurityMode.disabledLets the first test work with the self-signed certificate of a fresh Openfire. Change it to required as soon as the server has a real SSL certificate (Step 6), because without it passwords travel unencrypted.
connect().login()Opens the connection and signs in, on a background thread
addIncomingListenerRuns for every message that arrives and adds it to the screen
chatWith(jid).send()Sends a message to username@yourserver

If both phones show each other's messages, your server works and you understand the core of every XMPP chat app. Everything a real chat app adds sits on top of this: a contact list, message history, images and files, groups, calls, notifications when the app is closed, and a reconnect when the network changes. That is what the open-source app and the next steps give you.

Step 5: Set the Jingle Nodes ports

Jingle Nodes is what carries the audio and video. It needs its ports set, otherwise calls ring and then die.

Go to Server → Server Settings → Jingle Nodes in the admin panel and set the ports there.

For most people this is enough and calls start working. If they still fail, the problem is usually that one caller is behind a strict router — and that is what the next step fixes.

Step 6: Install a TURN server for reliable calls

A TURN server relays call traffic when two phones cannot reach each other directly. On mobile networks, that is often.

Connect to your Ubuntu server and run:

sudo apt-get update
sudo apt-get install coturn

Turn it on:

sudo sed -i '/TURNSERVER_ENABLED/c\TURNSERVER_ENABLED=1' /etc/default/coturn

Open the configuration file:

sudo nano /etc/turnserver.conf

Put these settings in. Replace the IP addresses with your server IP, and the realm with your domain:

listening-port=3478
tls-listening-port=5349
listening-ip=YOUR.SERVER.IP.HERE
relay-ip=YOUR.SERVER.IP.HERE

fingerprint
lt-cred-mech
realm=yourdomain.com
total-quota=100
stale-nonce

cert=/ssl/nws.crt
pkey=/ssl/nws.pem

cipher-list="ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AES:RSA+3DES:!ADH:!AECDH:!MD5"

no-loopback-peers
no-multicast-peers

Save with Ctrl+O, exit with Ctrl+X, then restart:

sudo service coturn restart

The cert and pkey lines point to your SSL certificate. If you do not have one yet, follow how to install a free SSL certificate on Ubuntu — calls will not work over TLS without it.

Step 7: Test it properly

Build the app and install it on a real phone, not only the emulator. The emulator handles calls badly and will make you think something is broken when it is not.

Test in this order, because each step depends on the one before:

  1. Register a new account from inside the app
  2. Send a text message between two accounts
  3. Send an image — this proves HTTP File Upload works
  4. Create a group and send a message in it
  5. Make a voice call
  6. Make a video call, with the two phones on different networks — one on Wi-Fi, one on mobile data

That last point matters. Calls between two phones on the same Wi-Fi almost always work. The real test is across networks, because that is when TURN is doing its job.

Common problems and how to fix them

The app will not connect to the server

Check the domain in Config.java first. Then check that ports 5222 and 5223 are open in your firewall. Run sudo ufw status to see.

Messages send but images fail

The HTTP File Upload plugin is missing or its domain is wrong. In the admin panel, check that the upload service points at your real domain and that the file size limit is not tiny.

Calls ring but never connect

This is almost always TURN. Confirm coturn is running with sudo service coturn status, and confirm ports 3478 and 5349 are open.

Calls work on Wi-Fi but not on mobile data

Same cause, narrower. Your relay-ip in turnserver.conf must be the server public IP, not a local one like 10.x.x.x.

Gradle build fails after cloning

Usually an SDK version mismatch. Open Tools → SDK Manager and install the SDK version the project asks for, rather than forcing the project to use yours.

What this costs to run

ItemCostNote
VPS (2 GB RAM)$5–$10 / monthHandles a few thousand users
Domain name$10–$15 / yearNeeded for SSL
SSL certificateFreeLet's Encrypt
Openfire and the appFreeOpen source
Play Store account$25 onceOnly if you publish

Compare that with a hosted chat service, where the same user count usually costs more per month than this does per year.

Skip the build: ready chat app source codes

If you want a finished app with your own name on it, our chat app source codes come with the server already worked out. They run on ejabberd 24.12, and each one includes a ready server folder and a step-by-step guide for ejabberd and the TURN server:

The server setup they use is written up in how to install ejabberd and a TURN server on Ubuntu.

Where to go next

Once chat works, the usual next steps are push notifications so messages arrive when the app is closed, a custom theme so it stops looking like the original, and end-to-end encryption if you need it.

If you want a chat you can use from a browser instead of a phone, see how to make your own chat room website for free.

Questions people ask

Can I make a chat app in Android Studio without coding?
Not entirely, but you do not need to write the messaging engine. You start from an open-source app like Conversations, change the package name and the server address, and build it. You need enough Android Studio knowledge to open a project, rename a package and produce an APK.
Should I use Firebase or XMPP for an Android chat app?
Use Firebase if you want one-to-one text chat working quickly and you do not mind Google hosting the data. Use XMPP with Openfire if you want to own the data, need voice and video calls built in, or want predictable costs as users grow. A $5 server running XMPP handles thousands of users.
How much does it cost to run a chat app?
About $5 to $10 a month for a VPS, plus $10 to $15 a year for a domain. The SSL certificate is free with Let's Encrypt, and both Openfire and the app are open source. Publishing on the Play Store costs $25 once.
Do I need a VPS, or can I use shared hosting?
You need a VPS. Openfire is a Java server that runs continuously and needs specific ports open. Shared hosting does not allow either, so it cannot run a chat server.
Why do my video calls ring but never connect?
Almost always the TURN server. Install coturn, set relay-ip to your server public IP, and open ports 3478 and 5349 in the firewall. Calls between two phones on the same Wi-Fi can work without TURN, which is why the problem often only shows up on mobile data.
Can I publish this chat app on the Play Store and sell it?
Yes. The base apps are GPL v3, which permits commercial use. The condition is that if you distribute your version, you must also make your modified source code available. Read the licence before building a paid product on it.
How many users can one server handle?
A VPS with 2 GB of RAM comfortably handles a few thousand connected users for text chat. Video calls need much more, because the TURN server relays the media. Budget more memory and bandwidth if calls will be common.
Which is the best open-source XMPP app to start from?
Conversations is the most actively maintained and the best starting point for text and group chat. Xabber feels closest to WhatsApp. aTalk has the strongest voice and video support. Pick based on the feature that matters most to you.
Does the app work if the phone is offline?
Messages sent to an offline user are stored by Openfire and delivered when they reconnect. For alerts while the app is closed you need push notifications, which are a separate step and not included in the base app.
How long does this take to set up?
Plan a full weekend if you have not run a server before. The Android side takes an hour or two. Most of the time goes on the server: installing Openfire, adding plugins, setting up SSL and getting calls to connect reliably.
What is the simplest code for an Android chat app?
One Kotlin file with the Smack XMPP library: build an XMPPTCPConnectionConfiguration with your server, call connect() and login() on a background thread, listen with ChatManager.addIncomingListener, and send with chatWith(jid).send(). The one-page app in this guide does exactly that against an Openfire server.
Why does my one-page chat app say Could not log in?
Check that the account exists in Openfire, that port 5222 is open on the server, and that the domain in the app is the XMPP domain Openfire was set up with. On a fresh Openfire with a self-signed certificate, the test needs SecurityMode.disabled; with a real SSL certificate use SecurityMode.required.

Written by Habib Baloch

I build Android apps, websites and Ubuntu servers, and write down exactly how I did it.

Ask me about this guide →