From e12339ec90abe6a80c09e045481d39cf5efad6ed Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 20:41:07 +0700 Subject: [PATCH 1/8] feat: message listener for server message --- .../simplesocket/ui/client/Client.kt | 10 +++ .../simplesocket/ui/server/Server.kt | 64 +++++++++++++++++++ .../ryccoatika/socketclient/SocketClient.java | 32 ++++++++++ .../socketclient/SocketClientCallback.java | 2 + 4 files changed, 108 insertions(+) diff --git a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/client/Client.kt b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/client/Client.kt index 0ec69ed..1c1c9d9 100644 --- a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/client/Client.kt +++ b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/client/Client.kt @@ -43,6 +43,7 @@ fun Client( var host by remember { mutableStateOf("") } var port by remember { mutableIntStateOf(0) } var message by remember { mutableStateOf("") } + var incomingMessages by remember { mutableStateOf("") } var isConnected by remember { mutableStateOf(false) } var socketClient by remember { mutableStateOf(null) } val socketClientCallback = remember { @@ -58,6 +59,11 @@ fun Client( socketClient = null } + override fun onMessageReceived(message: String) { + Log.i("190401", "onMessageReceived: $message") + incomingMessages = message + } + override fun onDisconnected() { Log.i("190401", "onDisconnected") isConnected = false @@ -140,6 +146,10 @@ fun Client( } Spacer(Modifier.height(30.dp)) if (isConnected) { + if (incomingMessages.isNotEmpty()) { + Text("Incoming message from server:\n$incomingMessages") + } + Spacer(Modifier.height(10.dp)) OutlinedTextField( value = message, label = { diff --git a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt index f254c8d..a2f5212 100644 --- a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt +++ b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt @@ -7,22 +7,33 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ryccoatika.simplesocket.ui.theme.AppTheme @@ -39,6 +50,8 @@ fun Server( val context = LocalContext.current val ipAddress = remember { NetworkUtils.getLocalIpv4Address(context) } val connectedClients = remember { mutableStateListOf() } + var selectedClient by remember { mutableStateOf(null) } + var sendMessage by remember { mutableStateOf("") } val incomingMessages = remember { mutableStateMapOf() } val socketServer = remember { SocketServer(1111) @@ -109,6 +122,57 @@ fun Server( Text(text = "localPort: ${client.localPort}") } Spacer(Modifier.height(10.dp)) + var showDropdown by remember { mutableStateOf(false) } + ExposedDropdownMenuBox( + expanded = showDropdown, + onExpandedChange = { + showDropdown = !showDropdown + } + ) { + TextField( + value = selectedClient?.hostAddress ?: "", + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showDropdown) }, + modifier = Modifier.menuAnchor() + ) + ExposedDropdownMenu( + expanded = showDropdown, + onDismissRequest = { showDropdown = !showDropdown } + ) { + connectedClients.forEach { client -> + DropdownMenuItem( + text = { + Text(client.hostAddress) + }, + onClick = { + selectedClient = client + showDropdown = !showDropdown + } + ) + } + } + } + OutlinedTextField( + value = sendMessage, + label = { + Text(text = "enter message") + }, + onValueChange = { + sendMessage = it + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Send, + ), + keyboardActions = KeyboardActions( + onSend = { + selectedClient?.let { + socketServer.sendMessage(selectedClient, sendMessage) + } + } + ) + ) + Spacer(Modifier.height(10.dp)) Text(text = "Messages:") incomingMessages.keys.forEach { client -> val message = incomingMessages[client] ?: "-" diff --git a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java index 41f5a5c..9ef965a 100644 --- a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java +++ b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java @@ -1,6 +1,8 @@ package com.ryccoatika.socketclient; +import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.net.Socket; import java.util.Objects; @@ -8,6 +10,9 @@ public class SocketClient { private Socket socket; private final String host; private final int port; + + volatile boolean keepProcessing = true; + private SocketClientCallback socketClientCallback; public SocketClient(String host, int port) { @@ -19,6 +24,8 @@ public void connect() { Runnable connectHandler = () -> { try { socket = new Socket(host, port); + + handleIncomingMessageListener(socket); if (Objects.nonNull(socketClientCallback)) { socketClientCallback.onConnected(); } @@ -61,6 +68,31 @@ public void sendMessage(String message) { sendMessageThread.start(); } + private void handleIncomingMessageListener(Socket socket) { + Runnable listenMessageHandler = () -> { + boolean keepProcessing = true; + while (this.keepProcessing && keepProcessing) { + try { + DataInputStream dataInputStream = new DataInputStream(socket.getInputStream()); + String message = dataInputStream.readUTF(); + if (Objects.nonNull(socketClientCallback)) { + socketClientCallback.onMessageReceived(message); + } + } catch (EOFException e) { + if (Objects.nonNull(socketClientCallback)) { + socketClientCallback.onConnectionFailure(e); + } + keepProcessing = false; + } catch (Exception e) { + socketClientCallback.onConnectionFailure(e); + e.printStackTrace(); + } + } + }; + Thread listenMessageThread = new Thread(listenMessageHandler); + listenMessageThread.start(); + } + public void setSocketClientCallback(SocketClientCallback socketClientCallback) { this.socketClientCallback = socketClientCallback; } diff --git a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClientCallback.java b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClientCallback.java index a3c5c96..f946703 100644 --- a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClientCallback.java +++ b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClientCallback.java @@ -7,5 +7,7 @@ public interface SocketClientCallback { void onConnectionFailure(@NonNull Exception e); + void onMessageReceived(@NonNull String message); + void onDisconnected(); } From 40c6afe59130d1d976e0264008a5bbd0d0317128 Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 20:41:55 +0700 Subject: [PATCH 2/8] increment version name --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index fecff7c..8796d1f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.gradle.caching=true org.gradle.parallel=true GROUP=com.ryccoatika.simplesocket -VERSION_NAME=0.6.9 +VERSION_NAME=0.7.0 SONATYPE_HOST=S01 RELEASE_SIGNING_ENABLED=true From 13685a46a479029ce28564b731debfea6d8f9a50 Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 20:43:56 +0700 Subject: [PATCH 3/8] chore: changelog and readme update --- CHANGELOG.md | 5 +++++ README.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bca4d..f8cbaad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.7.0] + +Update: + - New callback on Client for receiving message from Server + ## [0.6.9] Initial release of Simple Socket, including modules for diff --git a/README.md b/README.md index 14b9eef..d8da644 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Contributing Pull requests are welcome. TODO: -- Add callback for server message to client +- Server discovery - Generate mkdocs - Thread safe optimization (have to make sure for thread safe) From e35aa5e98240ab7f509620a09bc06f5f4594c497 Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 21:02:13 +0700 Subject: [PATCH 4/8] chore: removed unused comment --- .../ryccoatika/socketserver/SocketServer.java | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java b/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java index e76cffd..c6ff2a1 100644 --- a/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java +++ b/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java @@ -122,70 +122,3 @@ public String getHostAddress() { return inetAddress.getAddress().getHostAddress(); } } - -/* -* class SocketServer(port: Int = 0) { - private var serverSocket: ServerSocket = ServerSocket(port) - @Volatile - private var keepProcessing = true - - private fun startServer( - - ) { - CoroutineScope(Dispatchers.IO).launch { - withContext(Dispatchers.IO) { - while (true) { - try { - val socket = serverSocket.accept() - listenMessage(socket) - } catch (e: Exception) { - e.printStackTrace() - } - delay(2000) - } - } - } - } - - private fun stopServer() { - - } - - private fun listenMessage(socket: Socket) { - CoroutineScope(Dispatchers.IO).launch { - withContext(Dispatchers.IO) { - try { - val inputStream = DataInputStream(socket.getInputStream()) - while (true) { - if (inputStream.available() > 0) { - incomingMessages.value = Message( - hostAddress = socket.inetAddress.hostAddress ?: "", - message = inputStream.readUTF(), - ) - delay(2000) - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - } - } - - val messages: StateFlow = incomingMessages.asStateFlow() - - val address: String? - get() = serverSocket.inetAddress.hostAddress - - val port: Int - get() = serverSocket.localPort - - fun shutdownServer() { - CoroutineScope(Dispatchers.IO).launch { - withContext(Dispatchers.IO) { - serverSocket.close() - } - } - } -} -* */ \ No newline at end of file From 63a041d49e0aeeb3791be9109333bcdf8ad6a90b Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 21:02:51 +0700 Subject: [PATCH 5/8] chore: handle uncaught exception to stop processing --- .../main/java/com/ryccoatika/socketclient/SocketClient.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java index 9ef965a..3a11ca6 100644 --- a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java +++ b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java @@ -2,7 +2,6 @@ import java.io.DataInputStream; import java.io.DataOutputStream; -import java.io.EOFException; import java.net.Socket; import java.util.Objects; @@ -78,13 +77,11 @@ private void handleIncomingMessageListener(Socket socket) { if (Objects.nonNull(socketClientCallback)) { socketClientCallback.onMessageReceived(message); } - } catch (EOFException e) { + } catch (Exception e) { if (Objects.nonNull(socketClientCallback)) { socketClientCallback.onConnectionFailure(e); } keepProcessing = false; - } catch (Exception e) { - socketClientCallback.onConnectionFailure(e); e.printStackTrace(); } } From d3c493c5e65e47857d79af706b26fdf246617b16 Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 21:03:15 +0700 Subject: [PATCH 6/8] feat: disconnect client --- .../simplesocket/ui/server/Server.kt | 28 +++++++++++++++++-- .../ryccoatika/socketserver/SocketServer.java | 19 +++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt index a2f5212..8feccbf 100644 --- a/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt +++ b/sample-app/src/main/java/com/ryccoatika/simplesocket/ui/server/Server.kt @@ -3,6 +3,7 @@ package com.ryccoatika.simplesocket.ui.server import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -12,6 +13,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox @@ -21,6 +23,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -31,6 +34,7 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.ImeAction @@ -117,9 +121,27 @@ fun Server( Spacer(Modifier.height(10.dp)) Text(text = "Connected Clients:") connectedClients.forEach { client -> - Text(text = "address: ${client.hostAddress}") - Text(text = "port: ${client.port}") - Text(text = "localPort: ${client.localPort}") + Column { + Spacer(Modifier.height(5.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Column { + Text(text = "address: ${client.hostAddress}") + Text(text = "port: ${client.port}") + Text(text = "localPort: ${client.localPort}") + } + TextButton( + onClick = { + socketServer.disconnectClient(client) + } + ) { + Text("Disconnect") + } + } + Spacer(Modifier.height(5.dp)) + Divider() + } } Spacer(Modifier.height(10.dp)) var showDropdown by remember { mutableStateOf(false) } diff --git a/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java b/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java index c6ff2a1..decc966 100644 --- a/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java +++ b/socket-server/src/main/java/com/ryccoatika/socketserver/SocketServer.java @@ -113,6 +113,25 @@ public void sendMessage(Client client, String message) { sendMessageThread.start(); } + public void disconnectClient(Client client) { + Runnable disconnectHandler = () -> { + try { + Socket socket = connectedClients.get(client); + assert socket != null; + socket.shutdownInput(); + socket.shutdownOutput(); + socket.close(); + if (Objects.nonNull(socketServerCallback)) { + socketServerCallback.onClientDisconnected(client); + } + } catch (Exception e) { + e.printStackTrace(); + } + }; + Thread sendMessageThread = new Thread(disconnectHandler); + sendMessageThread.start(); + } + public int getPort() { return serverSocket.getLocalPort(); } From 0c4a6309cfefda1d06dd8356bb0e0eac471540ae Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 21:04:09 +0700 Subject: [PATCH 7/8] chore: update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8cbaad..a245471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## [0.7.0] Update: - - New callback on Client for receiving message from Server + - feat: New callback on Client for receiving message from Server + - feat: Server able to disconnect client ## [0.6.9] From 110637ca2ddac61f50ba6c53df6c8796dff99d99 Mon Sep 17 00:00:00 2001 From: Rycco Atika Date: Sun, 24 Dec 2023 21:04:31 +0700 Subject: [PATCH 8/8] chore: readme update --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d8da644..c7e7df4 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,10 @@ val socketClientCallback = object : SocketClientCallback { override fun onDisconnected() { // called when client call disconnect() or server has gone } + + override fun onMessageReceived(message: String) { + // message received from server + } } socketClient.setSocketClientCallback(socketClientCallback) ```