diff --git a/README.md b/README.md index f55eef3..33cb801 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ npx cap sync * [`send(...)`](#send) * [`disconnect(...)`](#disconnect) * [`close(...)`](#close) +* [`setNotificationText(...)`](#setnotificationtext) * [`addListener('state', ...)`](#addlistenerstate-) * [`addListener('message', ...)`](#addlistenermessage-) * [Interfaces](#interfaces) @@ -43,12 +44,12 @@ create(options: { id: string; }) => Promise ### connect(...) ```typescript -connect(options: { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; }) => Promise +connect(options: { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; delimiter?: string; }) => Promise ``` -| Param | Type | -| ------------- | --------------------------------------------------------------------------------------------------------------- | -| **`options`** | { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; } | +| Param | Type | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| **`options`** | { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; delimiter?: string; } | -------------------- @@ -92,6 +93,19 @@ close(options: { id: string; }) => Promise -------------------- +### setNotificationText(...) + +```typescript +setNotificationText(options: { notificationText: string; }) => Promise +``` + +| Param | Type | +| ------------- | ------------------------------------------ | +| **`options`** | { notificationText: string; } | + +-------------------- + + ### addListener('state', ...) ```typescript diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 626f16e..217ef06 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,11 +1,12 @@ - - + + + + android:name="software.eskimo.capacitor.sockets.SocketForegroundService" + android:enabled="true" + android:exported="false" + android:foregroundServiceType="connectedDevice" /> - + \ No newline at end of file diff --git a/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java b/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java index 41375cb..40a1aaf 100644 --- a/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java +++ b/android/src/main/java/software/eskimo/capacitor/sockets/SocketForegroundService.java @@ -14,7 +14,8 @@ import android.util.Log; public class SocketForegroundService extends Service { private static final String CHANNEL_ID = "socket_channel"; - private static final String CHANNEL_NAME = "Socket Service Channel"; + private static final String CHANNEL_NAME = "Socket Service"; + private static final int NOTIFICATION_ID = 1; @Override public void onCreate() { @@ -24,7 +25,20 @@ public class SocketForegroundService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { - // Retrieve the main app's "app_name" string resource + String action = intent != null ? intent.getAction() : null; + String notificationText = intent != null ? + intent.getStringExtra("notificationText") : "Maintaining connection"; + + if ("software.eskimo.capacitor.sockets.UPDATE_NOTIFICATION".equals(action)) { + updateNotification(notificationText); + } else { + startForegroundWithNotification(notificationText); + } + + return START_STICKY; + } + + private void startForegroundWithNotification(String notificationText) { int appNameResId = getResources().getIdentifier("app_name", "string", getApplicationContext().getPackageName()); String appName = (appNameResId != 0) ? getString(appNameResId) : "Socket Service"; @@ -35,18 +49,49 @@ public class SocketForegroundService extends Service { Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle(appName) - .setContentText("Background service running") + .setContentText(notificationText) .setSmallIcon(iconResId) + .setPriority(NotificationCompat.PRIORITY_LOW) .build(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(1, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC); + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE); } else { - startForeground(1, notification); + startForeground(NOTIFICATION_ID, notification); } - + Log.d("SocketForegroundService", "Foreground service started with app name: " + appName); - return START_STICKY; + } + + private void updateNotification(String notificationText) { + int appNameResId = getResources().getIdentifier("app_name", "string", getApplicationContext().getPackageName()); + String appName = (appNameResId != 0) ? getString(appNameResId) : "Socket Service"; + + int iconResId = getResources().getIdentifier("ic_notification", "drawable", getApplicationContext().getPackageName()); + if (iconResId == 0) { + iconResId = android.R.drawable.ic_dialog_info; + } + + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(appName) + .setContentText(notificationText) + .setSmallIcon(iconResId) + .setPriority(NotificationCompat.PRIORITY_LOW) + .build(); + + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.notify(NOTIFICATION_ID, notification); + } + + Log.d("SocketForegroundService", "Notification updated with text: " + notificationText); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + Log.d("SocketForegroundService", "App task removed. Stopping foreground service."); + stopSelf(); + super.onTaskRemoved(rootIntent); } private void createNotificationChannel() { @@ -68,4 +113,4 @@ public class SocketForegroundService extends Service { public IBinder onBind(Intent intent) { return null; } -} +} \ 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..8b0acbc 100644 --- a/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java +++ b/android/src/main/java/software/eskimo/capacitor/sockets/SocketHandler.java @@ -12,6 +12,8 @@ import java.io.IOException; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.security.cert.X509Certificate; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class SocketHandler { private String id; @@ -19,18 +21,24 @@ public class SocketHandler { private OutputStream outputStream; private BufferedReader inputStream; private SocketDelegate delegate; + private String delimiter; + private final ExecutorService executor = Executors.newFixedThreadPool(2); public SocketHandler(String id, SocketDelegate delegate) { this.id = id; this.delegate = delegate; + this.delimiter = "\r\n"; // Default delimiter } public String getId() { return id; } - public void connect(final String host, final int port, final boolean useTLS, final boolean acceptInvalidCertificates) { - new Thread(() -> { + public void connect(final String host, final int port, final boolean useTLS, final boolean acceptInvalidCertificates, final String delimiter) { + if (delimiter != null && !delimiter.isEmpty()) { + this.delimiter = delimiter; + } + executor.execute(() -> { try { delegate.onStateChanged(id, "connecting"); @@ -63,11 +71,11 @@ public class SocketHandler { Log.e("SocketHandler", "Connection error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); } - }).start(); + }); } public void send(String message) { - new Thread(() -> { + executor.execute(() -> { try { if (outputStream != null) { outputStream.write(message.getBytes()); @@ -77,11 +85,17 @@ public class SocketHandler { Log.e("SocketHandler", "Send error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); } - }).start(); + }); } public void disconnect() { try { + if (inputStream != null) { + inputStream.close(); + } + if (outputStream != null) { + outputStream.close(); + } if (socket != null) { socket.close(); } @@ -89,11 +103,16 @@ public class SocketHandler { } catch (IOException e) { Log.e("SocketHandler", "Disconnect error: " + e.getMessage(), e); delegate.onStateChanged(id, "disconnected"); + } finally { + inputStream = null; + outputStream = null; + socket = null; + executor.shutdownNow(); } } private void receive() { - new Thread(() -> { + executor.execute(() -> { try { char[] buffer = new char[1024]; StringBuilder messageBuilder = new StringBuilder(); @@ -102,22 +121,21 @@ public class SocketHandler { 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")) { + if (message.endsWith(delimiter)) { 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"); } - }).start(); + }); } public interface SocketDelegate { void onStateChanged(String socketId, String state); void onMessageReceived(String socketId, String message); } -} +} \ No newline at end of file diff --git a/android/src/main/java/software/eskimo/capacitor/sockets/Sockets.java b/android/src/main/java/software/eskimo/capacitor/sockets/Sockets.java index 09d2544..dc85ef7 100644 --- a/android/src/main/java/software/eskimo/capacitor/sockets/Sockets.java +++ b/android/src/main/java/software/eskimo/capacitor/sockets/Sockets.java @@ -7,10 +7,10 @@ import java.util.List; public class Sockets implements SocketHandler.SocketDelegate { private List sockets = new ArrayList<>(); - private SocketsPlugin plugin; // Reference to SocketsPlugin for notifying JS + private SocketsPlugin plugin; public Sockets(SocketsPlugin plugin) { - this.plugin = plugin; // Pass plugin reference for message forwarding + this.plugin = plugin; } public SocketHandler create(String id) { @@ -19,10 +19,10 @@ public class Sockets implements SocketHandler.SocketDelegate { return socket; } - public void connect(String id, String host, int port, boolean useTLS, boolean acceptInvalidCertificates) { + public void connect(String id, String host, int port, boolean useTLS, boolean acceptInvalidCertificates, String delimiter) { SocketHandler socket = getSocketById(id); if (socket != null) { - socket.connect(host, port, useTLS, acceptInvalidCertificates); + socket.connect(host, port, useTLS, acceptInvalidCertificates, delimiter); } } @@ -37,9 +37,14 @@ public class Sockets implements SocketHandler.SocketDelegate { SocketHandler socket = getSocketById(id); if (socket != null) { socket.disconnect(); + sockets.remove(socket); } } + public int getActiveSocketCount() { + return sockets.size(); + } + private SocketHandler getSocketById(String id) { for (SocketHandler socket : sockets) { if (socket.getId().equals(id)) { @@ -49,19 +54,15 @@ public class Sockets implements SocketHandler.SocketDelegate { return null; } - // Handle state changes (connected, disconnected) @Override public void onStateChanged(String socketId, String state) { Log.d("Sockets", "Socket state changed: " + state); - // Call notifyStateListeners in SocketsPlugin plugin.notifyStateListeners("state", socketId, state); } - // Handle incoming messages @Override public void onMessageReceived(String socketId, String message) { Log.d("Sockets", "Socket message received: " + message); - // Call notifyMessageListeners in SocketsPlugin - plugin.notifyMessageListeners(socketId, message); // Notify JS about the received message + plugin.notifyMessageListeners(socketId, message); } -} +} \ No newline at end of file diff --git a/android/src/main/java/software/eskimo/capacitor/sockets/SocketsPlugin.java b/android/src/main/java/software/eskimo/capacitor/sockets/SocketsPlugin.java index 17573d2..6d0223a 100644 --- a/android/src/main/java/software/eskimo/capacitor/sockets/SocketsPlugin.java +++ b/android/src/main/java/software/eskimo/capacitor/sockets/SocketsPlugin.java @@ -1,5 +1,7 @@ package software.eskimo.capacitor.sockets; +import android.app.ActivityManager; +import android.content.Context; import android.content.Intent; import android.util.Log; import com.getcapacitor.JSObject; @@ -11,7 +13,7 @@ import com.getcapacitor.PluginMethod; @CapacitorPlugin(name = "Sockets") public class SocketsPlugin extends Plugin { - private Sockets implementation = new Sockets(this); // Pass the plugin reference + private Sockets implementation = new Sockets(this); @PluginMethod public void create(PluginCall call) { @@ -27,11 +29,11 @@ public class SocketsPlugin extends Plugin { int port = call.getInt("port", 0); boolean useTLS = call.getBoolean("useTLS", false); boolean acceptInvalidCertificates = call.getBoolean("acceptInvalidCertificates", false); + String delimiter = call.getString("delimiter", "\r\n"); - // Start the foreground service to keep the socket connection alive in the background startSocketForegroundService(); - implementation.connect(id, host, port, useTLS, acceptInvalidCertificates); + implementation.connect(id, host, port, useTLS, acceptInvalidCertificates, delimiter); call.resolve(); } @@ -49,10 +51,29 @@ public class SocketsPlugin extends Plugin { String id = call.getString("id", ""); implementation.disconnect(id); + if (implementation.getActiveSocketCount() == 0) { + stopSocketForegroundService(); + } + call.resolve(); + } + + @PluginMethod + public void setNotificationText(PluginCall call) { + String notificationText = call.getString("notificationText", "Maintaining connection"); + + Intent intent = new Intent(getContext(), SocketForegroundService.class); + intent.setAction("software.eskimo.capacitor.sockets.UPDATE_NOTIFICATION"); + intent.putExtra("notificationText", notificationText); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + getContext().startForegroundService(intent); + } else { + getContext().startService(intent); + } + call.resolve(); } - // Helper method for notifying JavaScript listeners about state changes public void notifyStateListeners(String event, String id, String state) { JSObject data = new JSObject(); data.put("id", id); @@ -70,9 +91,26 @@ public class SocketsPlugin extends Plugin { notifyListeners("message", data); } - // Method to start the foreground service private void startSocketForegroundService() { - Intent serviceIntent = new Intent(getContext(), SocketForegroundService.class); - getContext().startForegroundService(serviceIntent); + if (!isServiceRunning(SocketForegroundService.class)) { + Intent serviceIntent = new Intent(getContext(), SocketForegroundService.class); + serviceIntent.putExtra("notificationText", "Maintaining connection"); + getContext().startForegroundService(serviceIntent); + } } -} + + private void stopSocketForegroundService() { + Intent serviceIntent = new Intent(getContext(), SocketForegroundService.class); + getContext().stopService(serviceIntent); + } + + private boolean isServiceRunning(Class serviceClass) { + ActivityManager manager = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (serviceClass.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 158e0bd..49ba024 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "capacitor-sockets", - "version": "0.0.1", + "version": "0.0.4", "description": "Sockets", "main": "dist/plugin.cjs.js", "module": "dist/esm/index.js", diff --git a/src/definitions.ts b/src/definitions.ts index 5d424b6..fb6d160 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -2,10 +2,11 @@ import { PluginListenerHandle } from "@capacitor/core"; export interface SocketsPlugin { create(options: { id: string; }): Promise; - connect(options: { id: string; host: string; port: number, useTLS?: boolean, acceptInvalidCertificates?: boolean }): Promise; + connect(options: { id: string; host: string; port: number, useTLS?: boolean, acceptInvalidCertificates?: boolean, delimiter?: string }): Promise; send(options: { id: string; message: string }): Promise; disconnect(options: { id: string }): Promise; close(options: { id: string }): Promise; + setNotificationText(options: { notificationText: string }): Promise; addListener(eventName: "state", listenerFunc: (message: { id: string; state: string }) => void): Promise; addListener(eventName: "message", listenerFunc: (message: { id: string; message: string }) => void): Promise; diff --git a/src/index.ts b/src/index.ts index 991e707..d17e3ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ import Socket from "./socket"; -export { Socket }; +export { Socket }; \ No newline at end of file diff --git a/src/socket.ts b/src/socket.ts index a3f31e6..c345340 100644 --- a/src/socket.ts +++ b/src/socket.ts @@ -5,110 +5,112 @@ import mitt, { Emitter } from "mitt"; const Sockets = registerPlugin("Sockets"); type SocketEvents = { - message: string; - state: string; + message: string; + state: string; }; class SocketManager { - private static instance: SocketManager; - private socketsMap: Map = new Map(); + private static instance: SocketManager; + private socketsMap: Map = new Map(); - private constructor() { - this.addGlobalListeners(); - } + private constructor() { + this.addGlobalListeners(); + } - public static getInstance() { - if (!SocketManager.instance) { - SocketManager.instance = new SocketManager(); - } - return SocketManager.instance; - } + public static getInstance() { + if (!SocketManager.instance) { + SocketManager.instance = new SocketManager(); + } + return SocketManager.instance; + } - public registerSocket(socket: Socket) { - this.socketsMap.set(socket.id, socket); - } + public registerSocket(socket: Socket) { + this.socketsMap.set(socket.id, socket); + } - public unregisterSocket(socket: Socket) { - this.socketsMap.delete(socket.id); - } + public unregisterSocket(socket: Socket) { + this.socketsMap.delete(socket.id); + } - private addGlobalListeners() { - Sockets.addListener("state", (data) => { - let socket = this.socketsMap.get(data.id); - if (socket) { - socket.didChangeState(data); - } - }); + private addGlobalListeners() { + Sockets.addListener("state", (data) => { + let socket = this.socketsMap.get(data.id); + if (socket) { + socket.didChangeState(data); + } + }); - Sockets.addListener("message", (data) => { - let socket = this.socketsMap.get(data.id); - if (socket) { - socket.didReceiveMessage(data); - } - }); - } + Sockets.addListener("message", (data) => { + let socket = this.socketsMap.get(data.id); + if (socket) { + socket.didReceiveMessage(data); + } + }); + } } export default class Socket { - id: string; + id: string; + private emitter: Emitter; - private emitter: Emitter; + constructor(config: { id: string }) { + console.log(config); + this.id = config.id; + this.emitter = mitt(); + this.create(); + SocketManager.getInstance().registerSocket(this); + } - constructor(config: { id: string }) { - console.log(config); + on(event: keyof SocketEvents, handler: (data: any) => void) { + this.emitter.on(event, handler); + } - this.id = config.id; + async create() { + await Sockets.create({ + id: this.id + }); + } - this.emitter = mitt(); + async connect(config: { host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; delimiter?: string }) { + await Sockets.connect({ + id: this.id, + host: config.host, + port: config.port, + useTLS: config.useTLS ?? false, + acceptInvalidCertificates: config.acceptInvalidCertificates ?? false, + delimiter: config.delimiter ?? "\r\n" + }); + } - this.create(); + async disconnect() { + await Sockets.disconnect({ + id: this.id + }); + } - SocketManager.getInstance().registerSocket(this); - } + async close() { + await this.disconnect(); + SocketManager.getInstance().unregisterSocket(this); + } - on(event: keyof SocketEvents, handler: (data: any) => void) { - this.emitter.on(event, handler); - } + async send(message: string) { + await Sockets.send({ + id: this.id, + message: message + }); + } - create() { - Sockets.create({ - id: this.id - }); - } + async setNotificationText(config: { notificationText: string }) { + await Sockets.setNotificationText({ + notificationText: config.notificationText + }); + } - connect(config: { host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean }) { - Sockets.connect({ - id: this.id, - host: config.host, - port: config.port, - useTLS: config.useTLS ?? false, - acceptInvalidCertificates: config.acceptInvalidCertificates ?? false - }); - } + didChangeState(data: { state: string }) { + this.emitter.emit("state", data.state); + } - disconnect() { - Sockets.disconnect({ - id: this.id - }); - } - - close() { - this.disconnect(); - SocketManager.getInstance().unregisterSocket(this); - } - - send(message: string) { - Sockets.send({ - id: this.id, - message: message - }); - } - - didChangeState(data: { state: string }) { - this.emitter.emit("state", data.state); - } - - didReceiveMessage(data: { message: string }) { - this.emitter.emit("message", data.message); - } -} + didReceiveMessage(data: { message: string }) { + this.emitter.emit("message", data.message); + } +} \ No newline at end of file