Skip to content

Commit

Permalink
Add retry logic for received messages and sent events. Closes #275
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Nov 8, 2023
1 parent 11eea8d commit 19d220d
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 41 deletions.
3 changes: 3 additions & 0 deletions android/app/src/main/java/com/httpsms/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
92 changes: 80 additions & 12 deletions android/app/src/main/java/com/httpsms/DeliveredReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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()
}
}
}
28 changes: 24 additions & 4 deletions android/app/src/main/java/com/httpsms/FirebaseMessagingService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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? {
Expand Down Expand Up @@ -237,7 +257,7 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
this.applicationContext,
id.hashCode(),
intent,
PendingIntent.FLAG_MUTABLE
PendingIntent.FLAG_IMMUTABLE
)
}
}
Expand Down
22 changes: 10 additions & 12 deletions android/app/src/main/java/com/httpsms/HttpSmsApiService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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\""
Expand All @@ -142,7 +139,7 @@ class HttpSmsApiService(private val apiKey: String, private val baseURL: URI) {
{
"event_name": "$event",
"reason": $reasonString,
"timestamp": "$timestampString"
"timestamp": "$timestamp"
}
""".trimIndent()

Expand All @@ -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
}


Expand Down
91 changes: 78 additions & 13 deletions android/app/src/main/java/com/httpsms/SentReceiver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()
}
}
}
9 changes: 9 additions & 0 deletions android/app/src/main/java/com/httpsms/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 19d220d

Please sign in to comment.