add notification text for foreground service

This commit is contained in:
eskimo 2025-04-12 16:35:59 -04:00
parent 152a31f876
commit 9a2f7a4735
10 changed files with 257 additions and 137 deletions

View File

@ -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<void>
### connect(...)
```typescript
connect(options: { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; }) => Promise<void>
connect(options: { id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; delimiter?: string; }) => Promise<void>
```
| Param | Type |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| **`options`** | <code>{ id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; }</code> |
| Param | Type |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **`options`** | <code>{ id: string; host: string; port: number; useTLS?: boolean; acceptInvalidCertificates?: boolean; delimiter?: string; }</code> |
--------------------
@ -92,6 +93,19 @@ close(options: { id: string; }) => Promise<void>
--------------------
### setNotificationText(...)
```typescript
setNotificationText(options: { notificationText: string; }) => Promise<void>
```
| Param | Type |
| ------------- | ------------------------------------------ |
| **`options`** | <code>{ notificationText: string; }</code> |
--------------------
### addListener('state', ...)
```typescript

View File

@ -1,11 +1,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<application>
<service
android:name=".SocketForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
android:name="software.eskimo.capacitor.sockets.SocketForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="connectedDevice" />
</application>
</manifest>
</manifest>

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -7,10 +7,10 @@ import java.util.List;
public class Sockets implements SocketHandler.SocketDelegate {
private List<SocketHandler> 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);
}
}
}

View File

@ -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;
}
}

View File

@ -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",

View File

@ -2,10 +2,11 @@ import { PluginListenerHandle } from "@capacitor/core";
export interface SocketsPlugin {
create(options: { id: string; }): Promise<void>;
connect(options: { id: string; host: string; port: number, useTLS?: boolean, acceptInvalidCertificates?: boolean }): Promise<void>;
connect(options: { id: string; host: string; port: number, useTLS?: boolean, acceptInvalidCertificates?: boolean, delimiter?: string }): Promise<void>;
send(options: { id: string; message: string }): Promise<void>;
disconnect(options: { id: string }): Promise<void>;
close(options: { id: string }): Promise<void>;
setNotificationText(options: { notificationText: string }): Promise<void>;
addListener(eventName: "state", listenerFunc: (message: { id: string; state: string }) => void): Promise<PluginListenerHandle>;
addListener(eventName: "message", listenerFunc: (message: { id: string; message: string }) => void): Promise<PluginListenerHandle>;

View File

@ -1,2 +1,2 @@
import Socket from "./socket";
export { Socket };
export { Socket };

View File

@ -5,110 +5,112 @@ import mitt, { Emitter } from "mitt";
const Sockets = registerPlugin<SocketsPlugin>("Sockets");
type SocketEvents = {
message: string;
state: string;
message: string;
state: string;
};
class SocketManager {
private static instance: SocketManager;
private socketsMap: Map<string, Socket> = new Map();
private static instance: SocketManager;
private socketsMap: Map<string, Socket> = 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<SocketEvents>;
private emitter: Emitter<SocketEvents>;
constructor(config: { id: string }) {
console.log(config);
this.id = config.id;
this.emitter = mitt<SocketEvents>();
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<SocketEvents>();
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);
}
}