From e2345ddb8cfce40b2dd17284fdab8566293606ff Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Wed, 21 Feb 2024 16:15:28 +0700 Subject: [PATCH 1/6] [messages] pending messages queue --- .../7.json | 144 +++++++++++ .../me/capcom/smsgateway/data/AppDatabase.kt | 3 +- .../capcom/smsgateway/data/dao/MessageDao.kt | 4 + .../smsgateway/data/entities/Message.kt | 2 + .../modules/messages/MessagesService.kt | 226 +++++++++--------- .../repositories/MessagesRepository.kt | 1 + .../messages/workers/SendMessagesWorker.kt | 56 +++++ 7 files changed, 320 insertions(+), 116 deletions(-) create mode 100644 app/schemas/me.capcom.smsgateway.data.AppDatabase/7.json create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/messages/workers/SendMessagesWorker.kt diff --git a/app/schemas/me.capcom.smsgateway.data.AppDatabase/7.json b/app/schemas/me.capcom.smsgateway.data.AppDatabase/7.json new file mode 100644 index 0000000..445b9e3 --- /dev/null +++ b/app/schemas/me.capcom.smsgateway.data.AppDatabase/7.json @@ -0,0 +1,144 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "0638620d8ed8717433cb8e718cfc4646", + "entities": [ + { + "tableName": "Message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `text` TEXT NOT NULL, `withDeliveryReport` INTEGER NOT NULL DEFAULT 1, `simNumber` INTEGER, `validUntil` TEXT, `isEncrypted` INTEGER NOT NULL DEFAULT 0, `skipPhoneValidation` INTEGER NOT NULL DEFAULT 0, `source` TEXT NOT NULL DEFAULT 'Local', `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "withDeliveryReport", + "columnName": "withDeliveryReport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "simNumber", + "columnName": "simNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "validUntil", + "columnName": "validUntil", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "isEncrypted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "skipPhoneValidation", + "columnName": "skipPhoneValidation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Local'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageRecipient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `phoneNumber` TEXT NOT NULL, `state` TEXT NOT NULL, `error` TEXT, PRIMARY KEY(`messageId`, `phoneNumber`), FOREIGN KEY(`messageId`) REFERENCES `Message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phoneNumber", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "messageId", + "phoneNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "Message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "messageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0638620d8ed8717433cb8e718cfc4646')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt index da381e2..2ef576e 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt @@ -11,13 +11,14 @@ import me.capcom.smsgateway.data.entities.MessageRecipient @Database( entities = [Message::class, MessageRecipient::class], - version = 6, + version = 7, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), AutoMigration(from = 3, to = 4), AutoMigration(from = 4, to = 5), AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7), ] ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/me/capcom/smsgateway/data/dao/MessageDao.kt b/app/src/main/java/me/capcom/smsgateway/data/dao/MessageDao.kt index 4d0de97..6ee420b 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/dao/MessageDao.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/dao/MessageDao.kt @@ -14,6 +14,10 @@ interface MessageDao { @Query("SELECT * FROM message ORDER BY createdAt DESC LIMIT 50") fun selectLast(): LiveData> + @Transaction + @Query("SELECT * FROM message WHERE state = 'Pending' ORDER BY createdAt") + fun selectPending(): List + @Transaction @Query("SELECT * FROM message WHERE id = :id") fun get(id: String): MessageWithRecipients? diff --git a/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt b/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt index a71b650..43bae82 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt @@ -16,6 +16,8 @@ data class Message( val validUntil: Date?, @ColumnInfo(defaultValue = "0") val isEncrypted: Boolean, + @ColumnInfo(defaultValue = "0") + val skipPhoneValidation: Boolean, @ColumnInfo(defaultValue = "Local") val source: MessageSource, diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt index e1a54c0..0b2c7e0 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt @@ -13,12 +13,7 @@ import android.telephony.SubscriptionManager import android.telephony.TelephonyManager import android.util.Log import androidx.core.app.ActivityCompat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import me.capcom.smsgateway.data.dao.MessageDao import me.capcom.smsgateway.data.entities.Message import me.capcom.smsgateway.data.entities.MessageRecipient @@ -28,6 +23,7 @@ import me.capcom.smsgateway.modules.encryption.EncryptionService import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.modules.messages.data.SendRequest import me.capcom.smsgateway.modules.messages.events.MessageStateChangedEvent +import me.capcom.smsgateway.modules.messages.workers.SendMessagesWorker import me.capcom.smsgateway.receivers.EventsReceiver import java.util.Date @@ -39,36 +35,42 @@ class MessagesService( ) { val events = EventBus() - private val queue = Channel(Channel.Factory.UNLIMITED) private val countryCode: String? = (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).networkCountryIso init { - // another way is to use WorkManager: - // 1. Insert message to DB - // 2. Start worker with KEEP policy - // 3. Worker: select all Pending messages from DB - // 4. Worker: send one by one with delays setting Sent state - // 5. Worker: repeat from step 3 while there are Pending messages - // Notes: we need to dismiss old messages in Pending state, so we need to introduce TTL - scope.launch(Dispatchers.Default) { - for (msg in queue) { - try { - sendMessage(msg) - } catch (e: Exception) { - e.printStackTrace() - } + SendMessagesWorker.start(context) + } - // random delay from 100ms to 5s - if (settings.secondsBetweenMessages > 0) { - delay((0..settings.secondsBetweenMessages).random() * 1000L) - } - } + fun enqueueMessage(request: SendRequest) { + if (getMessage(request.message.id) != null) { + Log.d(this.javaClass.name, "Message already exists: ${request.message.id}") + return } - } - suspend fun enqueueMessage(request: SendRequest) { - queue.send(request) + val message = MessageWithRecipients( + Message( + request.message.id, + request.message.text, + request.params.withDeliveryReport, + request.params.simNumber, + request.params.validUntil, + request.message.isEncrypted, + request.params.skipPhoneValidation, + request.source, + ), + request.message.phoneNumbers.map { + MessageRecipient( + request.message.id, + it, + Message.State.Pending + ) + }, + ) + + dao.insert(message) + + SendMessagesWorker.start(context) } fun getMessage(id: String): MessageWithRecipients? { @@ -108,47 +110,41 @@ class MessagesService( updateState(id, phone, state, error) } - private suspend fun sendMessage(request: SendRequest) { - val message = MessageWithRecipients( - Message( - request.message.id, - request.message.text, - request.params.withDeliveryReport, - request.params.simNumber, - request.params.validUntil, - request.message.isEncrypted, - request.source, - ), - request.message.phoneNumbers.map { - MessageRecipient( - request.message.id, - it, - Message.State.Pending - ) - }, - ) + internal suspend fun sendPendingMessages(): Boolean { + val messages = dao.selectPending() + if (messages.isEmpty()) { + return false + } - dao.insert(message) + for (message in messages) { + sendMessage(message) + + if (settings.secondsBetweenMessages > 0) { + delay((0..settings.secondsBetweenMessages).random() * 1000L) + } + } + + return true + } - if (request.params.validUntil?.before(Date()) == true) { + private suspend fun sendMessage(request: MessageWithRecipients) { + if (request.message.validUntil?.before(Date()) == true) { updateState(request.message.id, null, Message.State.Failed, "TTL expired") return } - if (message.state != Message.State.Pending) { + if (request.state != Message.State.Pending) { // не ясно когда такая ситуация может возникнуть - Log.w(this.javaClass.simpleName, "Unexpected state for message: $message") - updateState(request.message.id, null, message.state) + Log.w(this.javaClass.simpleName, "Unexpected state for message: $request") + updateState(request.message.id, null, request.state) return } try { - sendSMS( - request - ) + sendSMS(request) } catch (e: Exception) { e.printStackTrace() - updateState(message.message.id, null, Message.State.Failed, "Sending: " + e.message) + updateState(request.message.id, null, Message.State.Failed, "Sending: " + e.message) } } @@ -183,14 +179,11 @@ class MessagesService( ) } - private suspend fun sendSMS( - request: SendRequest - ) { + private suspend fun sendSMS(request: MessageWithRecipients) { val message = request.message - val params = request.params val id = message.id - val smsManager: SmsManager = getSmsManager(params.simNumber?.let { it - 1 }) + val smsManager: SmsManager = getSmsManager(message.simNumber?.let { it - 1 }) @Suppress("NAME_SHADOWING") val messageText = when (message.isEncrypted) { @@ -198,67 +191,75 @@ class MessagesService( false -> message.text } - message.phoneNumbers.forEach { - val sentIntent = PendingIntent.getBroadcast( - context, - 0, - Intent( - EventsReceiver.ACTION_SENT, - Uri.parse("$id|$it"), - context, - EventsReceiver::class.java - ), - PendingIntent.FLAG_IMMUTABLE - ) - val deliveredIntent = when (params.withDeliveryReport) { - false -> null - true -> PendingIntent.getBroadcast( + request.recipients + .filter { it.state == Message.State.Pending } + .forEach { rcp -> + val sourcePhoneNumber = rcp.phoneNumber + val sentIntent = PendingIntent.getBroadcast( context, 0, Intent( - EventsReceiver.ACTION_DELIVERED, - Uri.parse("$id|$it"), + EventsReceiver.ACTION_SENT, + Uri.parse("$id|$sourcePhoneNumber"), context, EventsReceiver::class.java ), PendingIntent.FLAG_IMMUTABLE ) - } - - try { - val parts = smsManager.divideMessage(messageText) - val phoneNumber = when (message.isEncrypted) { - true -> encryptionService.decrypt(it) - false -> it - } - val normalizedPhoneNumber = when (params.skipPhoneValidation) { - true -> phoneNumber.filter { it.isDigit() || it == '+' } - false -> PhoneHelper.filterPhoneNumber(phoneNumber, countryCode ?: "RU") + val deliveredIntent = when (message.withDeliveryReport) { + false -> null + true -> PendingIntent.getBroadcast( + context, + 0, + Intent( + EventsReceiver.ACTION_DELIVERED, + Uri.parse("$id|$sourcePhoneNumber"), + context, + EventsReceiver::class.java + ), + PendingIntent.FLAG_IMMUTABLE + ) } - if (parts.size > 1) { - smsManager.sendMultipartTextMessage( - normalizedPhoneNumber, - null, - parts, - ArrayList(parts.map { sentIntent }), - deliveredIntent?.let { ArrayList(parts.map { deliveredIntent }) } - ) - } else { - smsManager.sendTextMessage( - normalizedPhoneNumber, - null, - messageText, - sentIntent, - deliveredIntent + try { + val parts = smsManager.divideMessage(messageText) + val phoneNumber = when (message.isEncrypted) { + true -> encryptionService.decrypt(sourcePhoneNumber) + false -> sourcePhoneNumber + } + val normalizedPhoneNumber = when (message.skipPhoneValidation) { + true -> phoneNumber.filter { it.isDigit() || it == '+' } + false -> PhoneHelper.filterPhoneNumber(phoneNumber, countryCode ?: "RU") + } + + if (parts.size > 1) { + smsManager.sendMultipartTextMessage( + normalizedPhoneNumber, + null, + parts, + ArrayList(parts.map { sentIntent }), + deliveredIntent?.let { ArrayList(parts.map { deliveredIntent }) } + ) + } else { + smsManager.sendTextMessage( + normalizedPhoneNumber, + null, + messageText, + sentIntent, + deliveredIntent + ) + } + + updateState(id, sourcePhoneNumber, Message.State.Processed) + } catch (th: Throwable) { + th.printStackTrace() + updateState( + id, + sourcePhoneNumber, + Message.State.Failed, + "Sending: " + th.message ) } - - updateState(id, it, Message.State.Processed) - } catch (th: Throwable) { - th.printStackTrace() - updateState(id, it, Message.State.Failed, "Sending: " + th.message) - } } } @@ -365,9 +366,4 @@ class MessagesService( else -> "UNKNOWN" } } - - companion object { - private val job = SupervisorJob() - private val scope = CoroutineScope(job) - } } \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/repositories/MessagesRepository.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/repositories/MessagesRepository.kt index 518f126..b8edb9b 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/messages/repositories/MessagesRepository.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/repositories/MessagesRepository.kt @@ -6,5 +6,6 @@ import me.capcom.smsgateway.data.dao.MessageDao class MessagesRepository(private val dao: MessageDao) { val lastMessages = dao.selectLast().distinctUntilChanged() + fun selectPending() = dao.selectPending() fun get(id: String) = dao.get(id) } \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/workers/SendMessagesWorker.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/workers/SendMessagesWorker.kt new file mode 100644 index 0000000..c8bfb23 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/workers/SendMessagesWorker.kt @@ -0,0 +1,56 @@ +package me.capcom.smsgateway.modules.messages.workers + +import android.content.Context +import androidx.work.BackoffPolicy +import androidx.work.CoroutineWorker +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.WorkerParameters +import me.capcom.smsgateway.modules.messages.MessagesService +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import java.util.concurrent.TimeUnit + +class SendMessagesWorker(appContext: Context, params: WorkerParameters) : + CoroutineWorker(appContext, params), KoinComponent { + + private val messagesSvc: MessagesService by inject() + + override suspend fun doWork(): Result { + return try { + while (messagesSvc.sendPendingMessages()) { + } + + Result.success() + } catch (e: Exception) { + e.printStackTrace() + + Result.retry() + } + } + + companion object { + private const val NAME = "SendMessagesWorker" + + fun start(context: Context) { + val work = OneTimeWorkRequestBuilder() + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setBackoffCriteria( + BackoffPolicy.LINEAR, + WorkRequest.MIN_BACKOFF_MILLIS, + TimeUnit.MILLISECONDS + ) + .build() + + WorkManager.getInstance(context) + .enqueueUniqueWork( + NAME, + ExistingWorkPolicy.KEEP, + work + ) + } + } +} \ No newline at end of file From c7c8340434e9e0c94f6993789ccae22dc4b2b3f9 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 26 Feb 2024 15:31:42 +0700 Subject: [PATCH 2/6] [tests][messages] add `skipPhoneValidation` to constructor --- .../data/entities/MessageWithRecipientsTest.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/me/capcom/smsgateway/data/entities/MessageWithRecipientsTest.kt b/app/src/test/java/me/capcom/smsgateway/data/entities/MessageWithRecipientsTest.kt index 93d1787..a2b5e59 100644 --- a/app/src/test/java/me/capcom/smsgateway/data/entities/MessageWithRecipientsTest.kt +++ b/app/src/test/java/me/capcom/smsgateway/data/entities/MessageWithRecipientsTest.kt @@ -16,7 +16,8 @@ class MessageWithRecipientsTest { isEncrypted = false, source = MessageSource.Local, state = Message.State.Pending, - createdAt = System.currentTimeMillis() + createdAt = System.currentTimeMillis(), + skipPhoneValidation = true, ) val recipients = listOf( MessageRecipient("1", "1234567890", Message.State.Pending, null), @@ -38,7 +39,8 @@ class MessageWithRecipientsTest { isEncrypted = false, source = MessageSource.Local, state = Message.State.Pending, - createdAt = System.currentTimeMillis() + createdAt = System.currentTimeMillis(), + skipPhoneValidation = true, ) val recipients = listOf( MessageRecipient("1", "1234567890", Message.State.Delivered, null), @@ -60,7 +62,8 @@ class MessageWithRecipientsTest { isEncrypted = false, source = MessageSource.Local, state = Message.State.Pending, - createdAt = System.currentTimeMillis() + createdAt = System.currentTimeMillis(), + skipPhoneValidation = true, ) val recipients = listOf( MessageRecipient("2", "1234567890", Message.State.Delivered, null), From 131c26ec829ba8804a00a49bc85fa0f37cc79802 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 26 Feb 2024 23:40:23 +0700 Subject: [PATCH 3/6] [notifications] introduce notifications module --- app/src/main/java/me/capcom/smsgateway/App.kt | 2 + .../modules/localserver/WebService.kt | 40 +++++------------ .../modules/notifications/Module.kt | 7 +++ .../notifications/NotificationsService.kt | 45 +++++++++++++++++++ 4 files changed, 64 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/notifications/Module.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt diff --git a/app/src/main/java/me/capcom/smsgateway/App.kt b/app/src/main/java/me/capcom/smsgateway/App.kt index 2a9080c..5d5c7bc 100644 --- a/app/src/main/java/me/capcom/smsgateway/App.kt +++ b/app/src/main/java/me/capcom/smsgateway/App.kt @@ -6,6 +6,7 @@ import me.capcom.smsgateway.modules.encryption.encryptionModule import me.capcom.smsgateway.modules.gateway.GatewayModule import me.capcom.smsgateway.modules.localserver.LocalServerModule import me.capcom.smsgateway.modules.messages.messagesModule +import me.capcom.smsgateway.modules.notifications.notificationsModule import me.capcom.smsgateway.modules.settings.PreferencesStorage import me.capcom.smsgateway.modules.settings.settingsModule import me.capcom.smsgateway.receivers.EventsReceiver @@ -24,6 +25,7 @@ class App: Application() { modules( settingsModule, dbModule, + notificationsModule, messagesModule, encryptionModule, ) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt index 6fa268f..34f5fd8 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt @@ -1,14 +1,11 @@ package me.capcom.smsgateway.modules.localserver -import android.app.NotificationChannel -import android.app.NotificationManager import android.app.Service import android.content.Context import android.content.Intent import android.os.Build import android.os.IBinder import android.os.PowerManager -import androidx.core.app.NotificationCompat import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.aventrix.jnanoid.jnanoid.NanoIdUtils @@ -42,13 +39,15 @@ import me.capcom.smsgateway.modules.localserver.domain.PostMessageResponse import me.capcom.smsgateway.modules.messages.MessagesService import me.capcom.smsgateway.modules.messages.data.MessageSource import me.capcom.smsgateway.modules.messages.data.SendRequest +import me.capcom.smsgateway.modules.notifications.NotificationsService import org.koin.android.ext.android.inject import kotlin.concurrent.thread class WebService : Service() { - private val settingsHelper by lazy { SettingsHelper(this) } + private val settingsHelper: SettingsHelper by inject() private val messagesService: MessagesService by inject() + private val notificationsService: NotificationsService by inject() private val wakeLock: PowerManager.WakeLock by lazy { (getSystemService(Context.POWER_SERVICE) as PowerManager).run { @@ -207,19 +206,6 @@ class WebService : Service() { override fun onCreate() { super.onCreate() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create the NotificationChannel - val name = getString(R.string.sms_gateway) - val descriptionText = getString(R.string.local_sms_gateway_notifications) - val importance = NotificationManager.IMPORTANCE_LOW - val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance) - mChannel.description = descriptionText - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(mChannel) - } - server.start() wakeLock.acquire() @@ -227,18 +213,15 @@ class WebService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) - .setContentTitle(getText(R.string.notification_title)) - .setContentText( - getString( - R.string.sms_gateway_is_running_on_port, - settingsHelper.serverPort - ) + val notification = notificationsService.makeNotification( + this, + getString( + R.string.sms_gateway_is_running_on_port, + settingsHelper.serverPort ) - .setSmallIcon(R.drawable.ic_sms) - .build() + ) - startForeground(NOTIFICATION_ID, notification) + startForeground(NotificationsService.NOTIFICATION_ID_LOCAL_SERVICE, notification) return super.onStartCommand(intent, flags, startId) } @@ -259,9 +242,6 @@ class WebService : Service() { } companion object { - private const val NOTIFICATION_CHANNEL_ID = "WEBSERVICE" - private const val NOTIFICATION_ID = 1 - private val status = MutableLiveData(false) val STATUS: LiveData = status diff --git a/app/src/main/java/me/capcom/smsgateway/modules/notifications/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/notifications/Module.kt new file mode 100644 index 0000000..7addd34 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/notifications/Module.kt @@ -0,0 +1,7 @@ +package me.capcom.smsgateway.modules.notifications + +import org.koin.dsl.module + +val notificationsModule = module { + single { NotificationsService(get()) } +} \ No newline at end of file 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 new file mode 100644 index 0000000..997f0e0 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/notifications/NotificationsService.kt @@ -0,0 +1,45 @@ +package me.capcom.smsgateway.modules.notifications + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import me.capcom.smsgateway.R + +class NotificationsService( + context: Context +) { + private val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as + NotificationManager + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = context.getString(R.string.sms_gateway) + val descriptionText = context.getString(R.string.local_sms_gateway_notifications) + val importance = NotificationManager.IMPORTANCE_LOW + val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance) + mChannel.description = descriptionText + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + notificationManager.createNotificationChannel(mChannel) + } + } + + fun makeNotification(context: Context, contentText: String): Notification { + return NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID) + .setContentTitle(context.getText(R.string.notification_title)) + .setContentText(contentText) + .setSmallIcon(R.drawable.ic_sms) + .build() + } + + companion object { + const val NOTIFICATION_CHANNEL_ID = "sms-gateway" + + const val NOTIFICATION_ID_LOCAL_SERVICE = 1 + const val NOTIFICATION_ID_SEND_WORKER = 2 + } +} \ No newline at end of file From e644d6018cdf3092526f082f08b693819cccbae7 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 26 Feb 2024 23:43:40 +0700 Subject: [PATCH 4/6] [messages] send as foreground service --- app/src/main/AndroidManifest.xml | 11 +++++--- .../messages/workers/SendMessagesWorker.kt | 25 +++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c2bc31..7c7d4cc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + + android:enabled="true" /> + + Encryption Use empty to disable Passphrase + Sending messages... \ No newline at end of file From a86385dcad5cc18dea5e19c0395b531e49b1ee7c Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 26 Feb 2024 23:45:37 +0700 Subject: [PATCH 5/6] [messages] more straightforward starting/stopping of `Send` worker --- .../capcom/smsgateway/modules/messages/MessagesService.kt | 6 +++++- app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt index 0b2c7e0..7462c0a 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt @@ -38,10 +38,14 @@ class MessagesService( private val countryCode: String? = (context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager).networkCountryIso - init { + fun start() { SendMessagesWorker.start(context) } + fun stop() { + SendMessagesWorker.stop(context) + } + fun enqueueMessage(request: SendRequest) { if (getMessage(request.message.id) != null) { Log.d(this.javaClass.name, "Message already exists: ${request.message.id}") diff --git a/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt b/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt index e37f8f9..4cd6692 100644 --- a/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt +++ b/app/src/main/java/me/capcom/smsgateway/ui/HomeFragment.kt @@ -32,6 +32,7 @@ import me.capcom.smsgateway.databinding.FragmentSettingsBinding import me.capcom.smsgateway.helpers.SettingsHelper import me.capcom.smsgateway.modules.gateway.events.DeviceRegisteredEvent import me.capcom.smsgateway.modules.localserver.events.IPReceivedEvent +import me.capcom.smsgateway.modules.messages.MessagesService import org.koin.android.ext.android.inject class HomeFragment : Fragment() { @@ -40,6 +41,7 @@ class HomeFragment : Fragment() { private val binding get() = _binding!! private val settingsHelper: SettingsHelper by inject() + private val messagesSvc: MessagesService by inject() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -177,10 +179,12 @@ class HomeFragment : Fragment() { } else { App.instance.localServerModule.stop(requireContext()) App.instance.gatewayModule.stop(requireContext()) + messagesSvc.stop() } } private fun start() { + messagesSvc.start() App.instance.gatewayModule.start(requireContext()) App.instance.localServerModule.start(requireContext()) } From 2872aca28c46a3d8ca76ee457a00d3aecf48e414 Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Mon, 26 Feb 2024 23:58:17 +0700 Subject: [PATCH 6/6] [messages] expire old messages in pending state --- .../8.json | 144 ++++++++++++++++++ .../me/capcom/smsgateway/data/AppDatabase.kt | 3 +- .../me/capcom/smsgateway/data/Migrations.kt | 16 ++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 app/schemas/me.capcom.smsgateway.data.AppDatabase/8.json create mode 100644 app/src/main/java/me/capcom/smsgateway/data/Migrations.kt diff --git a/app/schemas/me.capcom.smsgateway.data.AppDatabase/8.json b/app/schemas/me.capcom.smsgateway.data.AppDatabase/8.json new file mode 100644 index 0000000..42a15e4 --- /dev/null +++ b/app/schemas/me.capcom.smsgateway.data.AppDatabase/8.json @@ -0,0 +1,144 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "0638620d8ed8717433cb8e718cfc4646", + "entities": [ + { + "tableName": "Message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `text` TEXT NOT NULL, `withDeliveryReport` INTEGER NOT NULL DEFAULT 1, `simNumber` INTEGER, `validUntil` TEXT, `isEncrypted` INTEGER NOT NULL DEFAULT 0, `skipPhoneValidation` INTEGER NOT NULL DEFAULT 0, `source` TEXT NOT NULL DEFAULT 'Local', `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "withDeliveryReport", + "columnName": "withDeliveryReport", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "1" + }, + { + "fieldPath": "simNumber", + "columnName": "simNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "validUntil", + "columnName": "validUntil", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isEncrypted", + "columnName": "isEncrypted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "skipPhoneValidation", + "columnName": "skipPhoneValidation", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Local'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageRecipient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `phoneNumber` TEXT NOT NULL, `state` TEXT NOT NULL, `error` TEXT, PRIMARY KEY(`messageId`, `phoneNumber`), FOREIGN KEY(`messageId`) REFERENCES `Message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phoneNumber", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "messageId", + "phoneNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "Message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "messageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0638620d8ed8717433cb8e718cfc4646')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt index 2ef576e..9317806 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt @@ -11,7 +11,7 @@ import me.capcom.smsgateway.data.entities.MessageRecipient @Database( entities = [Message::class, MessageRecipient::class], - version = 7, + version = 8, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), @@ -32,6 +32,7 @@ abstract class AppDatabase : RoomDatabase() { AppDatabase::class.java, "gateway" ) + .addMigrations(MIGRATION_7_8) .allowMainThreadQueries() .build() } diff --git a/app/src/main/java/me/capcom/smsgateway/data/Migrations.kt b/app/src/main/java/me/capcom/smsgateway/data/Migrations.kt new file mode 100644 index 0000000..6806400 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/data/Migrations.kt @@ -0,0 +1,16 @@ +package me.capcom.smsgateway.data + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + UPDATE message + SET validUntil = strftime('%FT%TZ', createdAt / 1000 + 86400, 'unixepoch') + WHERE validUntil IS NULL AND state = 'Pending' + """.trimIndent() + ) + } +} \ No newline at end of file