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?
Attaching and retrieving extras on those Intent
s is no different here than when passing data between Activities. The only real catch is that PendingIntent
s 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 PendingIntent
s, 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.
This example uses only one Receiver class for both the send and delivery results, so we will use different actions to distinguish Intent
s 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 ContentProvider
s 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 ArrayList
s of PendingIntent
s, 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:
FLAG_MUTABLE
and FLAG_IMMUTABLE
dictate whether the eventual sender of our Intent
can modify it with Intent#fillIn()
before sending it back to us. Both send and delivery reports involve extras on the received Intent
, so we use FLAG_MUTABLE
to allow those to be added. (The only possible extra on a send result is an optional error code for general failures, so we could use FLAG_IMMUTABLE
for that one if we're not planning to check that.)
Unfortunately, since API level 31 we're required to specify one or the other, and of course they didn't always exist, so you'll need to do an ugly Build.VERSION.SDK_INT
check, though I've omitted that above.
FLAG_CANCEL_CURRENT
is for cases where you've previously handed out a PendingIntent
with some data to someone, and now you need to update that data and also make sure that the first someone can't blindly send that Intent
with data that was changed underneath it. This isn't a concern for our particular situation.
FLAG_UPDATE_CURRENT
is for the same cases as FLAG_CANCEL_CURRENT
, but where you don't need to prevent that first someone from sending the updated data.
FLAG_NO_CREATE
is used when you want to check if the given PendingIntent
currently exists. This can be handy for several different things – e.g., determining if an alarm has been set – but isn't necessary for our design here.
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.
As you might've noticed above, we're using explicit Intent
s, 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.
For a send, the resultCode
indicates success if it's equal to Activity.RESULT_OK
; otherwise, it's an error code corresponding to a constant in SmsManager
. The full list of possible values is shown in the docs for sendMultipartTextMessage()
.
For delivery, the result is actually the status
of an SmsMessage
that's attached encoded in a byte array extra. Android recognizes only three broad categories for this – the Telephony.Sms.STATUS_*
constants shown below – but that status value can encode a specific failure mode, if you'd want to check that, for whatever reason. This answer has the details.
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 PendingIntent
s 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 Intent
s upon receipt of the test messages if it's set up to have the device/emulator send those messages to itself.
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.
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.