From 9665b6b05b70b6cff339dffafd0c9f5b7d42bd83 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 22 Jul 2025 21:05:24 +0700 Subject: [PATCH 1/5] [gateway] draft SSE implementation --- app/build.gradle | 4 + app/src/main/AndroidManifest.xml | 11 +- .../capcom/smsgateway/helpers/SSEManager.kt | 123 ++++++++++++++++++ .../modules/gateway/GatewayService.kt | 3 + .../gateway/services/SSEForegroundService.kt | 107 +++++++++++++++ 5 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/me/capcom/smsgateway/helpers/SSEManager.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt diff --git a/app/build.gradle b/app/build.gradle index 6d53edff..d0fc6a01 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -57,6 +57,7 @@ android { dependencies { def ktor_version = "2.2.4" + def okhttp_version = "4.10.0" def room_version = "2.4.3" def coroutines_version = "1.6.1" def work_version = "2.7.1" @@ -80,6 +81,9 @@ dependencies { implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") implementation("io.ktor:ktor-client-okhttp:$ktor_version") + implementation("com.squareup.okhttp3:okhttp:$okhttp_version") + implementation("com.squareup.okhttp3:okhttp-sse:$okhttp_version") + // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ed420f9..72ae36e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,10 +23,15 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:networkSecurityConfig="@xml/network_security_config" android:theme="@style/Theme.SmsGateway"> + + android:enabled="true" + android:foregroundServiceType="connectedDevice" /> Unit)? = null + var onConnected: (() -> Unit)? = null + var onError: ((Throwable?) -> Unit)? = null + var onClosed: (() -> Unit)? = null + + fun connect() { + isDisconnecting.set(false) + scope.launch { + try { + val request = Request.Builder() + .url(url) + .apply { + header("Authorization", "Bearer $authToken") + } + .build() + + eventSource = EventSources.createFactory(client) + .newEventSource(request, object : EventSourceListener() { + override fun onOpen(eventSource: EventSource, response: Response) { + Log.d(TAG, "SSE connected") + reconnectAttempts = 0 + onConnected?.invoke() + } + + override fun onEvent( + eventSource: EventSource, + id: String?, + type: String?, + data: String + ) { + Log.d(TAG, "Event received: $type - $data") + onEvent?.invoke(type ?: "message", data) + } + + override fun onClosed(eventSource: EventSource) { + Log.d(TAG, "SSE connection closed") + onClosed?.invoke() + scheduleReconnect() + } + + override fun onFailure( + eventSource: EventSource, + t: Throwable?, + response: Response? + ) { + Log.e(TAG, "SSE error", t) + onError?.invoke(t) + scheduleReconnect() + } + }) + } catch (e: Exception) { + Log.e(TAG, "Connection failed", e) + scheduleReconnect() + } + } + } + + fun disconnect() { + isDisconnecting.set(true) + scope.launch { + eventSource?.cancel() + eventSource = null + reconnectAttempts = 0 + } + scope.coroutineContext.cancelChildren() + } + + private fun scheduleReconnect() { + if (isDisconnecting.get()) { + return + } + + reconnectAttempts++ + val delay = when { + reconnectAttempts > 10 -> 60_000L // 1 minute + reconnectAttempts > 5 -> 30_000L // 30 seconds + else -> 5_000L // 5 seconds + } + + scope.launch { + eventSource?.cancel() + eventSource = null + Log.d(TAG, "Reconnecting in ${delay}ms (attempt $reconnectAttempts)") + kotlinx.coroutines.delay(delay) + connect() + } + } + + companion object { + const val TAG = "SSEManager" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt index 8c621706..9a45e968 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt @@ -9,6 +9,7 @@ import me.capcom.smsgateway.domain.EntitySource import me.capcom.smsgateway.domain.MessageContent import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.modules.gateway.events.DeviceRegisteredEvent +import me.capcom.smsgateway.modules.gateway.services.SSEForegroundService import me.capcom.smsgateway.modules.gateway.workers.PullMessagesWorker import me.capcom.smsgateway.modules.gateway.workers.SendStateWorker import me.capcom.smsgateway.modules.gateway.workers.SettingsUpdateWorker @@ -45,6 +46,7 @@ class GatewayService( PullMessagesWorker.start(context) WebhooksUpdateWorker.start(context) SettingsUpdateWorker.start(context) + SSEForegroundService.start(context) eventsReceiver.start() } @@ -52,6 +54,7 @@ class GatewayService( fun stop(context: Context) { eventsReceiver.stop() + SSEForegroundService.stop(context) SettingsUpdateWorker.stop(context) WebhooksUpdateWorker.stop(context) PullMessagesWorker.stop(context) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt new file mode 100644 index 00000000..e80c711d --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt @@ -0,0 +1,107 @@ +package me.capcom.smsgateway.modules.gateway.services + +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.wifi.WifiManager +import android.os.Build +import android.os.IBinder +import android.os.PowerManager +import android.util.Log +import me.capcom.smsgateway.helpers.SSEManager +import me.capcom.smsgateway.modules.events.EventBus +import me.capcom.smsgateway.modules.gateway.GatewaySettings +import me.capcom.smsgateway.modules.notifications.NotificationsService +import org.koin.android.ext.android.get +import org.koin.android.ext.android.inject + +class SSEForegroundService : Service() { + private val settings: GatewaySettings by inject() + private val eventBus = get() + + private val notificationsSvc: NotificationsService by inject() + + private val wakeLock: PowerManager.WakeLock by lazy { + (getSystemService(Context.POWER_SERVICE) as PowerManager).run { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, this.javaClass.name) + } + } + private val wifiLock: WifiManager.WifiLock by lazy { + (getSystemService(Context.WIFI_SERVICE) as WifiManager).createWifiLock( + WifiManager.WIFI_MODE_FULL_HIGH_PERF, + this.javaClass.name + ) + } + + private val sseManager by lazy { + SSEManager( + "${settings.serverUrl}/events", + requireNotNull( + settings.registrationInfo?.token + ) { "Authentication token is required for SSE connection" } + ) + .apply { + onEvent = { event, data -> + Log.d("SSEForegroundService", "$event: $data") + } + } + } + + override fun onCreate() { + super.onCreate() + + if (!wakeLock.isHeld) { + wakeLock.acquire() + } + if (!wifiLock.isHeld) { + wifiLock.acquire() + } + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val notification = notificationsSvc.makeNotification( + this, + NotificationsService.NOTIFICATION_ID_PING_SERVICE, + "Listening to the server events..." + ) + + startForeground(NotificationsService.NOTIFICATION_ID_PING_SERVICE, notification) + + sseManager.connect() + + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent): IBinder? { + return null + } + + override fun onDestroy() { + sseManager.disconnect() + if (wifiLock.isHeld) { + wifiLock.release() + } + if (wakeLock.isHeld) { + wakeLock.release() + } + stopForeground(true) + + super.onDestroy() + } + + companion object { + fun start(context: Context) { + val intent = Intent(context, SSEForegroundService::class.java) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + } + + fun stop(context: Context) { + context.stopService(Intent(context, SSEForegroundService::class.java)) + } + } +} \ No newline at end of file From e16bdda736178d10fd0b24eff45a33664bd4d5a2 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Thu, 31 Jul 2025 10:24:06 +0700 Subject: [PATCH 2/5] [events] unify events processing --- .../capcom/smsgateway/helpers/SSEManager.kt | 4 +- .../modules/events/ExternalEvent.kt | 6 ++ .../Event.kt => events/ExternalEventType.kt} | 4 +- .../modules/gateway/EventsReceiver.kt | 32 +++++++++- .../gateway/events/MessageEnqueuedEvent.kt | 9 +++ .../gateway/events/SettingsUpdatedEvent.kt | 10 ++++ .../gateway/events/WebhooksUpdatedEvent.kt | 9 +++ .../gateway/services/SSEForegroundService.kt | 40 ++++++++++++- .../gateway/workers/WebhooksUpdateWorker.kt | 2 +- .../modules/orchestrator/EventsRouter.kt | 47 +++++++++++++++ .../smsgateway/modules/orchestrator/Module.kt | 1 + .../orchestrator/OrchestratorService.kt | 11 +++- .../push/events/PushMessageEnqueuedEvent.kt | 9 --- .../MessagesExportRequestedPayload.kt | 17 ------ .../modules/receiver/EventsReceiver.kt | 30 ++++++++++ .../modules/receiver/ReceiverService.kt | 10 ++++ .../events/MessagesExportRequestedEvent.kt | 28 +++++++++ .../capcom/smsgateway/services/PushService.kt | 58 +++++++------------ 18 files changed, 249 insertions(+), 78 deletions(-) create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEvent.kt rename app/src/main/java/me/capcom/smsgateway/modules/{push/Event.kt => events/ExternalEventType.kt} (80%) create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/events/MessageEnqueuedEvent.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/events/SettingsUpdatedEvent.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/events/WebhooksUpdatedEvent.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/orchestrator/EventsRouter.kt delete mode 100644 app/src/main/java/me/capcom/smsgateway/modules/push/events/PushMessageEnqueuedEvent.kt delete mode 100644 app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/receiver/EventsReceiver.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessagesExportRequestedEvent.kt diff --git a/app/src/main/java/me/capcom/smsgateway/helpers/SSEManager.kt b/app/src/main/java/me/capcom/smsgateway/helpers/SSEManager.kt index 39dc8e27..289cecc9 100644 --- a/app/src/main/java/me/capcom/smsgateway/helpers/SSEManager.kt +++ b/app/src/main/java/me/capcom/smsgateway/helpers/SSEManager.kt @@ -29,7 +29,7 @@ class SSEManager( private val isDisconnecting = AtomicBoolean(false) // Event callbacks - var onEvent: ((type: String, data: String) -> Unit)? = null + var onEvent: ((type: String?, data: String) -> Unit)? = null var onConnected: (() -> Unit)? = null var onError: ((Throwable?) -> Unit)? = null var onClosed: (() -> Unit)? = null @@ -60,7 +60,7 @@ class SSEManager( data: String ) { Log.d(TAG, "Event received: $type - $data") - onEvent?.invoke(type ?: "message", data) + onEvent?.invoke(type, data) } override fun onClosed(eventSource: EventSource) { diff --git a/app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEvent.kt new file mode 100644 index 00000000..0ce1a984 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEvent.kt @@ -0,0 +1,6 @@ +package me.capcom.smsgateway.modules.events + +data class ExternalEvent( + val type: ExternalEventType, + val data: String?, +) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt b/app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEventType.kt similarity index 80% rename from app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt rename to app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEventType.kt index 92a25fd7..15714788 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/push/Event.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/events/ExternalEventType.kt @@ -1,8 +1,8 @@ -package me.capcom.smsgateway.modules.push +package me.capcom.smsgateway.modules.events import com.google.gson.annotations.SerializedName -enum class Event { +enum class ExternalEventType { @SerializedName("MessageEnqueued") MessageEnqueued, diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt index 99737271..77acd017 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt @@ -6,11 +6,15 @@ import kotlinx.coroutines.launch import me.capcom.smsgateway.domain.EntitySource import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.modules.events.EventsReceiver +import me.capcom.smsgateway.modules.gateway.events.MessageEnqueuedEvent +import me.capcom.smsgateway.modules.gateway.events.SettingsUpdatedEvent +import me.capcom.smsgateway.modules.gateway.events.WebhooksUpdatedEvent import me.capcom.smsgateway.modules.gateway.workers.PullMessagesWorker import me.capcom.smsgateway.modules.gateway.workers.SendStateWorker +import me.capcom.smsgateway.modules.gateway.workers.SettingsUpdateWorker +import me.capcom.smsgateway.modules.gateway.workers.WebhooksUpdateWorker import me.capcom.smsgateway.modules.messages.events.MessageStateChangedEvent import me.capcom.smsgateway.modules.ping.events.PingEvent -import me.capcom.smsgateway.modules.push.events.PushMessageEnqueuedEvent import org.koin.core.component.get class EventsReceiver : EventsReceiver() { @@ -20,8 +24,8 @@ class EventsReceiver : EventsReceiver() { override suspend fun collect(eventBus: EventBus) { coroutineScope { launch { - Log.d("EventsReceiver", "launched PushMessageEnqueuedEvent") - eventBus.collect { event -> + Log.d("EventsReceiver", "launched MessageEnqueuedEvent") + eventBus.collect { event -> Log.d("EventsReceiver", "Event: $event") if (!settings.enabled) return@collect @@ -53,6 +57,28 @@ class EventsReceiver : EventsReceiver() { PullMessagesWorker.start(get()) } } + + launch { + Log.d("EventsReceiver", "launched WebhooksUpdatedEvent") + eventBus.collect { + Log.d("EventsReceiver", "Event: $it") + + if (!settings.enabled) return@collect + + WebhooksUpdateWorker.start(get()) + } + } + + launch { + Log.d("EventsReceiver", "launched SettingsUpdatedEvent") + eventBus.collect { + Log.d("EventsReceiver", "Event: $it") + + if (!settings.enabled) return@collect + + SettingsUpdateWorker.start(get()) + } + } } } diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/MessageEnqueuedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/MessageEnqueuedEvent.kt new file mode 100644 index 00000000..795a1a15 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/MessageEnqueuedEvent.kt @@ -0,0 +1,9 @@ +package me.capcom.smsgateway.modules.gateway.events + +import me.capcom.smsgateway.modules.events.AppEvent + +class MessageEnqueuedEvent : AppEvent(NAME) { + companion object { + const val NAME = "MessageEnqueuedEvent" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/SettingsUpdatedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/SettingsUpdatedEvent.kt new file mode 100644 index 00000000..f04d511b --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/SettingsUpdatedEvent.kt @@ -0,0 +1,10 @@ +package me.capcom.smsgateway.modules.gateway.events + +import me.capcom.smsgateway.modules.events.AppEvent + +class SettingsUpdatedEvent : AppEvent(NAME) { + + companion object { + const val NAME = "SettingsUpdatedEvent" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/WebhooksUpdatedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/WebhooksUpdatedEvent.kt new file mode 100644 index 00000000..ca259044 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/events/WebhooksUpdatedEvent.kt @@ -0,0 +1,9 @@ +package me.capcom.smsgateway.modules.gateway.events + +import me.capcom.smsgateway.modules.events.AppEvent + +class WebhooksUpdatedEvent : AppEvent(NAME) { + companion object { + const val NAME = "WebhooksUpdatedEvent" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt index e80c711d..55750646 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt @@ -9,17 +9,22 @@ import android.os.IBinder import android.os.PowerManager import android.util.Log import me.capcom.smsgateway.helpers.SSEManager -import me.capcom.smsgateway.modules.events.EventBus +import me.capcom.smsgateway.modules.events.ExternalEvent +import me.capcom.smsgateway.modules.events.ExternalEventType import me.capcom.smsgateway.modules.gateway.GatewaySettings +import me.capcom.smsgateway.modules.logs.LogsService +import me.capcom.smsgateway.modules.logs.db.LogEntry import me.capcom.smsgateway.modules.notifications.NotificationsService -import org.koin.android.ext.android.get +import me.capcom.smsgateway.modules.orchestrator.EventsRouter import org.koin.android.ext.android.inject class SSEForegroundService : Service() { private val settings: GatewaySettings by inject() - private val eventBus = get() + + private val eventsRouter by inject() private val notificationsSvc: NotificationsService by inject() + private val logsService: LogsService by inject() private val wakeLock: PowerManager.WakeLock by lazy { (getSystemService(Context.POWER_SERVICE) as PowerManager).run { @@ -43,6 +48,19 @@ class SSEForegroundService : Service() { .apply { onEvent = { event, data -> Log.d("SSEForegroundService", "$event: $data") + + try { + processEvent(event, data) + } catch (e: Throwable) { + e.printStackTrace() + + logsService.insert( + LogEntry.Priority.ERROR, + "SSEForegroundService", + "Failed to process event", + mapOf("event" to event, "data" to data) + ) + } } } } @@ -76,6 +94,22 @@ class SSEForegroundService : Service() { return null } + private fun processEvent(event: String?, data: String) { + val type = try { + event?.let { ExternalEventType.valueOf(it) } + ?: ExternalEventType.MessageEnqueued + } catch (e: Throwable) { + throw RuntimeException("Unknown event type: $event", e) + } + + eventsRouter.route( + ExternalEvent( + type = type, + data = data + ) + ) + } + override fun onDestroy() { sseManager.disconnect() if (wifiLock.isHeld) { diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/workers/WebhooksUpdateWorker.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/workers/WebhooksUpdateWorker.kt index 2f744f17..7d7f2054 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/workers/WebhooksUpdateWorker.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/workers/WebhooksUpdateWorker.kt @@ -46,7 +46,7 @@ class WebhooksUpdateWorker(appContext: Context, params: WorkerParameters) : } companion object { - private const val NAME = "CloudUpdateWorker" + private const val NAME = "WebhooksUpdateWorker" fun start(context: Context) { val work = PeriodicWorkRequestBuilder( diff --git a/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/EventsRouter.kt b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/EventsRouter.kt new file mode 100644 index 00000000..bc1c6f13 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/EventsRouter.kt @@ -0,0 +1,47 @@ +package me.capcom.smsgateway.modules.orchestrator + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import me.capcom.smsgateway.modules.events.EventBus +import me.capcom.smsgateway.modules.events.ExternalEvent +import me.capcom.smsgateway.modules.events.ExternalEventType +import me.capcom.smsgateway.modules.gateway.events.MessageEnqueuedEvent +import me.capcom.smsgateway.modules.gateway.events.SettingsUpdatedEvent +import me.capcom.smsgateway.modules.gateway.events.WebhooksUpdatedEvent +import me.capcom.smsgateway.modules.receiver.events.MessagesExportRequestedEvent + +class EventsRouter( + private val eventBus: EventBus +) { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + fun route(event: ExternalEvent) { + scope.launch { + when (event.type) { + ExternalEventType.MessageEnqueued -> + eventBus.emit( + MessageEnqueuedEvent() + ) + + ExternalEventType.WebhooksUpdated -> + eventBus.emit( + WebhooksUpdatedEvent() + ) + + ExternalEventType.MessagesExportRequested -> + eventBus.emit( + MessagesExportRequestedEvent.withPayload( + requireNotNull(event.data) + ) + ) + + ExternalEventType.SettingsUpdated -> + eventBus.emit( + SettingsUpdatedEvent() + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/Module.kt index 457d6221..9ef0158d 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/Module.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/Module.kt @@ -5,6 +5,7 @@ import org.koin.dsl.module val orchestratorModule = module { singleOf(::OrchestratorService) + singleOf(::EventsRouter) } val MODULE_NAME = "orchestrator" diff --git a/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/OrchestratorService.kt b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/OrchestratorService.kt index 2f0698fa..52d3246c 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/OrchestratorService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/orchestrator/OrchestratorService.kt @@ -10,6 +10,7 @@ import me.capcom.smsgateway.modules.logs.LogsService import me.capcom.smsgateway.modules.logs.db.LogEntry import me.capcom.smsgateway.modules.messages.MessagesService import me.capcom.smsgateway.modules.ping.PingService +import me.capcom.smsgateway.modules.receiver.ReceiverService import me.capcom.smsgateway.modules.webhooks.WebHooksService class OrchestratorService( @@ -17,6 +18,7 @@ class OrchestratorService( private val gatewaySvc: GatewayService, private val localServerSvc: LocalServerService, private val webHooksSvc: WebHooksService, + private val receiverService: ReceiverService, private val pingSvc: PingService, private val logsSvc: LogsService, private val settings: SettingsHelper, @@ -28,12 +30,13 @@ class OrchestratorService( logsSvc.start(context) messagesSvc.start(context) - gatewaySvc.start(context) webHooksSvc.start(context) + gatewaySvc.start(context) try { localServerSvc.start(context) pingSvc.start(context) + receiverService.start(context) } catch (e: Throwable) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && e is ForegroundServiceStartNotAllowedException @@ -46,15 +49,17 @@ class OrchestratorService( return } - throw e; + throw e } } fun stop(context: Context) { + receiverService.stop(context) pingSvc.stop(context) - webHooksSvc.stop(context) localServerSvc.stop(context) + gatewaySvc.stop(context) + webHooksSvc.stop(context) messagesSvc.stop(context) logsSvc.stop(context) } diff --git a/app/src/main/java/me/capcom/smsgateway/modules/push/events/PushMessageEnqueuedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/push/events/PushMessageEnqueuedEvent.kt deleted file mode 100644 index 60a7d822..00000000 --- a/app/src/main/java/me/capcom/smsgateway/modules/push/events/PushMessageEnqueuedEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package me.capcom.smsgateway.modules.push.events - -import me.capcom.smsgateway.modules.events.AppEvent - -class PushMessageEnqueuedEvent : AppEvent(NAME) { - companion object { - private const val NAME = "MessageEnqueuedEvent" - } -} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt b/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt deleted file mode 100644 index 135b0bcd..00000000 --- a/app/src/main/java/me/capcom/smsgateway/modules/push/payloads/MessagesExportRequestedPayload.kt +++ /dev/null @@ -1,17 +0,0 @@ -package me.capcom.smsgateway.modules.push.payloads - -import com.google.gson.GsonBuilder -import me.capcom.smsgateway.extensions.configure -import java.util.Date - -data class MessagesExportRequestedPayload( - val since: Date, - val until: Date, -) { - companion object { - fun from(json: String): MessagesExportRequestedPayload { - val gson = GsonBuilder().configure().create() - return gson.fromJson(json, MessagesExportRequestedPayload::class.java) - } - } -} diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/EventsReceiver.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/EventsReceiver.kt new file mode 100644 index 00000000..d73f2d64 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/EventsReceiver.kt @@ -0,0 +1,30 @@ +package me.capcom.smsgateway.modules.receiver + +import android.util.Log +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import me.capcom.smsgateway.modules.events.EventBus +import me.capcom.smsgateway.modules.events.EventsReceiver +import me.capcom.smsgateway.modules.receiver.events.MessagesExportRequestedEvent +import org.koin.core.component.get +import org.koin.core.component.inject + +class EventsReceiver : EventsReceiver() { + private val service by inject() + + override suspend fun collect(eventBus: EventBus) { + coroutineScope { + launch { + Log.d("EventsReceiver", "launched MessagesExportRequestedEvent") + eventBus.collect { event -> + Log.d("EventsReceiver", "Event: $event") + + service.export( + get(), + event.since to event.until, + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt index 8535c2f4..0eba9677 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/ReceiverService.kt @@ -19,6 +19,16 @@ class ReceiverService : KoinComponent { private val webHooksService: WebHooksService by inject() private val logsService: LogsService by inject() + private val eventsReceiver by lazy { EventsReceiver() } + + fun start(context: Context) { + eventsReceiver.start() + } + + fun stop(context: Context) { + eventsReceiver.stop() + } + fun export(context: Context, period: Pair) { logsService.insert( LogEntry.Priority.DEBUG, diff --git a/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessagesExportRequestedEvent.kt b/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessagesExportRequestedEvent.kt new file mode 100644 index 00000000..892baa1a --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/receiver/events/MessagesExportRequestedEvent.kt @@ -0,0 +1,28 @@ +package me.capcom.smsgateway.modules.receiver.events + +import com.google.gson.GsonBuilder +import com.google.gson.annotations.SerializedName +import me.capcom.smsgateway.extensions.configure +import me.capcom.smsgateway.modules.events.AppEvent +import java.util.Date + +class MessagesExportRequestedEvent( + val since: Date, + val until: Date, +) : AppEvent(NAME) { + data class Payload( + @SerializedName("since") + val since: Date, + @SerializedName("until") + val until: Date, + ) + + companion object { + const val NAME = "MessagesExportRequestedEvent" + + fun withPayload(payload: String): MessagesExportRequestedEvent { + val obj = GsonBuilder().configure().create().fromJson(payload, Payload::class.java) + return MessagesExportRequestedEvent(obj.since, obj.until) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt index 63509c64..9eaa2605 100644 --- a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt +++ b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt @@ -6,22 +6,13 @@ import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch import me.capcom.smsgateway.helpers.SettingsHelper -import me.capcom.smsgateway.modules.events.EventBus +import me.capcom.smsgateway.modules.events.ExternalEvent +import me.capcom.smsgateway.modules.events.ExternalEventType import me.capcom.smsgateway.modules.gateway.workers.RegistrationWorker -import me.capcom.smsgateway.modules.gateway.workers.SettingsUpdateWorker -import me.capcom.smsgateway.modules.gateway.workers.WebhooksUpdateWorker import me.capcom.smsgateway.modules.logs.LogsService import me.capcom.smsgateway.modules.logs.db.LogEntry -import me.capcom.smsgateway.modules.push.Event -import me.capcom.smsgateway.modules.push.events.PushMessageEnqueuedEvent -import me.capcom.smsgateway.modules.push.payloads.MessagesExportRequestedPayload -import me.capcom.smsgateway.modules.receiver.ReceiverService +import me.capcom.smsgateway.modules.orchestrator.EventsRouter import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koin.core.component.inject @@ -29,8 +20,8 @@ import org.koin.core.component.inject class PushService : FirebaseMessagingService(), KoinComponent { private val settingsHelper by inject() - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private val eventBus by inject() + private val logsService by inject() + private val eventsRouter by inject() override fun onNewToken(token: String) { settingsHelper.fcmToken = token @@ -42,36 +33,27 @@ class PushService : FirebaseMessagingService(), KoinComponent { try { Log.d(this.javaClass.name, message.data.toString()) - val event = message.data["event"]?.let { Event.valueOf(it) } ?: Event.MessageEnqueued + val event = message.data["event"]?.let { ExternalEventType.valueOf(it) } + ?: ExternalEventType.MessageEnqueued val data = message.data["data"] - when (event) { - Event.MessageEnqueued -> scope.launch { eventBus.emit(PushMessageEnqueuedEvent()) } - Event.WebhooksUpdated -> WebhooksUpdateWorker.start(this) - Event.MessagesExportRequested -> data - ?.let { - MessagesExportRequestedPayload.from( - data - ) - } - ?.let { payload -> - get().export( - this, - payload.since to payload.until - ) - } - Event.SettingsUpdated -> SettingsUpdateWorker.start(this) - } + eventsRouter.route( + ExternalEvent( + type = event, + data = data, + ) + ) } catch (e: Throwable) { - e.printStackTrace() + Log.e(this.javaClass.name, "Error processing push message", e) + get().insert( + priority = LogEntry.Priority.ERROR, + module = this.javaClass.simpleName, + message = "Failed to process push message: ${e.message}", + mapOf("error" to e.toString()) + ) } } - override fun onDestroy() { - scope.cancel() - super.onDestroy() - } - companion object : KoinComponent { fun register(context: Context): Unit { val logger = get() From 7e3aef848fbd14896a4eeaf6a3a1935a7d784fb8 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Thu, 31 Jul 2025 10:35:15 +0700 Subject: [PATCH 3/5] [notifications] add realtime events notification --- .../gateway/services/SSEForegroundService.kt | 7 +- .../notifications/NotificationsService.kt | 2 + .../drawable-hdpi/notif_realtime_events.png | Bin 0 -> 921 bytes .../drawable-mdpi/notif_realtime_events.png | Bin 0 -> 630 bytes .../drawable-xhdpi/notif_realtime_events.png | Bin 0 -> 1393 bytes .../drawable-xxhdpi/notif_realtime_events.png | Bin 0 -> 2117 bytes .../notif_realtime_events.png | Bin 0 -> 3436 bytes app/src/main/res/values/strings.xml | 69 +++++++++++------- 8 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 app/src/main/res/drawable-hdpi/notif_realtime_events.png create mode 100644 app/src/main/res/drawable-mdpi/notif_realtime_events.png create mode 100644 app/src/main/res/drawable-xhdpi/notif_realtime_events.png create mode 100644 app/src/main/res/drawable-xxhdpi/notif_realtime_events.png create mode 100644 app/src/main/res/drawable-xxxhdpi/notif_realtime_events.png diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt index 55750646..3a346d6f 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt @@ -8,6 +8,7 @@ import android.os.Build import android.os.IBinder import android.os.PowerManager import android.util.Log +import me.capcom.smsgateway.R import me.capcom.smsgateway.helpers.SSEManager import me.capcom.smsgateway.modules.events.ExternalEvent import me.capcom.smsgateway.modules.events.ExternalEventType @@ -79,11 +80,11 @@ class SSEForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = notificationsSvc.makeNotification( this, - NotificationsService.NOTIFICATION_ID_PING_SERVICE, - "Listening to the server events..." + NotificationsService.NOTIFICATION_ID_REALTIME_EVENTS, + getString(R.string.listening_to_the_server_events) ) - startForeground(NotificationsService.NOTIFICATION_ID_PING_SERVICE, notification) + startForeground(NotificationsService.NOTIFICATION_ID_REALTIME_EVENTS, notification) sseManager.connect() diff --git a/app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt b/app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt index 56681a47..f7540494 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt @@ -24,6 +24,7 @@ class NotificationsService( NOTIFICATION_ID_PING_SERVICE to R.drawable.notif_ping, NOTIFICATION_ID_SETTINGS_CHANGED to R.drawable.notif_settings, NOTIFICATION_ID_SMS_RECEIVED_WEBHOOK to R.drawable.notif_webhook_registered, + NOTIFICATION_ID_REALTIME_EVENTS to R.drawable.notif_realtime_events, ) private val builders = mapOf NotificationCompat.Builder>( @@ -91,5 +92,6 @@ class NotificationsService( const val NOTIFICATION_ID_PING_SERVICE = 4 const val NOTIFICATION_ID_SETTINGS_CHANGED = 5 const val NOTIFICATION_ID_SMS_RECEIVED_WEBHOOK = 6 + const val NOTIFICATION_ID_REALTIME_EVENTS = 7 } } \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/notif_realtime_events.png b/app/src/main/res/drawable-hdpi/notif_realtime_events.png new file mode 100644 index 0000000000000000000000000000000000000000..27f793cbc5235f2512759b3ca004b4022f7e70e0 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbB7>k44ofy`glX(f`xTHpSruq6Z zXaU(A42G$TlC0TWzSWdSpS4N|DZ_xKM`IkTsWV@L$& z+iBh&A%POdrR7ukHLELE7Mb?Jp=D-LwYsVTB@vZgg!{;lg{U-y{DcspC~4F0;$bDx+0dv5*w-p2AFpm+#+Js%7&rRSqtYcim=z51W zS!0vfnMU~ivsy*Zx!`GI}L0sQY5in}S=E8j4ZC^M;3{L|1s;p86e(hp*1 z+%^P+<=mV0*5gj4!RdE@SW4QCaoRWBXVrhe_#@~J&%U+%O=V)Oc9pLenq3w7JiYTG zkM1J5HzKW<9|V>ij(gEEdqFIxcc9tkgHakkj>z)&#goA85nANoQL$n0RN z-7w45`Ha@9Td6zDT&&whNp1OYR znC34z(TYi2N{L(jt-5g9CyTFhuU*zT;*)(YP<6%hU)6ny8+UbHXKPRP>Ydijd@mw*_wBx;X?@tMK$SpmtevJ2N(W%}KBHM(L zdFEf;#8t%B{mWzf0kH_D4OM&I*4gbiAiabCqQ$~B;;eTP-U-(v@-X@qoX)7|-z2aZCxI)YZdBS|wSMru=%8+&!e-+RrvEy#Et%e*I3wZoC*k}5)BM{m%?zBl tP0Q)ehOVg>w+I}~GGso=^G8_n2k-Oe8~WY3kFEw~3r|-+mvv4FO#la@fPVl0 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/notif_realtime_events.png b/app/src/main/res/drawable-mdpi/notif_realtime_events.png new file mode 100644 index 0000000000000000000000000000000000000000..fc80266311a796f2f9dd8cf5912dfcb8567aa6b7 GIT binary patch literal 630 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjjKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDlgf?186?V~7Xu z+bgHFT@odZf0TSu7wIwiQcsSD%JvB-n6#JtVcfiNv(m|1IcsldaI2o0wCV66Rq5$L zVq&LeI3*fu@~S=lecJMIkMZFnQ+k)%%`<-gd*1i5d(#hpl)wL`;q&ZYJFZUjP?^o8 zuUF7zz5Mb|r){onYgK+W3HWLowp6oyHS9Q5CtniQrO0;s^?(4^A;rr*@hnnU+DQLvftiusQ2M4n=a)$4<+k(7BfWE z3MQ$^em-op=G4|%yn7Z2?7pz^3YS`7YxIg-;jWv%6|UGxZCfw5w$Nf#<-J2(UxFlZ z3m)>Ax9JrLiZp)z^6~p>#&4O|I>i?Mw5*g1pJ80Yyi>lkL%Nf#V%b-%J2y7(S=n#X zSH|OTJaV;X6~o;7J}oy-M;0xzSd?^Z&6C5g&b6Gmf9+2t>n@8di?iOdZZa{qR$g=e zcF3k_R!85uA~Bl_-)=hf<~X)*6?xUu@1T9D>?G%!_b(#9KW*sLP#5Q!z;>7Y@A;XH z-+sAId?B~OIJZ4-%ezBTLDFY-G2dPJ_Ur!b`)@Rwn;u~))O~r*2^8m^u6{1-oD!M< DIvNQ; literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/notif_realtime_events.png b/app/src/main/res/drawable-xhdpi/notif_realtime_events.png new file mode 100644 index 0000000000000000000000000000000000000000..30c9981557724f7fddce0cadf1f08d1882153f9e GIT binary patch literal 1393 zcmV-%1&;cOP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91FrWhf1ONa40RR91FaQ7m0NXcg3;+NG+et)0RA>e5m}`htRTO}|R+?y9 zHc|MPJs%engW^5)u_8LNGW=CQ)Qo){h`6f}}#qAfzY*O$jp<>>ZjDQW=vm zrCF&x7*wWl8Z{lAzVFUC!iN1*?aA^*Is+?b7#Kdwg!$lVY=fGr zoCJ=Ig%z*?n&BY)3^TxqsQ1HWIIAirp;I`gv!>59Xo0`rNjL=-!cll=z$SpPT38E4 z$HAmCqAmA_Q9cYi;A+?opMdEz4o1KlFn#90P-udmU|7;567K)7xrpF z+xuWVOa+hCxiA{Og0CSOsCZ-j)zbMwI0T;E=?w(~U}F;0!vw3)?tufa1ctzOU^*GN zx1j@0@6SK$2N!$EyYQy&U@1)#2X&*l36szX&-%eZTWvi8>tGfrrb!E!Qm4TtIG?fp zMw_zA$Q~8{H*_%mZQnfC`SCP-4km#slAjbhDq+1&%!O%iJG=wegK{gj4xsB`18jzN zcoE!Z$}8{%{N_U{VUWpS3N6$C`bKDnC&1ICn0(618u%t?biAN(`0QzuIz5w8D2!$p z0H23|H|2cz1q!8;ikIMiI1Ed`mHGi(jkDoSFh#6?5b|HBOk@hUx&tE^y9lByP{*@p zx@Yz}xD3JwQ_Rae{yqxN!e{U&DD|ONl-(PRRw8>rTdvk}csXoWicVUAxE$B+NuWb+ z#(Urca5LTwyTQ$BRL8+6P2KQc0(f#^P3CBlXK+8F)@FhET4c@ECj# z2HaHPV`g%E-od77^7FdrF0vNfiP;lFJlUAIA4o1*B^gM}V+{J0xD0NBZO{(8;BnI8cXY4N)(9>DFQzy#jT|#f zO|@Cz2KPjJ9u{Hu2)ZOmoPbtvYy_$n>fi#n25w=XCUjnCb2)|gpbNa2O+6(#MqJst zo#4gzBR1{-?+K39LK`@VyioQd%x@xp_g)p&S3)Ok;y53oQFL{@dDnyKP)`K?hOd~U znLwDoI)4eCz6R!O1J3WLnCHPjX(kZ*XsVdE9bn|QhA*mG6ZLDsUsL`Ep#=u+2S_Dm zj>Ds1C9Hypl^8P{Ba1*5fh+=91hNSHZzJ#zg^a3FNqE>s00000NkvXXu0mjfzZPQz literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/notif_realtime_events.png b/app/src/main/res/drawable-xxhdpi/notif_realtime_events.png new file mode 100644 index 0000000000000000000000000000000000000000..2f10ca3090ccc2e4ac0935b6c9ad153ae7d35c52 GIT binary patch literal 2117 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 WpDgXEyJE$krE|#~g{(8Z8UH_4wS_qV literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9ecb128..edcf9e12 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,27 +1,31 @@ %1$s - API URL API URL, private token, credentials, etc. + API URL SMS Gateway App version (build) - Battery optimization Battery optimization already disabled - Battery optimization is not supported on this device + Battery optimization is not + supported on this device + Battery optimization Cancel Continue By Code can affect battery life - Click Continue to create an account. No personal information is required.\nBy continuing, you agree to our Privacy Policy at https://docs.sms-gate.app/privacy/policy/ - Cloud - Cloud server + Click + Continue to create an account. No personal information is required.\nBy continuing, you + agree to our Privacy Policy at https://docs.sms-gate.app/privacy/policy/ Cloud server… + Cloud server + Cloud Copied Credentials Delays, limits, etc. Delays, seconds Delete after, days - Device Device ID + Device Disabled Enabled @@ -37,22 +41,24 @@ Internet connection: unavailable Interval (seconds) Invalid URL - %1$s is not a valid port. Must be between 1024 and 65535 + %1$s is not a valid port. Must + be between 1024 and 65535 Password: Username: Limits List of last 50 log entries - Local + Listening to the server events... Local address: Local Server… Local SMS Gateway notifications - Login Code + Local Login Code, expires %1$s + Login Code Logs Maximum - Messages… Messages count Messages + Messages… Minimum More settings… n/a @@ -61,18 +67,20 @@ not registered Not set SMS Gateway - Online status at the cost of battery life + Online status at the cost of battery + life Passphrase - Password Password changed successfully Password must be at least 14 characters Password must be at least 8 characters + Password Period - Ping Ping service is active - Please enter one-time code displayed on already registered device - Port + Ping + Please enter + one-time code displayed on already registered device Port, credentials, etc. + Port Private Token Public address: Require Internet connection @@ -81,11 +89,12 @@ Retry count Sending messages… Sending webhook… - Server Server address: + Server Set maximum value to activate api.sms-gate.app - Settings changed via API. Restart the app to apply changes. + Settings changed via + API. Restart the app to apply changes. <a href>%1$s:%2$d</a> Not available Local server @@ -97,26 +106,32 @@ Sign In Sign Up Signing Key - sms - SMS Gateway SMS gateway is running on port %1$d + SMS Gateway + sms Success, long press to copy System Home MESSAGES SETTINGS - The webhook request will wait for an internet connection - To add a device to an existing account, please fill in the credentials below. - To apply the changes, restart the app using the button below. + The webhook request will + wait for an internet connection + To + add a device to an existing account, please fill in the credentials below. + To apply the changes, + restart the app using the button below. Use empty to disable - Use this code to sign in on another device - Username + Use this code to sign in on another + device Username must be at least 3 characters + Username View ID: %1$s View registered webhooks Registered Webhooks - Webhooks Webhooks… - You have %1$d SMS received webhooks registered. Please review them to avoid any security risks. + Webhooks + You + have %1$d SMS received webhooks registered. Please review them to avoid any security risks. \ No newline at end of file From b829b282558d039f223edb7895ad302b0266c9ab Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Thu, 31 Jul 2025 10:39:57 +0700 Subject: [PATCH 4/5] [push] improve logging --- app/src/main/java/me/capcom/smsgateway/services/PushService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt index 9eaa2605..3250799c 100644 --- a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt +++ b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt @@ -37,6 +37,8 @@ class PushService : FirebaseMessagingService(), KoinComponent { ?: ExternalEventType.MessageEnqueued val data = message.data["data"] + Log.d(this.javaClass.name, "Routing event: $event with data: $data") + eventsRouter.route( ExternalEvent( type = event, From 0a94d1e9c5d6aee4cfa1a69983b9decfe9699999 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Fri, 1 Aug 2025 09:47:03 +0700 Subject: [PATCH 5/5] [gateway] conditional SSE connection --- .../smsgateway/helpers/SettingsHelper.kt | 4 --- .../modules/gateway/EventsReceiver.kt | 14 +++++++++++ .../modules/gateway/GatewayService.kt | 25 +++++++++++-------- .../modules/gateway/GatewaySettings.kt | 5 ++++ .../gateway/services/SSEForegroundService.kt | 8 +++++- .../capcom/smsgateway/services/PushService.kt | 12 +++------ 6 files changed, 43 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt b/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt index edafc403..6c72fa12 100644 --- a/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt +++ b/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt @@ -27,10 +27,6 @@ class SettingsHelper(private val context: Context) { settings.edit { putBoolean(PREF_KEY_AUTOSTART, value) } } - var fcmToken: String? - get() = settings.getString(PREF_KEY_FCM_TOKEN, null) - set(value) = settings.edit { putString(PREF_KEY_FCM_TOKEN, value) } - private fun migrate() { // remove after 2025-11-28 val PREF_KEY_SERVER_TOKEN = "server_token" diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt index 77acd017..d4884b13 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/EventsReceiver.kt @@ -6,9 +6,11 @@ import kotlinx.coroutines.launch import me.capcom.smsgateway.domain.EntitySource import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.modules.events.EventsReceiver +import me.capcom.smsgateway.modules.gateway.events.DeviceRegisteredEvent import me.capcom.smsgateway.modules.gateway.events.MessageEnqueuedEvent import me.capcom.smsgateway.modules.gateway.events.SettingsUpdatedEvent import me.capcom.smsgateway.modules.gateway.events.WebhooksUpdatedEvent +import me.capcom.smsgateway.modules.gateway.services.SSEForegroundService import me.capcom.smsgateway.modules.gateway.workers.PullMessagesWorker import me.capcom.smsgateway.modules.gateway.workers.SendStateWorker import me.capcom.smsgateway.modules.gateway.workers.SettingsUpdateWorker @@ -79,6 +81,18 @@ class EventsReceiver : EventsReceiver() { SettingsUpdateWorker.start(get()) } } + + launch { + Log.d("EventsReceiver", "launched DeviceRegisteredEvent") + eventBus.collect { + Log.d("EventsReceiver", "Event: $it") + + if (!settings.enabled) return@collect + if (settings.fcmToken != null) return@collect + + SSEForegroundService.start(get()) + } + } } } diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt index 9a45e968..02a766dc 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayService.kt @@ -46,7 +46,6 @@ class GatewayService( PullMessagesWorker.start(context) WebhooksUpdateWorker.start(context) SettingsUpdateWorker.start(context) - SSEForegroundService.start(context) eventsReceiver.start() } @@ -109,9 +108,7 @@ class GatewayService( if (accessToken != null) { // if there's an access token, try to update push token try { - pushToken?.let { - updateDevice(it) - } + updateDevice(pushToken) return } catch (e: ClientRequestException) { // if token is invalid, try to register new one @@ -135,6 +132,8 @@ class GatewayService( registerMode.login to registerMode.password ) } + + this.settings.fcmToken = pushToken this.settings.registrationInfo = response events.emit( @@ -156,19 +155,23 @@ class GatewayService( } } - internal suspend fun updateDevice(pushToken: String) { + internal suspend fun updateDevice(pushToken: String?) { if (!settings.enabled) return val settings = settings.registrationInfo ?: return val accessToken = settings.token - api.devicePatch( - accessToken, - GatewayApi.DevicePatchRequest( - settings.id, - pushToken + pushToken?.let { + api.devicePatch( + accessToken, + GatewayApi.DevicePatchRequest( + settings.id, + it + ) ) - ) + } + + this.settings.fcmToken = pushToken events.emit( DeviceRegisteredEvent.Success( diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt index 4c418bde..37ec5e6a 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt @@ -19,6 +19,10 @@ class GatewaySettings( get() = storage.get(REGISTRATION_INFO) set(value) = storage.set(REGISTRATION_INFO, value) + var fcmToken: String? + get() = storage.get(FCM_TOKEN) + set(value) = storage.set(FCM_TOKEN, value) + val username: String? get() = registrationInfo?.login val password: String? @@ -32,6 +36,7 @@ class GatewaySettings( companion object { private const val REGISTRATION_INFO = "REGISTRATION_INFO" private const val ENABLED = "ENABLED" + private const val FCM_TOKEN = "fcm_token" private const val CLOUD_URL = "cloud_url" private const val PRIVATE_TOKEN = "private_token" diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt index 3a346d6f..29828bf9 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt @@ -119,7 +119,13 @@ class SSEForegroundService : Service() { if (wakeLock.isHeld) { wakeLock.release() } - stopForeground(true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + @Suppress("DEPRECATION") + stopForeground(true) + } super.onDestroy() } diff --git a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt index 3250799c..189214be 100644 --- a/app/src/main/java/me/capcom/smsgateway/services/PushService.kt +++ b/app/src/main/java/me/capcom/smsgateway/services/PushService.kt @@ -2,11 +2,9 @@ package me.capcom.smsgateway.services import android.content.Context import android.util.Log -import com.google.android.gms.tasks.OnCompleteListener import com.google.firebase.messaging.FirebaseMessaging import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -import me.capcom.smsgateway.helpers.SettingsHelper import me.capcom.smsgateway.modules.events.ExternalEvent import me.capcom.smsgateway.modules.events.ExternalEventType import me.capcom.smsgateway.modules.gateway.workers.RegistrationWorker @@ -18,14 +16,10 @@ import org.koin.core.component.get import org.koin.core.component.inject class PushService : FirebaseMessagingService(), KoinComponent { - private val settingsHelper by inject() - private val logsService by inject() private val eventsRouter by inject() override fun onNewToken(token: String) { - settingsHelper.fcmToken = token - RegistrationWorker.start(this@PushService, token, true) } @@ -47,7 +41,7 @@ class PushService : FirebaseMessagingService(), KoinComponent { ) } catch (e: Throwable) { Log.e(this.javaClass.name, "Error processing push message", e) - get().insert( + logsService.insert( priority = LogEntry.Priority.ERROR, module = this.javaClass.simpleName, message = "Failed to process push message: ${e.message}", @@ -65,7 +59,7 @@ class PushService : FirebaseMessagingService(), KoinComponent { module = PushService::class.java.simpleName, message = "FCM registration started" ) - FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task -> + FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (!task.isSuccessful || task.isCanceled) { logger.insert( priority = LogEntry.Priority.ERROR, @@ -90,7 +84,7 @@ class PushService : FirebaseMessagingService(), KoinComponent { // Log and toast RegistrationWorker.start(context, token, false) - }) + } } } } \ No newline at end of file