diff --git a/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java b/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java new file mode 100644 index 0000000..7f40f80 --- /dev/null +++ b/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java @@ -0,0 +1,146 @@ +package software.eskimo.capacitor.sockets; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import androidx.core.app.NotificationCompat; +import com.getcapacitor.JSObject; +import android.util.Log; + +public class SocketForegroundService extends Service { + private static final String TAG = "SocketForegroundService"; + + // Default values + private static final String DEFAULT_CHANNEL_ID = "socket_service_channel"; + private static final String DEFAULT_CHANNEL_NAME = "Socket Connection"; + private static final String DEFAULT_NOTIFICATION_TITLE = "Connected to Server"; + private static final String DEFAULT_NOTIFICATION_TEXT = "Tap to open"; + private static final int DEFAULT_NOTIFICATION_ID = 1001; + + // Configurable values + private String channelId = DEFAULT_CHANNEL_ID; + private String channelName = DEFAULT_CHANNEL_NAME; + private String notificationTitle = DEFAULT_NOTIFICATION_TITLE; + private String notificationText = DEFAULT_NOTIFICATION_TEXT; + private int notificationId = DEFAULT_NOTIFICATION_ID; + private int notificationIcon = android.R.drawable.ic_dialog_info; + private boolean running = false; + + private final IBinder binder = new LocalBinder(); + + public class LocalBinder extends Binder { + SocketForegroundService getService() { + return SocketForegroundService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + channelId = intent.getStringExtra("channelId") != null ? + intent.getStringExtra("channelId") : DEFAULT_CHANNEL_ID; + channelName = intent.getStringExtra("channelName") != null ? + intent.getStringExtra("channelName") : DEFAULT_CHANNEL_NAME; + notificationTitle = intent.getStringExtra("notificationTitle") != null ? + intent.getStringExtra("notificationTitle") : DEFAULT_NOTIFICATION_TITLE; + notificationText = intent.getStringExtra("notificationText") != null ? + intent.getStringExtra("notificationText") : DEFAULT_NOTIFICATION_TEXT; + notificationId = intent.getIntExtra("notificationId", DEFAULT_NOTIFICATION_ID); + notificationIcon = intent.getIntExtra("notificationIcon", notificationIcon); + + Log.d(TAG, "Starting foreground service with: " + + "channelId=" + channelId + + ", title=" + notificationTitle + + ", text=" + notificationText); + + startForeground(); + } else { + Log.e(TAG, "Intent was null in onStartCommand"); + } + + return START_STICKY; + } + + public void startForeground() { + if (running) { + Log.d(TAG, "Service already running, not starting again"); + return; + } + + try { + createNotificationChannel(); + + Intent notificationIntent = getPackageManager() + .getLaunchIntentForPackage(getPackageName()); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); + + Log.d(TAG, "Building notification with title: " + notificationTitle); + + Notification notification = new NotificationCompat.Builder(this, channelId) + .setContentTitle(notificationTitle) + .setContentText(notificationText) + .setSmallIcon(notificationIcon) + .setContentIntent(pendingIntent) + .setOngoing(true) + .build(); + + startForeground(notificationId, notification); + running = true; + Log.d(TAG, "Foreground service started with notification ID: " + notificationId); + } catch (Exception e) { + Log.e(TAG, "Error in startForeground", e); + } + } + + public void updateNotification(String title, String text) { + if (!running) return; + + NotificationManager notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + Notification notification = new NotificationCompat.Builder(this, channelId) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(notificationIcon) + .setContentIntent(PendingIntent.getActivity( + this, 0, + getPackageManager().getLaunchIntentForPackage(getPackageName()), + PendingIntent.FLAG_IMMUTABLE)) + .setOngoing(true) + .build(); + + notificationManager.notify(notificationId, notification); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + channelId, + channelName, + NotificationManager.IMPORTANCE_LOW + ); + + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } + + @Override + public void onDestroy() { + running = false; + Log.d(TAG, "Foreground service stopped"); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java b/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java index 9ec4344..038156b 100644 --- a/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java +++ b/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java @@ -1,17 +1,23 @@ package software.eskimo.capacitor.sockets; +import android.os.Build; import android.util.Log; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.InetSocketAddress; import java.net.Socket; -import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -import java.io.IOException; +import javax.net.ssl.SNIHostName; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -import java.security.cert.X509Certificate; +import java.io.IOException; +import java.util.Collections; public class SocketHandler { private String id; @@ -33,39 +39,58 @@ public class SocketHandler { new Thread(() -> { try { delegate.onStateChanged(id, "connecting"); - + if (useTLS) { - SSLContext sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); if (acceptInvalidCertificates) { - sslContext.init(null, new TrustManager[]{new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) {} - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) {} - - @Override - public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } - }}, new java.security.SecureRandom()); + sslContext.init(null, new TrustManager[]{new InsecureTrustAllManager()}, new java.security.SecureRandom()); } else { sslContext.init(null, null, new java.security.SecureRandom()); } - SSLSocketFactory socketFactory = sslContext.getSocketFactory(); - socket = socketFactory.createSocket(host, port); + + SSLSocketFactory factory = sslContext.getSocketFactory(); + SSLSocket sslSocket = (SSLSocket) factory.createSocket(); + + if (acceptInvalidCertificates) { + java.net.InetAddress addr = java.net.InetAddress.getByName(host); + sslSocket.connect(new java.net.InetSocketAddress(addr, port)); + } else { + sslSocket.connect(new java.net.InetSocketAddress(host, port)); + } + + javax.net.ssl.SSLParameters params = sslSocket.getSSLParameters(); + try { + if (android.os.Build.VERSION.SDK_INT >= 24) { + params.setServerNames(java.util.Collections.singletonList(new javax.net.ssl.SNIHostName(host))); + } + } catch (IllegalArgumentException ignored) {} + + if (acceptInvalidCertificates) { + params.setEndpointIdentificationAlgorithm(null); + } else { + params.setEndpointIdentificationAlgorithm("HTTPS"); + } + sslSocket.setSSLParameters(params); + + sslSocket.startHandshake(); + socket = sslSocket; } else { - socket = new Socket(host, port); + socket = new java.net.Socket(host, port); } + delegate.onStateChanged(id, "connected"); outputStream = socket.getOutputStream(); - inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream())); + inputStream = new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream())); receive(); } catch (Exception e) { - Log.e("SocketHandler", "Connection error: " + e.getMessage(), e); + android.util.Log.e("SocketHandler", "Connection error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); + closeQuietly(); } }).start(); } + public void send(String message) { new Thread(() -> { try { @@ -76,6 +101,7 @@ public class SocketHandler { } catch (IOException e) { Log.e("SocketHandler", "Send error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); + closeQuietly(); } }).start(); } @@ -101,23 +127,35 @@ public class SocketHandler { while ((numCharsRead = inputStream.read(buffer)) != -1) { messageBuilder.append(buffer, 0, numCharsRead); String message = messageBuilder.toString(); - - // Check if the message ends with \r\n (or \n, depending on protocol) if (message.endsWith("\r\n")) { Log.d("SocketHandler", "Message received: " + message); - delegate.onMessageReceived(id, message); // Notify with full message including \r\n - messageBuilder.setLength(0); // Clear the buffer for the next message + delegate.onMessageReceived(id, message); + messageBuilder.setLength(0); } } } catch (IOException e) { Log.e("SocketHandler", "Receive error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); + } finally { + closeQuietly(); } }).start(); } + private void closeQuietly() { + try { + if (socket != null) socket.close(); + } catch (Throwable ignored) {} + } + public interface SocketDelegate { void onStateChanged(String socketId, String state); void onMessageReceived(String socketId, String message); } + + private static class InsecureTrustAllManager implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } + } }