androidsmsbroadcastreceiverandroid-broadcast

How to get sms sent confirmation for each contact/person in android?


I want to send sms to multiple people and verify whether sms sent or not. I checked multiple links (mentioned here) and got the idea of using PendingIntent and broadCast Receiver for confirmation.

Practical way to find out if SMS has been sent
Sending text messages programmatically in android
http://mobiforge.com/design-development/sms-messaging-android

But the key problem is that, I have different 50 contacts number in an arrayList and their different msgs in another arrayList.

I use this code :

for (Condition) {   
    sms = SmsManager.getDefault();   
    try {   
        . . . sms.sendTextMessage(phoneNumbers[i], null, messages[i], sentPI, deliveredPI);  
    }   
    catch(IllegalArgumentException e) {     }  
}

Now, I can't identify how many people do get their msg and how many don't. Because as shown in post(mentioned link above), every time we just get one msg, "SMS delivered".

So please let me know, how can I put "extras" in Intent, when I send msg and get the extras from broadcast Receiver to get the detail of specific contact/person.

One More thing : There are four different option for flag value in PendingIntent (FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT,FLAG_UPDATE_CURRENT). Which one should I use when I send messages in for loop for correct result?


Solution

  • Attaching and retrieving extras on those Intents is no different here than when passing data between Activities. The only real catch is that PendingIntents might not behave as one expects.

    The get*() methods will return a distinct PendingIntent only if the passed Intent is different than all currently active ones per the Intent#filterEquals() method, or if the request code is not currently in use for an equal Intent. Different extras on an otherwise-same Intent with the same request code will not cause a new PendingIntent to be created. Depending on the flag passed in that case, those extras might be ignored, or they might overwrite those in a currently active PendingIntent, possibly leading to incorrect results.

    Any of the filter properties in Intent – i.e., the component, the action, the data URI, etc. – can be used to distinguish them, and also to relay information in some form, so there are a few slightly different ways to set up the construction of the PendingIntents, and their receipt and processing on the other end. The original version of this answer showed a somewhat simplistic yet straightforward Java example in a single Activity class, using single-part messages with redundant extras on each result. The current version comprises relevant snippets from a complete, proper example and is therefore a bit more involved, so if you don't need anything terribly complex, that previous revision might be a preferable starting point.

    Sending

    This example uses only one Receiver class for both the send and delivery results, so we will use different actions to distinguish Intents up to result type. If you prefer separate Receiver classes for each, you don't necessarily need the two different actions, since the components would be different in that case.

    const val ACTION_SMS_SENT = "com.your.app.action.SMS_SENT"
    const val ACTION_SMS_DELIVERED = "com.your.app.action.SMS_DELIVERED"
    

    NB: The full package name prefix isn't technically necessary – just need a couple of unique strings – but it can help prevent confusion during debugging, especially if you're testing additional SMS stuff alongside this.

    To track the individual message, the data URI is used to pass the ID for the message in our app's database, since this corresponds with Android's general design around ContentProviders and content URIs, though we won't be using a Provider here. Also, as mentioned, the URI is a filter criterion, so now we can ensure distinct send and delivery results for each message. To illustrate, an Intent for a send report:

    val sendIntent = Intent(
        ACTION_SMS_SENT,
        Uri.fromParts("app", "com.your.app", messageId.toString()),
        context,
        SmsResultReceiver::class.java
    )
    

    The next thing to consider is multi-part messages, which require ArrayLists of PendingIntents, because each part is actually an individual SMS message with its own results. As far as the send results are concerned, at least, we should consider a message failed if any one part fails, so we need a distinct result for each part. This is a good use for PendingIntent's requestCode, and passing the part number for that parameter in the getBroadcast() calls will ensure a new PendingIntent for each:

    val sendPending = PendingIntent.getBroadcast(
        context,
        partNumber,
        sendIntent,
        PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE
    )
    

    FLAG_ONE_SHOT automatically cancels the PendingIntent after it's sent, and since we only need these once, there's no need to have the system keep them around. If we need to resend, we'll request them again.

    As for the other available flags, briefly:

    Lastly for the send, because we're supporting multi-part messages, we need some way to determine in the Receiver when the results are complete for a given message. Since we don't really care which specific part may fail, we don't need to track part numbers all the way through, so we'll simply include a Boolean extra as a flag for the final part. This actually ends up being the only extra needed for this design, but all of the previous considerations were certainly necessary to make sure that the correct values are set on each part.

    const val EXTRA_IS_LAST_PART = "com.your.app.action.IS_LAST_PART"
    

    If we put all of that together, with a little refactoring, we end up with a send function that looks like:

    fun sendMessage(context: Context, id: Int, address: String, body: String) {
        val manager = getSmsManager(context)
        val parts = manager.divideMessage(body)
        val partCount = parts.size
    
        val send = arrayListOf<PendingIntent>()
        val delivery = arrayListOf<PendingIntent?>()
    
        for (partNumber in 1..partCount) {
            val isLastPart = partNumber == partCount
            send += PendingIntent.getBroadcast(
                context,
                partNumber,
                createResultIntent(context, id)
                    .setAction(ACTION_SMS_SENT)
                    .putExtra(EXTRA_IS_LAST_PART, isLastPart),
                RESULT_FLAGS
            )
            delivery += if (isLastPart) {
                PendingIntent.getBroadcast(
                    context,
                    0,
                    createResultIntent(context, id)
                        .setAction(ACTION_SMS_DELIVERED),
                    RESULT_FLAGS
                )
            } else null
        }
    
        manager.sendMultipartTextMessage(address, null, parts, send, delivery)
    }
    
    private fun createResultIntent(context: Context, messageId: Int) = Intent(
        null,
        Uri.fromParts("app", "com.your.app", messageId.toString()),
        context,
        SmsResultReceiver::class.java
    )
    
    private val RESULT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_MUTABLE
    } else {
        PendingIntent.FLAG_ONE_SHOT
    }
    

    Note that the delivery list has null entries for all except the last part, as we're assuming that the status of the last part will be the overall status for the message as a whole. I'm not sure that that's a valid assumption to make, but all of the AOSP messaging apps do it this way. You can, of course, rework that to get delivery results for each part just like the send results, if you'd prefer.

    Results

    As you might've noticed above, we're using explicit Intents, so we need to register SmsResultReceiver in the manifest. For example:

    <receiver android:name=".SmsResultReceiver" />
    

    The outline for the Receiver is quite simple: pull the message ID from the data URI, check the action for the result type, then grab the relevant values and figure the result appropriately.

    class SmsResultReceiver : BroadcastReceiver() {
    
        override fun onReceive(context: Context, intent: Intent) {
            val messageId = intent.data?.fragment?.toIntOrNull() ?: return
    
            when (intent.action) {
                ACTION_SMS_SENT -> {
                    val isLastPart =
                        intent.getBooleanExtra(EXTRA_IS_LAST_PART, false)
    
                    if (resultCode == Activity.RESULT_OK) {
                        // Success
                    } else {
                        // Failed; resultCode is an error code, the
                        // values for which are in SmsManager constants
                    }
                }
                ACTION_SMS_DELIVERED -> {
                    val status =
                        getResultMessageFromIntent(intent)?.status ?: return
                    when {
                        status == Telephony.Sms.STATUS_COMPLETE -> { … }
                        status >= Telephony.Sms.STATUS_FAILED -> { … } // >=, not ==
                        else -> { … }  // Telephony.Sms.STATUS_PENDING
                    }
                }
            }
        }
    }
    
    private fun getResultMessageFromIntent(intent: Intent): SmsMessage? =
        SmsMessage.createFromPdu(
            intent.getByteArrayExtra("pdu"),
            intent.getStringExtra("format")
        )
    

    It is important to note that not all carriers offer delivery reports. For those that don't, the delivery PendingIntents just won't fire, so don't rely on that happening. Unfortunately, the emulators won't ever fire those either, so testing this particular part is not as easy as the other stuff. However, the example project linked below includes a fake report mechanism that can send the delivery Intents upon receipt of the test messages if it's set up to have the device/emulator send those messages to itself.

    Full Example

    https://github.com/gonodono/sms-sender

    The example is in Kotlin and uses coroutines, Compose, Room, Hilt, WorkManager, and a little (optional) API desugaring for java.time. Kotlin, coroutines, and Compose are ready out of the box in the appropriate project templates. The remainder all require additional configuration, and though the example project is already good to go, you might want to check the setup pages to see exactly what went where.

    The design is a modern update to the classic pattern that uses a Service to send messages queued through a ContentProvider or local SQLiteDatabase.

    The Room database comprises two entities – Message and SendTask – and their corresponding DAOs. Each DAO has functions for the CRUD operations you would expect, and MessageDao also provides a Flow on a query for the most recent queued Message, which greatly simplifies the send routine.

    That routine is executed in a Worker which is started immediately for our demonstration, but which could be easily modified to be scheduled for whenever, with whatever constraints are needed.

    The actual work for the send is handled in SmsSendRepository, and Hilt lets us easily maintain a singleton of that which we can also inject into the statically-registered Receiver for the results, allowing us to keep the overall logic in one place.

    The UI is done in minimal Compose, and is basically just text logs with a couple of buttons. The left image shows the middle of a successful send of six test messages. The right image shows the same send attempted again, but with the emulator's airplane mode enabled.

    Screenshot of a running send

    The last thing to note is the fake delivery report mechanism. This only works when the test device is sending to itself. It's nothing more than a regular BroadcastReceiver registered for SMS_RECEIVED that checks each incoming message against those sent, and upon a match, sends the same kind of broadcast you would get in a real run, complete with valid result PDU attached as an extra.

    This was mainly meant to make testing on the emulators more convenient, but it'll work on a real device too, should your particular carrier not provide delivery reports. As is, the app assumes that it'll be running on a single default emulator; i.e., it sends messages to hard-coded port number 5554.