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