How to Embed Web Server In Android Application

It might sound odd, but you can turn your Android application into a full-blown web server. This will allow your application to accept network connections from external (or internal) clients and establish a bi-directional communication with them. Embedding web servers in Android apps is a niche use case, but, after seeing three different clients using this approach, I’m pretty sure it’s not exceptionally rare either.

In this post I’ll explain the basics of web servers in Android applications and provide a reference implementation that you can use to bootstrap your own efforts.

Web Server Building Blocks

Let’s review the main building blocks of an embedded web server in Android.

First, you’ll need to use ServerSocket class. This class basically realizes server’s functionality and abstracts out most of the underlying complexity from you. To instantiate ServerSocket, you’ll need to specify a specific port that the server will be listening on:

val serverSocket = ServerSocket(50000)

Since ports are involved, you can already guess that this server will become an endpoint on a TCP/IP network.

To make ServerSocket accept connection requests from clients, call its accept() method. This call will block until the next client connects, so don’t call this method on UI thread:

val socket = serverSocket.accept()

The returned Socket object represents the communication channel between the server and the connected client. Since the communication is bi-directional, you can obtain two different streams from the socket:

val inputStream = socket.getInputStream()
val outputStream = socket.getOutputStream()

You can use the resulting InputStream to receive data from the client, and use the corresponding OutputStream to send data back. For example, the following code will accumulate all the incoming data from the client until the end of the stream is reached (which usually means that the client disconnected). Since read() method can block, this code shouldn’t execute on UI thread:

val inputStream = socket.getInputStream()

val buffer = ByteArrayOutputStream()
val bytes = ByteArray(1024)
var read: Int

while (inputStream.read(bytes).also { read = it } != -1) {
    buffer.write(bytes, 0, read)
}

val data = buffer.toString("UTF-8")

As I wrote above, this code will wait for the end of the stream before processing the incoming data. In some cases, you’ll want to establish a more granular communication protocol to receive “messages” from clients in “real time”. I’ll show you one simple example of such a protocol later in this post.

Android Web Server Implementation

Even though the building blocks described above are relatively straightforward, implementing a robust web server can still be a challenging task. For example, since you can’t call some of the aforementioned methods on the UI thread, you’ll most probably end up writing multithreaded code. To get you started with this project, below I show one relatively simple implementation that you can use in your applications right away, or modify to fit your specific requirements.

This Server class is an abstraction that supports a single connected client at any instant (it needs to be restarted after a client disconnects) and allows for bi-directional communication using string messages. The format of a message is basically a line of text (i.e. text terminated with a newline character). This abstraction handles multithreading internally using a bare Thread class and delivers notifications to registered listeners on the UI thread.

import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.annotation.WorkerThread
import java.io.*
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

class Server {
    private val lock = ReentrantLock()

    private val uiHandler = Handler(Looper.getMainLooper())

    private val listeners = Collections.newSetFromMap(ConcurrentHashMap<ServerListener, Boolean>(1))

    private val isServerRunning = AtomicBoolean(false)
    private val isClientConnected = AtomicBoolean(false)

    private var serverSocket: ServerSocket? = null
    private var clientSocket: Socket? = null

    fun registerListener(listener: ServerListener) {
        listeners.add(listener)
    }

    fun unregisterListener(listener: ServerListener) {
        listeners.remove(listener)
    }

    fun startServer(port: Int) {
        if (!isServerRunning.compareAndSet(false, true)) {
            Log.i(TAG, "start server aborted: server already running")
            return
        }
        Thread { startServerSync(port) }.start()
    }

    fun stopServer() {
        shutdownServer()
        notifyServerStopped()
    }

    @WorkerThread
    private fun startServerSync(port: Int) {
        try {
            val localServerSocket = ServerSocket(port)
            lock.withLock {
                serverSocket = localServerSocket
            }

            notifyServerStarted(localServerSocket)

            val localClientSocket = waitForClientConnectionSync()
            lock.withLock {
                clientSocket = localClientSocket
            }

            isClientConnected.set(true)

            notifyClientConnected()

            waitForDataFromSocketAndNotify(localClientSocket)
        } catch (e: IOException) {
            if (isServerRunning.get()) {
                notifyServerError(e.message ?: "")
            }
            shutdownServer()
        }
    }

    @WorkerThread
    @Throws(IOException::class)
    private fun waitForClientConnectionSync(): Socket {
        val localServerSocket: ServerSocket = lock.withLock {
            serverSocket!!
        }

        return localServerSocket.accept() // will block until client connects
    }

    @WorkerThread
    private fun waitForDataFromSocketAndNotify(socket: Socket) {
        try {
            val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
            var line: String? = null
            while (reader.readLine().also { line = it  } != null) {
                notifyMessageFromClient(line!!)
            }
        } catch (e: IOException) {
            if (isServerRunning.get()) {
                notifyClientDisconnected()
            }
        } finally {
            stopServer()
        }
    }

