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.
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 need | Why | Cost |
|---|---|---|
| Android Studio | To open, edit and build the app | Free |
| A VPS running Ubuntu | Your chat server lives here | About $5/month |
| A domain name | Users connect to it, and calls need SSL | About $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.
- Conversations — group and one-to-one chat. This is the one we use below.
- Xabber Android — feels closest to WhatsApp.
- aTalk Android — the strongest choice if calls matter most.
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:
- 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. - The server address. Open
Config.javaand 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:
| Plugin | What it gives you |
|---|---|
| Registration | Lets users sign up from the app |
| HTTP File Upload | Sending images and files |
| Jingle Nodes | Voice and video calls |
| MUC Service | Group chat |
| Monitoring Service | Message history and stats |
| Avatar Resizer | Profile pictures that do not break layouts |
| Bookmarks | Auto-join a group for every new user |
| Presence Service | Online and offline status |
| STUN Server | Helps calls connect through routers |
| User Status | Records user IP addresses |
| Content Filter | Blocks 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.
- In the Openfire admin panel open Users/Groups → Create New User and make two accounts, for example
aliandsara. - In Android Studio choose New Project → Empty Activity, language Kotlin, minimum SDK API 24.
- Add Smack to
app/build.gradle.ktsand click Sync Now, and add the internet permission to the manifest. - Replace everything in
MainActivity.ktwith the code below and changechat.yourdomain.comto your server. - Run it on two phones (or a phone and an emulator). Log in as
alion one andsaraon 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()
}
}
| Part | What it does |
|---|---|
XMPPTCPConnectionConfiguration | Your server's address, the port (5222) and the login details |
SecurityMode.disabled | Lets 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 |
addIncomingListener | Runs 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:
- Register a new account from inside the app
- Send a text message between two accounts
- Send an image — this proves HTTP File Upload works
- Create a group and send a message in it
- Make a voice call
- 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
| Item | Cost | Note |
|---|---|---|
| VPS (2 GB RAM) | $5–$10 / month | Handles a few thousand users |
| Domain name | $10–$15 / year | Needed for SSL |
| SSL certificate | Free | Let's Encrypt |
| Openfire and the app | Free | Open source |
| Play Store account | $25 once | Only 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:
- Live video chat app (Java): one-to-one and group chat, voice and video calls, five built-in games and an online users list
- Video dating app (Kotlin, Jetpack Compose): profiles, chat and video calls
- Video call chat app (Java): chat, calls and three games
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.