diff --git a/CHANGELOG.md b/CHANGELOG.md index 81bca4d..a245471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.7.0] + +Update: + - feat: New callback on Client for receiving message from Server + - feat: Server able to disconnect client + ## [0.6.9] Initial release of Simple Socket, including modules for diff --git a/README.md b/README.md index 14b9eef..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) ``` @@ -117,7 +121,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) 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 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..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,26 +3,41 @@ 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 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.Divider +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.TextButton +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.Alignment 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 +54,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) @@ -104,10 +121,79 @@ 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) } + 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 -> 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..3a11ca6 100644 --- a/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java +++ b/socket-client/src/main/java/com/ryccoatika/socketclient/SocketClient.java @@ -1,5 +1,6 @@ package com.ryccoatika.socketclient; +import java.io.DataInputStream; import java.io.DataOutputStream; import java.net.Socket; import java.util.Objects; @@ -8,6 +9,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 +23,8 @@ public void connect() { Runnable connectHandler = () -> { try { socket = new Socket(host, port); + + handleIncomingMessageListener(socket); if (Objects.nonNull(socketClientCallback)) { socketClientCallback.onConnected(); } @@ -61,6 +67,29 @@ 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 (Exception e) { + if (Objects.nonNull(socketClientCallback)) { + socketClientCallback.onConnectionFailure(e); + } + keepProcessing = false; + 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(); } 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..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(); } @@ -122,70 +141,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