    fun sendMessageToConnectedClient(message: String) {
        Thread {
            var localClientSocket: Socket?
            lock.withLock { localClientSocket = clientSocket }
            if (!isClientConnected.get()) {
                Log.i(TAG, "message to client aborted: no connected client")
                return@Thread
            }
            try {
                val writer = PrintWriter(localClientSocket!!.getOutputStream())
                writer.println(message)
                writer.flush()
            } catch (e: IOException) {
                notifyServerError(e.message ?: "")
            }
        }.start()
    }

    private fun shutdownServer() {
        isServerRunning.set(false)
        isClientConnected.set(false)
        lock.withLock {
            try {
                serverSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Failed to close the server socket", e)
            }
            try {
                clientSocket?.close()
            } catch (e: IOException) {
                Log.e(TAG, "Failed to close the socket", e)
            }
        }
    }

    private fun getDeviceIpAddress(): String {
        try {
            val networkInterfaces = NetworkInterface.getNetworkInterfaces()
            while (networkInterfaces.hasMoreElements()) {
                val networkInterface = networkInterfaces.nextElement()
                val inetAddresses = networkInterface.inetAddresses
                while (inetAddresses.hasMoreElements()) {
                    val inetAddress = inetAddresses.nextElement()
                    if (inetAddress.isSiteLocalAddress) {
                        return inetAddress.hostAddress
                    }
                }
            }
        } catch (e: SocketException) {
            return ""
        }
        return ""
    }

    private fun notifyServerStarted(serverSocket: ServerSocket) {
        uiHandler.post {
            val ip = deviceIpAddress()
            val port = serverSocket.localPort
            listeners.forEach {
                it.onServerStarted(ip, port)
            }
        }
    }

    private fun notifyServerStopped() {
        uiHandler.post {
            listeners.forEach {
                it.onServerStopped()
            }
        }
    }

    private fun notifyClientConnected() {
        uiHandler.post {
            listeners.forEach {
                it.onClientConnected()
            }
        }
    }

    private fun notifyClientDisconnected() {
        uiHandler.post {
            listeners.forEach {
                it.onClientDisconnected()
            }
        }
    }

    private fun notifyMessageFromClient(messageFromClient: String) {
        uiHandler.post {
            listeners.forEach {
                it.onMessage(messageFromClient)
            }
        }
    }

    private fun notifyServerError(message: String) {
        uiHandler.post {
            listeners.forEach {
                it.onError(message)
            }
        }
    }

    companion object {
        private const val TAG = "AndroidServer"
    }
}

I tried to make this class thread-safe, but, as students enrolled in my multithreading course know very well, concurrency is tricky. Therefore, if you spot any concurrency bugs here, please let me know in a comment.

Also note getDeviceIpAddress() method which returns the external IP of your Android device. This means that onServerStarted(ip, port) callback carries all the information required to connect to your embedded server from outside.

Integrating Web Server with Presentation Layer Logic

Server abstraction that I described above hides all the irrelevant details under the hood and exposes a clean API that other components can use.

For example, this simple Fragment uses Server to communicate with a connected client and “logs” the information from Server’s callbacks:

class ServerFragment: Fragment(), ServerListener {

    val server = Server()

    private lateinit var binding: LayoutWebserverBinding
    private lateinit var btnStartServer: Button
    private lateinit var btnStopServer: Button
    private lateinit var btnSendMessage: Button
    private lateinit var txtServerLog: TextView

    private var counter = 0

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        binding = LayoutWebserverBinding.inflate(inflater, container, false)
        btnStartServer = binding.btnStartServer
        btnStopServer = binding.btnStopServer
        btnSendMessage = binding.btnSendMessage
        txtServerLog = binding.txtServerLog

        btnStartServer.setOnClickListener {
            server.startServer(50000)
        }

        btnStopServer.setOnClickListener {
            server.stopServer()
        }

        btnSendMessage.setOnClickListener {
            server.sendMessageToConnectedClient("Server message #${++counter}")
        }
        return binding.root
    }

    override fun onStart() {
        super.onStart()
        server.registerListener(this)
    }

    override fun onStop() {
        super.onStop()
        server.stopServer()

        server.unregisterListener(this)
    }

    override fun onServerStarted(ip: String, port: Int) {
        addServerLog("\n[Server started] $ip:$port")
        btnStartServer.isEnabled = false
        btnStopServer.isEnabled = true
        btnSendMessage.isEnabled = true
    }

    override fun onServerStopped() {
        addServerLog("\n[Server stopped]")
        btnStartServer.isEnabled = true
        btnStopServer.isEnabled = false
        btnSendMessage.isEnabled = false
    }

    override fun onMessage(message: String) {
        addServerLog("\n[Message from client] $message")
    }

    override fun onError(message: String) {
        addServerLog("\n[Error] $message")
    }

    override fun onClientConnected() {
        addServerLog("\n[Client connected]")
    }

    override fun onClientDisconnected() {
        addServerLog("\n[Client disconnected]")
    }

    private fun addServerLog(message: String) {
        txtServerLog.text = txtServerLog.text.toString() + message
    }

}

Conclusion

As I said at the beginning of this article, embedding web servers in Android applications is not a common use case. However, this approach can come in handy at times.

So, if that’s what you need to implement in your own Android project, I hope you found this article helpful.

Check out my premium

Android Development Courses

Leave a Comment