From cba66ad7927a475eccc04cdcbc6a2f0ca4c5e30b Mon Sep 17 00:00:00 2001 From: Acho Arnold Date: Wed, 8 Nov 2023 21:19:43 +0200 Subject: [PATCH] Add retry logic for received messages and sent events --- .../src/main/java/com/httpsms/Constants.kt | 3 + .../java/com/httpsms/DeliveredReceiver.kt | 92 ++++++++++++++++--- .../com/httpsms/FirebaseMessagingService.kt | 28 +++++- .../java/com/httpsms/HttpSmsApiService.kt | 22 ++--- .../src/main/java/com/httpsms/SentReceiver.kt | 91 +++++++++++++++--- .../app/src/main/java/com/httpsms/Settings.kt | 9 ++ 6 files changed, 204 insertions(+), 41 deletions(-) diff --git a/android/app/src/main/java/com/httpsms/Constants.kt b/android/app/src/main/java/com/httpsms/Constants.kt index 81993fa6..2e202caa 100644 --- a/android/app/src/main/java/com/httpsms/Constants.kt +++ b/android/app/src/main/java/com/httpsms/Constants.kt @@ -8,6 +8,9 @@ class Constants { const val KEY_MESSAGE_SIM = "KEY_MESSAGE_SIM" const val KEY_MESSAGE_CONTENT = "KEY_MESSAGE_CONTENT" const val KEY_MESSAGE_TIMESTAMP = "KEY_MESSAGE_TIMESTAMP" + const val KEY_MESSAGE_REASON = "KEY_MESSAGE_REASON" + + const val KEY_HEARTBEAT_ID = "KEY_HEARTBEAT_ID" const val SIM1 = "SIM1" diff --git a/android/app/src/main/java/com/httpsms/DeliveredReceiver.kt b/android/app/src/main/java/com/httpsms/DeliveredReceiver.kt index b9d10be4..444979cc 100644 --- a/android/app/src/main/java/com/httpsms/DeliveredReceiver.kt +++ b/android/app/src/main/java/com/httpsms/DeliveredReceiver.kt @@ -4,9 +4,15 @@ import android.app.Activity import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.workDataOf import timber.log.Timber -import java.time.ZoneOffset -import java.time.ZonedDateTime internal class DeliveredReceiver : BroadcastReceiver() { @@ -18,25 +24,87 @@ internal class DeliveredReceiver : BroadcastReceiver() { } private fun handleMessageDelivered(context: Context, messageId: String?) { - val timestamp = ZonedDateTime.now(ZoneOffset.UTC) if (!Receiver.isValid(context, messageId)) { return } - Thread { - Timber.i("delivered message with ID [${messageId}]") - HttpSmsApiService.create(context).sendDeliveredEvent(messageId!!, timestamp) - }.start() + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val inputData: Data = workDataOf( + Constants.KEY_MESSAGE_ID to messageId, + Constants.KEY_MESSAGE_TIMESTAMP to Settings.currentTimestamp() + ) + + val work = OneTimeWorkRequest + .Builder(DeliveredMessageWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .build() + + WorkManager + .getInstance(context) + .enqueue(work) + + Timber.d("work enqueued with ID [${work.id}] for [DELIVERED] message with ID [${messageId}]") } private fun handleMessageFailed(context: Context, messageId: String?) { - val timestamp = ZonedDateTime.now(ZoneOffset.UTC) if (!Receiver.isValid(context, messageId)) { return } - Thread { - Timber.i("message with ID [${messageId}] not delivered") - HttpSmsApiService.create(context).sendFailedEvent(messageId!!,timestamp, "NOT_DELIVERED") - }.start() + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val inputData: Data = workDataOf( + Constants.KEY_MESSAGE_ID to messageId, + Constants.KEY_MESSAGE_REASON to "CANNOT BE DELIVERED", + Constants.KEY_MESSAGE_TIMESTAMP to Settings.currentTimestamp() + ) + + val work = OneTimeWorkRequest + .Builder(FailedMessageWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .build() + + WorkManager + .getInstance(context) + .enqueue(work) + + Timber.d("work enqueued with ID [${work.id}] for [FAILED] message with ID [${messageId}]") + } + + + internal class DeliveredMessageWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + override fun doWork(): Result { + val messageId = this.inputData.getString(Constants.KEY_MESSAGE_ID) + val timestamp = this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP) + + Timber.i("[${timestamp}] sending [SENT] message event with ID [${messageId}]") + + if (HttpSmsApiService.create(applicationContext).sendDeliveredEvent(messageId!!, timestamp!!)){ + return Result.success() + } + return Result.retry() + } + } + + internal class FailedMessageWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + override fun doWork(): Result { + val messageId = this.inputData.getString(Constants.KEY_MESSAGE_ID) + val reason = this.inputData.getString(Constants.KEY_MESSAGE_REASON) + val timestamp = this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP) + + Timber.i("[${timestamp}] sending [FAILED] message event with ID [${messageId}] and reason [$reason]") + + if (HttpSmsApiService.create(applicationContext).sendFailedEvent(messageId!!, timestamp!!, reason!!)){ + return Result.success() + } + return Result.retry() + } } } diff --git a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt index 82f5d4fc..b63b759a 100644 --- a/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt +++ b/android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt @@ -184,9 +184,29 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { } private fun handleFailed(context: Context, messageID: String) { - Timber.d("sending failed event for message with ID [${messageID}]") - HttpSmsApiService.create(context) - .sendFailedEvent(messageID, ZonedDateTime.now(ZoneOffset.UTC), "MOBILE_APP_INACTIVE") + Timber.d("sending [FAILED] event for message with ID [${messageID}]") + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val inputData: Data = workDataOf( + Constants.KEY_MESSAGE_ID to messageID, + Constants.KEY_MESSAGE_REASON to "MOBILE_APP_INACTIVE", + Constants.KEY_MESSAGE_TIMESTAMP to Settings.currentTimestamp() + ) + + val work = OneTimeWorkRequest + .Builder(SentReceiver.FailedMessageWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .build() + + WorkManager + .getInstance(context) + .enqueue(work) + + Timber.d("work enqueued with ID [${work.id}] for [FAILED] message with ID [${messageID}]") } private fun getMessage(context: Context, messageID: String): Message? { @@ -237,7 +257,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() { this.applicationContext, id.hashCode(), intent, - PendingIntent.FLAG_MUTABLE + PendingIntent.FLAG_IMMUTABLE ) } } diff --git a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt index 6977d1d8..4bc754dc 100644 --- a/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt +++ b/android/app/src/main/java/com/httpsms/HttpSmsApiService.kt @@ -59,16 +59,16 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { return null } - fun sendDeliveredEvent(messageId: String, timestamp: ZonedDateTime) { - sendEvent(messageId, "DELIVERED", timestamp) + fun sendDeliveredEvent(messageId: String, timestamp: String): Boolean { + return sendEvent(messageId, "DELIVERED", timestamp) } - fun sendSentEvent(messageId: String, timestamp: ZonedDateTime) { - sendEvent(messageId, "SENT", timestamp) + fun sendSentEvent(messageId: String, timestamp: String): Boolean { + return sendEvent(messageId, "SENT", timestamp) } - fun sendFailedEvent(messageId: String, timestamp: ZonedDateTime, reason: String) { - sendEvent(messageId, "FAILED", timestamp, reason) + fun sendFailedEvent(messageId: String, timestamp: String, reason: String): Boolean { + return sendEvent(messageId, "FAILED", timestamp, reason) } fun receive(sim: String, from: String, to: String, content: String, timestamp: String): Boolean { @@ -129,10 +129,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { } - private fun sendEvent(messageId: String, event: String, timestamp: ZonedDateTime, reason: String? = null) { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'000000'ZZZZZ") - val timestampString = formatter.format(timestamp).replace("+", "Z") - + private fun sendEvent(messageId: String, event: String, timestamp: String, reason: String? = null): Boolean { var reasonString = "null" if (reason != null) { reasonString = "\"$reason\"" @@ -142,7 +139,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { { "event_name": "$event", "reason": $reasonString, - "timestamp": "$timestampString" + "timestamp": "$timestamp" } """.trimIndent() @@ -157,11 +154,12 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) { if (!response.isSuccessful) { Timber.e("error response [${response.body?.string()}] with code [${response.code}] while sending [${event}] event [${body}] for message with ID [${messageId}]") response.close() - return + return false } response.close() Timber.i( "[$event] event sent successfully for message with ID [$messageId]" ) + return true } diff --git a/android/app/src/main/java/com/httpsms/SentReceiver.kt b/android/app/src/main/java/com/httpsms/SentReceiver.kt index 3b67d070..2d4d2b6f 100644 --- a/android/app/src/main/java/com/httpsms/SentReceiver.kt +++ b/android/app/src/main/java/com/httpsms/SentReceiver.kt @@ -5,9 +5,18 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.telephony.SmsManager +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.workDataOf import timber.log.Timber import java.time.ZoneOffset import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter internal class SentReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -22,26 +31,82 @@ internal class SentReceiver : BroadcastReceiver() { } private fun handleMessageSent(context: Context, messageId: String?) { - val timestamp = ZonedDateTime.now(ZoneOffset.UTC) - if (!Receiver.isValid(context, messageId)) { - return - } + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val inputData: Data = workDataOf( + Constants.KEY_MESSAGE_ID to messageId, + Constants.KEY_MESSAGE_TIMESTAMP to Settings.currentTimestamp() + ) + + val work = OneTimeWorkRequest + .Builder(SentMessageWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .build() - Thread { - Timber.d("sent message with ID [${messageId}]") - HttpSmsApiService.create(context).sendSentEvent(messageId!!,timestamp) - }.start() + WorkManager + .getInstance(context) + .enqueue(work) + + Timber.d("work enqueued with ID [${work.id}] for [SENT] message with ID [${messageId}]") } private fun handleMessageFailed(context: Context, messageId: String?, reason: String) { - val timestamp = ZonedDateTime.now(ZoneOffset.UTC) if (!Receiver.isValid(context, messageId)) { return } - Thread { - Timber.i("message with ID [${messageId}] not sent with reason [$reason]") - HttpSmsApiService.create(context).sendFailedEvent(messageId!!, timestamp, reason) - }.start() + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val inputData: Data = workDataOf( + Constants.KEY_MESSAGE_ID to messageId, + Constants.KEY_MESSAGE_REASON to reason, + Constants.KEY_MESSAGE_TIMESTAMP to Settings.currentTimestamp() + ) + + val work = OneTimeWorkRequest + .Builder(FailedMessageWorker::class.java) + .setConstraints(constraints) + .setInputData(inputData) + .build() + + WorkManager + .getInstance(context) + .enqueue(work) + + Timber.d("work enqueued with ID [${work.id}] for [FAILED] message with ID [${messageId}]") + } + + internal class SentMessageWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + override fun doWork(): Result { + val messageId = this.inputData.getString(Constants.KEY_MESSAGE_ID) + val timestamp = this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP) + + Timber.i("[${timestamp}] sending [SENT] message event with ID [${messageId}]") + + if (HttpSmsApiService.create(applicationContext).sendSentEvent(messageId!!, timestamp!!)){ + return Result.success() + } + return Result.retry() + } + } + + internal class FailedMessageWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + override fun doWork(): Result { + val messageId = this.inputData.getString(Constants.KEY_MESSAGE_ID) + val reason = this.inputData.getString(Constants.KEY_MESSAGE_REASON) + val timestamp = this.inputData.getString(Constants.KEY_MESSAGE_TIMESTAMP) + + Timber.i("[${timestamp}] sending [FAILED] message event with ID [${messageId}] and reason [$reason]") + + if (HttpSmsApiService.create(applicationContext).sendFailedEvent(messageId!!, timestamp!!, reason!!)){ + return Result.success() + } + return Result.retry() + } } } diff --git a/android/app/src/main/java/com/httpsms/Settings.kt b/android/app/src/main/java/com/httpsms/Settings.kt index 7a892df0..4c83c06e 100644 --- a/android/app/src/main/java/com/httpsms/Settings.kt +++ b/android/app/src/main/java/com/httpsms/Settings.kt @@ -5,6 +5,9 @@ import android.os.BatteryManager import androidx.preference.PreferenceManager import timber.log.Timber import java.net.URI +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter object Settings { private const val SETTINGS_SIM1_PHONE_NUMBER = "SETTINGS_SIM1_PHONE_NUMBER" @@ -267,6 +270,12 @@ object Settings { return timestamp } + fun currentTimestamp(): String { + return DateTimeFormatter.ofPattern(Constants.TIMESTAMP_PATTERN).format( + ZonedDateTime.now(ZoneOffset.UTC) + ).replace("+", "Z") + } + fun setHeartbeatTimestampAsync(context: Context, timestamp: Long) { Timber.d(Settings::setHeartbeatTimestampAsync.name)