From 6f9294cc500a196277237bcb3bea288a2053f8eb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:20:33 +0000 Subject: [PATCH 1/2] feat: Implement lightweight WebSocket server and receiver service Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 4 + .../librecast/receiver/LibreCastServer.java | 125 +++++++++ .../org/librecast/receiver/MainActivity.java | 73 +++++- .../org/librecast/receiver/NetworkUtils.java | 68 +++++ .../librecast/receiver/ReceiverService.java | 230 +++++++++++++++++ app/src/main/res/layout/activity_main.xml | 13 +- app/src/main/res/values/strings.xml | 5 + .../receiver/LibreCastServerTest.java | 243 ++++++++++++++++++ .../librecast/receiver/NetworkUtilsTest.java | 20 ++ 10 files changed, 781 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/librecast/receiver/LibreCastServer.java create mode 100644 app/src/main/java/org/librecast/receiver/NetworkUtils.java create mode 100644 app/src/main/java/org/librecast/receiver/ReceiverService.java create mode 100644 app/src/test/java/org/librecast/receiver/LibreCastServerTest.java create mode 100644 app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java diff --git a/app/build.gradle b/app/build.gradle index 7ddc97a..99cb2f2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,4 +31,7 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.core:core:1.6.0' + implementation 'org.java-websocket:Java-WebSocket:1.5.4' + + testImplementation 'junit:junit:4.13.2' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6616d58..f2ff5f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,10 @@ + + diff --git a/app/src/main/java/org/librecast/receiver/LibreCastServer.java b/app/src/main/java/org/librecast/receiver/LibreCastServer.java new file mode 100644 index 0000000..77231a9 --- /dev/null +++ b/app/src/main/java/org/librecast/receiver/LibreCastServer.java @@ -0,0 +1,125 @@ +package org.librecast.receiver; + +import org.java_websocket.WebSocket; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +public class LibreCastServer extends WebSocketServer { + + public interface ServerEventListener { + void onServerStarted(int port); + void onServerStopped(); + void onClientConnected(String clientIp); + void onClientDisconnected(String clientIp); + void onVideoChunkReceived(ByteBuffer chunk); + void onError(Exception ex); + } + + public static final int DEFAULT_PORT = 8080; + + private ServerEventListener listener; + private WebSocket activeConnection; + + public LibreCastServer(int port) { + super(new InetSocketAddress(port)); + setReuseAddr(true); + } + + public LibreCastServer(int port, ServerEventListener listener) { + this(port); + this.listener = listener; + } + + public void setListener(ServerEventListener listener) { + this.listener = listener; + } + + public synchronized WebSocket getActiveConnection() { + return activeConnection; + } + + @Override + public synchronized void onOpen(WebSocket conn, ClientHandshake handshake) { + if (activeConnection != null && activeConnection != conn && activeConnection.isOpen()) { + try { + activeConnection.close(1000, "Replaced by new client connection"); + } catch (Exception ignored) { + } + } + activeConnection = conn; + String clientIp = getClientIp(conn); + if (listener != null) { + listener.onClientConnected(clientIp); + } + } + + @Override + public synchronized void onClose(WebSocket conn, int code, String reason, boolean remote) { + if (conn == activeConnection) { + String clientIp = getClientIp(conn); + activeConnection = null; + if (listener != null) { + listener.onClientDisconnected(clientIp); + } + } + } + + @Override + public void onMessage(WebSocket conn, String message) { + // Text messages handled or ignored + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer message) { + WebSocket currentActive; + synchronized (this) { + currentActive = activeConnection; + } + if (conn != null && conn == currentActive) { + if (listener != null) { + listener.onVideoChunkReceived(message); + } + } + } + + @Override + public void onError(WebSocket conn, Exception ex) { + if (listener != null) { + listener.onError(ex); + } + } + + @Override + public void onStart() { + if (listener != null) { + listener.onServerStarted(getPort()); + } + } + + @Override + public void stop() throws InterruptedException { + synchronized (this) { + if (activeConnection != null) { + try { + activeConnection.close(1000, "Server stopping"); + } catch (Exception ignored) { + } + activeConnection = null; + } + } + super.stop(); + if (listener != null) { + listener.onServerStopped(); + } + } + + private String getClientIp(WebSocket conn) { + if (conn != null && conn.getRemoteSocketAddress() != null && conn.getRemoteSocketAddress().getAddress() != null) { + return conn.getRemoteSocketAddress().getAddress().getHostAddress(); + } + return "Unknown"; + } +} diff --git a/app/src/main/java/org/librecast/receiver/MainActivity.java b/app/src/main/java/org/librecast/receiver/MainActivity.java index 0048e8c..228760a 100644 --- a/app/src/main/java/org/librecast/receiver/MainActivity.java +++ b/app/src/main/java/org/librecast/receiver/MainActivity.java @@ -1,22 +1,64 @@ package org.librecast.receiver; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; import android.view.View; import android.view.WindowManager; +import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { + private TextView textServerStatus; + private TextView textConnectionStatus; + + private final BroadcastReceiver statusReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent != null && ReceiverService.ACTION_STATUS_UPDATE.equals(intent.getAction())) { + updateUI(intent); + } + } + }; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Keep screen on for TV display and set full screen getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - hideSystemUI(); setContentView(R.layout.activity_main); + + textServerStatus = findViewById(R.id.text_server_status); + textConnectionStatus = findViewById(R.id.text_connection_status); + + startReceiverService(); + } + + @Override + protected void onStart() { + super.onStart(); + IntentFilter filter = new IntentFilter(ReceiverService.ACTION_STATUS_UPDATE); + registerReceiver(statusReceiver, filter); + + // Request current status + Intent queryIntent = new Intent(this, ReceiverService.class); + queryIntent.setAction(ReceiverService.ACTION_QUERY_STATUS); + startService(queryIntent); + } + + @Override + protected void onStop() { + super.onStop(); + try { + unregisterReceiver(statusReceiver); + } catch (Exception ignored) { + } } @Override @@ -25,6 +67,35 @@ protected void onResume() { hideSystemUI(); } + private void startReceiverService() { + Intent serviceIntent = new Intent(this, ReceiverService.class); + serviceIntent.setAction(ReceiverService.ACTION_START_SERVER); + startService(serviceIntent); + } + + private void updateUI(Intent intent) { + boolean isRunning = intent.getBooleanExtra(ReceiverService.EXTRA_IS_RUNNING, false); + String serverIp = intent.getStringExtra(ReceiverService.EXTRA_SERVER_IP); + int serverPort = intent.getIntExtra(ReceiverService.EXTRA_SERVER_PORT, LibreCastServer.DEFAULT_PORT); + boolean isClientConnected = intent.getBooleanExtra(ReceiverService.EXTRA_IS_CLIENT_CONNECTED, false); + String clientIp = intent.getStringExtra(ReceiverService.EXTRA_CLIENT_IP); + + if (isRunning) { + if (serverIp == null || serverIp.isEmpty() || "0.0.0.0".equals(serverIp)) { + serverIp = NetworkUtils.getLocalIpAddress(this); + } + textServerStatus.setText(getString(R.string.server_ready_format, serverIp, serverPort)); + } else { + textServerStatus.setText(getString(R.string.server_stopped)); + } + + if (isClientConnected) { + textConnectionStatus.setText(getString(R.string.client_connected_format, clientIp != null ? clientIp : "")); + } else { + textConnectionStatus.setText(getString(R.string.waiting_for_connection)); + } + } + private void hideSystemUI() { View decorView = getWindow().getDecorView(); decorView.setSystemUiVisibility( diff --git a/app/src/main/java/org/librecast/receiver/NetworkUtils.java b/app/src/main/java/org/librecast/receiver/NetworkUtils.java new file mode 100644 index 0000000..2f07fe4 --- /dev/null +++ b/app/src/main/java/org/librecast/receiver/NetworkUtils.java @@ -0,0 +1,68 @@ +package org.librecast.receiver; + +import android.content.Context; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.Locale; + +public class NetworkUtils { + + /** + * Helper utility to get the device's local Wi-Fi or local network IPv4 address. + * Falls back to NetworkInterface enumeration if WifiManager returns 0.0.0.0. + */ + public static String getLocalIpAddress(Context context) { + if (context != null) { + try { + WifiManager wifiManager = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + if (wifiManager != null && wifiManager.isWifiEnabled()) { + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo != null) { + int ipAddress = wifiInfo.getIpAddress(); + if (ipAddress != 0) { + return String.format(Locale.US, "%d.%d.%d.%d", + (ipAddress & 0xff), + (ipAddress >> 8 & 0xff), + (ipAddress >> 16 & 0xff), + (ipAddress >> 24 & 0xff)); + } + } + } + } catch (Exception ignored) { + } + } + + return getNetworkInterfaceIpAddress(); + } + + /** + * Resolves local IPv4 address by checking network interfaces (Ethernet / Wi-Fi fallback). + */ + public static String getNetworkInterfaceIpAddress() { + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + NetworkInterface netInterface = interfaces.nextElement(); + if (netInterface.isLoopback() || !netInterface.isUp()) { + continue; + } + Enumeration addresses = netInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) { + return addr.getHostAddress(); + } + } + } + } + } catch (Exception ignored) { + } + return "0.0.0.0"; + } +} diff --git a/app/src/main/java/org/librecast/receiver/ReceiverService.java b/app/src/main/java/org/librecast/receiver/ReceiverService.java new file mode 100644 index 0000000..fdb0e00 --- /dev/null +++ b/app/src/main/java/org/librecast/receiver/ReceiverService.java @@ -0,0 +1,230 @@ +package org.librecast.receiver; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.net.wifi.WifiManager; +import android.os.Binder; +import android.os.IBinder; +import android.os.PowerManager; +import android.util.Log; + +import java.nio.ByteBuffer; + +public class ReceiverService extends Service implements LibreCastServer.ServerEventListener { + + private static final String TAG = "ReceiverService"; + + public static final String ACTION_STATUS_UPDATE = "org.librecast.receiver.ACTION_STATUS_UPDATE"; + public static final String ACTION_START_SERVER = "org.librecast.receiver.ACTION_START_SERVER"; + public static final String ACTION_STOP_SERVER = "org.librecast.receiver.ACTION_STOP_SERVER"; + public static final String ACTION_QUERY_STATUS = "org.librecast.receiver.ACTION_QUERY_STATUS"; + + public static final String EXTRA_IS_RUNNING = "extra_is_running"; + public static final String EXTRA_SERVER_IP = "extra_server_ip"; + public static final String EXTRA_SERVER_PORT = "extra_server_port"; + public static final String EXTRA_IS_CLIENT_CONNECTED = "extra_is_client_connected"; + public static final String EXTRA_CLIENT_IP = "extra_client_ip"; + public static final String EXTRA_ERROR_MESSAGE = "extra_error_message"; + + private final IBinder binder = new LocalBinder(); + private LibreCastServer server; + private WifiManager.WifiLock wifiLock; + private PowerManager.WakeLock wakeLock; + + private boolean isRunning = false; + private boolean isStarting = false; + private String serverIp = "0.0.0.0"; + private int serverPort = LibreCastServer.DEFAULT_PORT; + private boolean isClientConnected = false; + private String clientIp = ""; + + public class LocalBinder extends Binder { + public ReceiverService getService() { + return ReceiverService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + acquireLocks(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null && intent.getAction() != null) { + String action = intent.getAction(); + if (ACTION_STOP_SERVER.equals(action)) { + stopSelf(); + return START_NOT_STICKY; + } else if (ACTION_QUERY_STATUS.equals(action)) { + broadcastStatus(); + } else if (ACTION_START_SERVER.equals(action)) { + if (!isRunning && !isStarting) { + startWebSocketServer(); + } else { + broadcastStatus(); + } + } + } else { + if (!isRunning && !isStarting) { + startWebSocketServer(); + } else { + broadcastStatus(); + } + } + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + private void acquireLocks() { + try { + WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "LibreCast:WifiLock"); + wifiLock.setReferenceCounted(false); + wifiLock.acquire(); + } + } catch (Exception e) { + Log.e(TAG, "Failed to acquire WifiLock", e); + } + + try { + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "LibreCast:WakeLock"); + wakeLock.setReferenceCounted(false); + wakeLock.acquire(); + } + } catch (Exception e) { + Log.e(TAG, "Failed to acquire WakeLock", e); + } + } + + private void releaseLocks() { + if (wifiLock != null && wifiLock.isHeld()) { + try { + wifiLock.release(); + } catch (Exception ignored) { + } + } + if (wakeLock != null && wakeLock.isHeld()) { + try { + wakeLock.release(); + } catch (Exception ignored) { + } + } + } + + private synchronized void startWebSocketServer() { + if (server != null) { + try { + server.stop(); + } catch (Exception ignored) { + } + } + isStarting = true; + serverIp = NetworkUtils.getLocalIpAddress(this); + serverPort = LibreCastServer.DEFAULT_PORT; + server = new LibreCastServer(serverPort, this); + server.start(); + } + + private synchronized void stopWebSocketServer() { + if (server != null) { + try { + server.stop(); + } catch (Exception e) { + Log.e(TAG, "Error stopping server", e); + } + server = null; + } + isStarting = false; + isRunning = false; + isClientConnected = false; + clientIp = ""; + broadcastStatus(); + } + + public synchronized void broadcastStatus() { + Intent intent = new Intent(ACTION_STATUS_UPDATE); + intent.setPackage(getPackageName()); + intent.putExtra(EXTRA_IS_RUNNING, isRunning); + intent.putExtra(EXTRA_SERVER_IP, serverIp); + intent.putExtra(EXTRA_SERVER_PORT, serverPort); + intent.putExtra(EXTRA_IS_CLIENT_CONNECTED, isClientConnected); + intent.putExtra(EXTRA_CLIENT_IP, clientIp); + sendBroadcast(intent); + } + + // LibreCastServer.ServerEventListener Callbacks + + @Override + public void onServerStarted(int port) { + isStarting = false; + isRunning = true; + serverPort = port; + serverIp = NetworkUtils.getLocalIpAddress(this); + Log.i(TAG, "WebSocket server started at ws://" + serverIp + ":" + serverPort); + broadcastStatus(); + } + + @Override + public void onServerStopped() { + isStarting = false; + isRunning = false; + isClientConnected = false; + clientIp = ""; + Log.i(TAG, "WebSocket server stopped"); + broadcastStatus(); + } + + @Override + public void onClientConnected(String ip) { + isClientConnected = true; + clientIp = ip; + Log.i(TAG, "Client connected: " + ip); + broadcastStatus(); + } + + @Override + public void onClientDisconnected(String ip) { + isClientConnected = false; + clientIp = ""; + Log.i(TAG, "Client disconnected: " + ip); + broadcastStatus(); + } + + @Override + public void onVideoChunkReceived(ByteBuffer chunk) { + // Raw video chunk received for processing / streaming playback pipeline + } + + @Override + public void onError(Exception ex) { + Log.e(TAG, "WebSocket server error", ex); + Intent intent = new Intent(ACTION_STATUS_UPDATE); + intent.setPackage(getPackageName()); + intent.putExtra(EXTRA_IS_RUNNING, isRunning); + intent.putExtra(EXTRA_SERVER_IP, serverIp); + intent.putExtra(EXTRA_SERVER_PORT, serverPort); + intent.putExtra(EXTRA_IS_CLIENT_CONNECTED, isClientConnected); + intent.putExtra(EXTRA_CLIENT_IP, clientIp); + if (ex != null) { + intent.putExtra(EXTRA_ERROR_MESSAGE, ex.getMessage()); + } + sendBroadcast(intent); + } + + @Override + public void onDestroy() { + stopWebSocketServer(); + releaseLocks(); + super.onDestroy(); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 04b57c5..2774066 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -22,14 +22,23 @@ android:textStyle="bold" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6cee3b5..519dec6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,9 @@ LibreCast Receiver LibreCast Receiver IP: 0.0.0.0 + Starting server... + Server stopped + Ready: ws://%1$s:%2$d + Waiting for connection... + Client Connected: %1$s diff --git a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java b/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java new file mode 100644 index 0000000..d5e0f14 --- /dev/null +++ b/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java @@ -0,0 +1,243 @@ +package org.librecast.receiver; + +import org.java_websocket.WebSocket; +import org.java_websocket.drafts.Draft; +import org.java_websocket.enums.Opcode; +import org.java_websocket.enums.ReadyState; +import org.java_websocket.framing.Framedata; +import org.java_websocket.protocols.IProtocol; +import org.junit.Test; + +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.*; + +public class LibreCastServerTest { + + @Test + public void testServerLifecycle() throws Exception { + AtomicBoolean started = new AtomicBoolean(false); + AtomicBoolean stopped = new AtomicBoolean(false); + + LibreCastServer server = new LibreCastServer(0, new LibreCastServer.ServerEventListener() { + @Override + public void onServerStarted(int port) { + started.set(true); + } + + @Override + public void onServerStopped() { + stopped.set(true); + } + + @Override + public void onClientConnected(String clientIp) {} + + @Override + public void onClientDisconnected(String clientIp) {} + + @Override + public void onVideoChunkReceived(ByteBuffer chunk) {} + + @Override + public void onError(Exception ex) {} + }); + + server.start(); + for (int i = 0; i < 20; i++) { + if (started.get()) break; + Thread.sleep(100); + } + assertTrue("Server should have started", started.get()); + + server.stop(); + assertTrue("Server should have stopped", stopped.get()); + } + + @Test + public void testSingleClientEnforcementAndBinaryChunk() { + AtomicReference connectedIp = new AtomicReference<>(""); + AtomicReference receivedChunk = new AtomicReference<>(null); + + LibreCastServer server = new LibreCastServer(0, new LibreCastServer.ServerEventListener() { + @Override + public void onServerStarted(int port) {} + + @Override + public void onServerStopped() {} + + @Override + public void onClientConnected(String clientIp) { + connectedIp.set(clientIp); + } + + @Override + public void onClientDisconnected(String clientIp) {} + + @Override + public void onVideoChunkReceived(ByteBuffer chunk) { + receivedChunk.set(chunk); + } + + @Override + public void onError(Exception ex) {} + }); + + DummyWebSocket conn1 = new DummyWebSocket("192.168.1.10"); + DummyWebSocket conn2 = new DummyWebSocket("192.168.1.20"); + + // First client connects + server.onOpen(conn1, null); + assertEquals(conn1, server.getActiveConnection()); + assertEquals("192.168.1.10", connectedIp.get()); + + // Send binary data from active client + ByteBuffer buffer = ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02, 0x03}); + server.onMessage(conn1, buffer); + assertNotNull("Binary chunk should be received", receivedChunk.get()); + assertEquals(4, receivedChunk.get().remaining()); + + // Second client connects, replacing first + server.onOpen(conn2, null); + assertEquals(conn2, server.getActiveConnection()); + assertTrue("First connection should be closed", conn1.isClosed()); + assertEquals("192.168.1.20", connectedIp.get()); + + // Send binary data from old client conn1 should be ignored + receivedChunk.set(null); + server.onMessage(conn1, buffer); + assertNull("Binary chunk from inactive client should be ignored", receivedChunk.get()); + + // Send binary data from active client conn2 should succeed + server.onMessage(conn2, buffer); + assertNotNull("Binary chunk from active client should be received", receivedChunk.get()); + } + + private static class DummyWebSocket implements WebSocket { + private final String ip; + private boolean closed = false; + + public DummyWebSocket(String ip) { + this.ip = ip; + } + + @Override + public void close(int code, String message) { + this.closed = true; + } + + @Override + public void close(int code) { + this.closed = true; + } + + @Override + public void close() { + this.closed = true; + } + + @Override + public void closeConnection(int code, String message) { + this.closed = true; + } + + @Override + public void send(String text) {} + + @Override + public void send(ByteBuffer bytes) {} + + @Override + public void send(byte[] bytes) {} + + @Override + public void sendFrame(Framedata framedata) {} + + @Override + public void sendFrame(Collection frames) {} + + @Override + public void sendPing() {} + + @Override + public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) {} + + @Override + public boolean hasBufferedData() { + return false; + } + + @Override + public InetSocketAddress getRemoteSocketAddress() { + return new InetSocketAddress(ip, 12345); + } + + @Override + public InetSocketAddress getLocalSocketAddress() { + return new InetSocketAddress("0.0.0.0", 8080); + } + + @Override + public boolean isOpen() { + return !closed; + } + + @Override + public boolean isClosing() { + return false; + } + + @Override + public boolean isFlushAndClose() { + return false; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public Draft getDraft() { + return null; + } + + @Override + public ReadyState getReadyState() { + return closed ? ReadyState.CLOSED : ReadyState.OPEN; + } + + @Override + public String getResourceDescriptor() { + return ""; + } + + @Override + public void setAttachment(T attachment) {} + + @Override + public T getAttachment() { + return null; + } + + @Override + public boolean hasSSLSupport() { + return false; + } + + @Override + public SSLSession getSSLSession() { + return null; + } + + @Override + public IProtocol getProtocol() { + return null; + } + } +} diff --git a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java b/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java new file mode 100644 index 0000000..76a5f16 --- /dev/null +++ b/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java @@ -0,0 +1,20 @@ +package org.librecast.receiver; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class NetworkUtilsTest { + + @Test + public void testGetNetworkInterfaceIpAddress() { + String ip = NetworkUtils.getNetworkInterfaceIpAddress(); + assertNotNull("IP address should not be null", ip); + assertFalse("IP address should not be empty", ip.trim().isEmpty()); + } + + @Test + public void testGetLocalIpAddressWithNullContext() { + String ip = NetworkUtils.getLocalIpAddress(null); + assertNotNull("IP address should fall back to non-null", ip); + } +} From 6e16b5fa4a26c87f669ac2921d69467a20839319 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:05:10 +0000 Subject: [PATCH 2/2] feat: Implement lightweight WebSocket server and receiver service - Refactor package name and applicationId to com.m5dev.librecast - Add Java-WebSocket 1.5.4 dependency compatible with minSdk 19 - Implement LibreCastServer extending WebSocketServer on port 8080 with binary video streaming support and single client enforcement - Implement ReceiverService acquiring WifiLock and WakeLock and broadcasting status updates - Implement NetworkUtils to resolve local Wi-Fi / Ethernet IPv4 address - Update MainActivity and layout to display server and client connection status - Add unit tests for NetworkUtils and LibreCastServer Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com> --- app/build.gradle | 4 ++-- .../m5dev/librecast}/LibreCastServer.java | 2 +- .../receiver => com/m5dev/librecast}/MainActivity.java | 2 +- .../receiver => com/m5dev/librecast}/NetworkUtils.java | 2 +- .../m5dev/librecast}/ReceiverService.java | 10 +++++----- .../m5dev/librecast}/LibreCastServerTest.java | 2 +- .../m5dev/librecast}/NetworkUtilsTest.java | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/LibreCastServer.java (99%) rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/MainActivity.java (99%) rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/NetworkUtils.java (98%) rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/ReceiverService.java (94%) rename app/src/test/java/{org/librecast/receiver => com/m5dev/librecast}/LibreCastServerTest.java (99%) rename app/src/test/java/{org/librecast/receiver => com/m5dev/librecast}/NetworkUtilsTest.java (94%) diff --git a/app/build.gradle b/app/build.gradle index 99cb2f2..f34418e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,11 +3,11 @@ plugins { } android { - namespace 'org.librecast.receiver' + namespace 'com.m5dev.librecast' compileSdk 34 defaultConfig { - applicationId "org.librecast.receiver" + applicationId "com.m5dev.librecast" minSdk 19 targetSdk 34 versionCode 1 diff --git a/app/src/main/java/org/librecast/receiver/LibreCastServer.java b/app/src/main/java/com/m5dev/librecast/LibreCastServer.java similarity index 99% rename from app/src/main/java/org/librecast/receiver/LibreCastServer.java rename to app/src/main/java/com/m5dev/librecast/LibreCastServer.java index 77231a9..36f6fb1 100644 --- a/app/src/main/java/org/librecast/receiver/LibreCastServer.java +++ b/app/src/main/java/com/m5dev/librecast/LibreCastServer.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; diff --git a/app/src/main/java/org/librecast/receiver/MainActivity.java b/app/src/main/java/com/m5dev/librecast/MainActivity.java similarity index 99% rename from app/src/main/java/org/librecast/receiver/MainActivity.java rename to app/src/main/java/com/m5dev/librecast/MainActivity.java index 228760a..c7f9549 100644 --- a/app/src/main/java/org/librecast/receiver/MainActivity.java +++ b/app/src/main/java/com/m5dev/librecast/MainActivity.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import android.content.BroadcastReceiver; import android.content.Context; diff --git a/app/src/main/java/org/librecast/receiver/NetworkUtils.java b/app/src/main/java/com/m5dev/librecast/NetworkUtils.java similarity index 98% rename from app/src/main/java/org/librecast/receiver/NetworkUtils.java rename to app/src/main/java/com/m5dev/librecast/NetworkUtils.java index 2f07fe4..60abdc0 100644 --- a/app/src/main/java/org/librecast/receiver/NetworkUtils.java +++ b/app/src/main/java/com/m5dev/librecast/NetworkUtils.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import android.content.Context; import android.net.wifi.WifiInfo; diff --git a/app/src/main/java/org/librecast/receiver/ReceiverService.java b/app/src/main/java/com/m5dev/librecast/ReceiverService.java similarity index 94% rename from app/src/main/java/org/librecast/receiver/ReceiverService.java rename to app/src/main/java/com/m5dev/librecast/ReceiverService.java index fdb0e00..1ddef25 100644 --- a/app/src/main/java/org/librecast/receiver/ReceiverService.java +++ b/app/src/main/java/com/m5dev/librecast/ReceiverService.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import android.app.Service; import android.content.Context; @@ -15,10 +15,10 @@ public class ReceiverService extends Service implements LibreCastServer.ServerEv private static final String TAG = "ReceiverService"; - public static final String ACTION_STATUS_UPDATE = "org.librecast.receiver.ACTION_STATUS_UPDATE"; - public static final String ACTION_START_SERVER = "org.librecast.receiver.ACTION_START_SERVER"; - public static final String ACTION_STOP_SERVER = "org.librecast.receiver.ACTION_STOP_SERVER"; - public static final String ACTION_QUERY_STATUS = "org.librecast.receiver.ACTION_QUERY_STATUS"; + public static final String ACTION_STATUS_UPDATE = "com.m5dev.librecast.ACTION_STATUS_UPDATE"; + public static final String ACTION_START_SERVER = "com.m5dev.librecast.ACTION_START_SERVER"; + public static final String ACTION_STOP_SERVER = "com.m5dev.librecast.ACTION_STOP_SERVER"; + public static final String ACTION_QUERY_STATUS = "com.m5dev.librecast.ACTION_QUERY_STATUS"; public static final String EXTRA_IS_RUNNING = "extra_is_running"; public static final String EXTRA_SERVER_IP = "extra_server_ip"; diff --git a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java b/app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java similarity index 99% rename from app/src/test/java/org/librecast/receiver/LibreCastServerTest.java rename to app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java index d5e0f14..e19fb61 100644 --- a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java +++ b/app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import org.java_websocket.WebSocket; import org.java_websocket.drafts.Draft; diff --git a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java b/app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java similarity index 94% rename from app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java rename to app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java index 76a5f16..f648e24 100644 --- a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java +++ b/app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java @@ -1,4 +1,4 @@ -package org.librecast.receiver; +package com.m5dev.librecast; import org.junit.Test; import static org.junit.Assert.*;