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/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/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" /> + + > + @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/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/messages/MessagesService.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt index e1a54c0..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 @@ -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,46 @@ 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() - } + fun start() { + SendMessagesWorker.start(context) + } - // random delay from 100ms to 5s - if (settings.secondsBetweenMessages > 0) { - delay((0..settings.secondsBetweenMessages).random() * 1000L) - } - } - } + fun stop() { + SendMessagesWorker.stop(context) } - suspend fun enqueueMessage(request: SendRequest) { - queue.send(request) + fun enqueueMessage(request: SendRequest) { + if (getMessage(request.message.id) != null) { + Log.d(this.javaClass.name, "Message already exists: ${request.message.id}") + return + } + + 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 +114,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 (request.params.validUntil?.before(Date()) == true) { + if (settings.secondsBetweenMessages > 0) { + delay((0..settings.secondsBetweenMessages).random() * 1000L) + } + } + + return 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 +183,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 +195,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 +370,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..f9e4fc0 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/workers/SendMessagesWorker.kt @@ -0,0 +1,81 @@ +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.ForegroundInfo +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.OutOfQuotaPolicy +import androidx.work.WorkManager +import androidx.work.WorkRequest +import androidx.work.WorkerParameters +import me.capcom.smsgateway.R +import me.capcom.smsgateway.modules.messages.MessagesService +import me.capcom.smsgateway.modules.notifications.NotificationsService +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() + private val notificationsSvc: NotificationsService by inject() + + override suspend fun doWork(): Result { + return try { + while (messagesSvc.sendPendingMessages()) { + } + + Result.success() + } catch (e: Exception) { + e.printStackTrace() + + Result.retry() + } + } + + override suspend fun getForegroundInfo(): ForegroundInfo { + return createForegroundInfo() + } + + // Creates an instance of ForegroundInfo which can be used to update the + // ongoing notification. + private fun createForegroundInfo(): ForegroundInfo { + val notificationId = NotificationsService.NOTIFICATION_ID_SEND_WORKER + val notification = notificationsSvc.makeNotification( + applicationContext, + applicationContext.getString(R.string.send_messages_notification) + ) + + return ForegroundInfo(notificationId, notification) + } + + 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 + ) + } + + fun stop(context: Context) { + WorkManager.getInstance(context) + .cancelUniqueWork(NAME) + } + } +} \ No newline at end of file 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 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()) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2aad1f1..01a84ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,4 +33,5 @@ Encryption Use empty to disable Passphrase + Sending messages... \ No newline at end of file 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),