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, 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/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/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..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,11 +6,17 @@ 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
+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 +26,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 +59,40 @@ 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())
+ }
+ }
+
+ 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 8c621706..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
@@ -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
@@ -52,6 +53,7 @@ class GatewayService(
fun stop(context: Context) {
eventsReceiver.stop()
+ SSEForegroundService.stop(context)
SettingsUpdateWorker.stop(context)
WebhooksUpdateWorker.stop(context)
PullMessagesWorker.stop(context)
@@ -106,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
@@ -132,6 +132,8 @@ class GatewayService(
registerMode.login to registerMode.password
)
}
+
+ this.settings.fcmToken = pushToken
this.settings.registrationInfo = response
events.emit(
@@ -153,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/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
new file mode 100644
index 00000000..29828bf9
--- /dev/null
+++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/services/SSEForegroundService.kt
@@ -0,0 +1,148 @@
+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.R
+import me.capcom.smsgateway.helpers.SSEManager
+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 me.capcom.smsgateway.modules.orchestrator.EventsRouter
+import org.koin.android.ext.android.inject
+
+class SSEForegroundService : Service() {
+ private val settings: GatewaySettings by inject()
+
+ 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 {
+ 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")
+
+ 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)
+ )
+ }
+ }
+ }
+ }
+
+ 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_REALTIME_EVENTS,
+ getString(R.string.listening_to_the_server_events)
+ )
+
+ startForeground(NotificationsService.NOTIFICATION_ID_REALTIME_EVENTS, notification)
+
+ sseManager.connect()
+
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ 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) {
+ wifiLock.release()
+ }
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ } else {
+ @Suppress("DEPRECATION")
+ 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
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/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/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..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,39 +2,24 @@ 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 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
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
-
RegistrationWorker.start(this@PushService, token, true)
}
@@ -42,36 +27,29 @@ 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)
- }
+ Log.d(this.javaClass.name, "Routing event: $event with data: $data")
+
+ eventsRouter.route(
+ ExternalEvent(
+ type = event,
+ data = data,
+ )
+ )
} catch (e: Throwable) {
- e.printStackTrace()
+ Log.e(this.javaClass.name, "Error processing push message", e)
+ logsService.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()
@@ -81,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,
@@ -106,7 +84,7 @@ class PushService : FirebaseMessagingService(), KoinComponent {
// Log and toast
RegistrationWorker.start(context, token, false)
- })
+ }
}
}
}
\ 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 00000000..27f793cb
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notif_realtime_events.png differ
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 00000000..fc802663
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notif_realtime_events.png differ
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 00000000..30c99815
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notif_realtime_events.png differ
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 00000000..2f10ca30
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/notif_realtime_events.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/notif_realtime_events.png b/app/src/main/res/drawable-xxxhdpi/notif_realtime_events.png
new file mode 100644
index 00000000..f2bb2231
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/notif_realtime_events.png differ
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