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.