Mms短信群发实现原理
1、引入
大家也许对单条短信的发送已经非常清楚明白,但对短信的群发可能是一知半解,那短信的群发是怎么样实现的了?
本章重点解析Mms短息群发功能的实现。这里要注意了如果大家对于短信发送的流程不清楚的同学可以看看我前面讲到
的短信发送和接收流程。
2、概括
2.1、类图
2.2、时序图
由于此时序图太大有一些不清晰还请见谅;
3、具体解析
时序图有太大所以不是很清晰;这里对时序图进行解析,主要一大部分是短信的发送流程,所以在此重点讲解群发与单条短信的区别之处;
3.1 短信进入数据库待发送表中
首先在这里唠叨一下在mmsms.db数据库中的sms表,该表保存了短信的相关内容,如下图所示:
该表示保存了短信的相关内容,这里的截图没有截完整,大家可以下来后自己查看该表的详情,这里要说的是type字段;
该字段非常重要,先来看看type的分类:
public static final int MESSAGE_TYPE_ALL = 0;
public static final int MESSAGE_TYPE_INBOX = 1;
public static final int MESSAGE_TYPE_SENT = 2;
public static final int MESSAGE_TYPE_DRAFT = 3;
public static final int MESSAGE_TYPE_OUTBOX = 4;
public static final int MESSAGE_TYPE_FAILED = 5; // for failed outgoing messages
public static final int MESSAGE_TYPE_QUEUED = 6; // for messages to send later
public static final int MESSAGE_TYPE_INBOX_SUB1 = 7;
public static final int MESSAGE_TYPE_INBOX_SUB2 = 8;
看到这你有莫有感悟了,大家请看时序图的第三步,讲需要发送的短信保存到quen队列中去,下面请看如下代码:
private boolean queueMessage(long token) throws MmsException {
if ((mMessageText == null) || (mNumberOfDests == 0)) {
// Don't try to send an empty message.
throw new MmsException("Null message body or dest.");
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
boolean requestDeliveryReport = prefs.getBoolean(
MessagingPreferenceActivity.SMS_DELIVERY_REPORT_MODE,
DEFAULT_DELIVERY_REPORT_MODE);
for (int i = 0; i < mNumberOfDests; i++) {
try {
log("updating Database with sub = " + mSubscription);
Sms.addMessageToUri(mContext.getContentResolver(),
Uri.parse("content://sms/queued"), mDests[i],
mMessageText, null, mTimestamp,
true /* read */,
requestDeliveryReport,
mThreadId, mSubscription);
} catch (SQLiteException e) {
SqliteWrapper.checkSQLiteException(mContext, e);
}
}
// Notify the SmsReceiverService to send the message out
Intent intent = new Intent(SmsReceiverService.ACTION_SEND_MESSAGE, null, mContext, SmsReceiver.class);
intent.putExtra(SUBSCRIPTION, mSubscription);
mContext.sendBroadcast(intent);
return false;
}
在for循环里面,将对应短信的内容加入到content://sms/queued中去,大家可能看到这糊涂了,为了不让大家都糊涂下面简单介绍SmsProvider的insert()方法,
insert方法在插入数据库前会去解析content://sms/queued这个uri,解析后会根据这个uri的类型进行设置type字段的值,然后将数据插入数据库。如果像上面的queued类型其type字段的值就是6。
注意:
该字段的值很重要,后面会多次修该值
3.2 取出第一条短信进行发送
时序图4-8步,可以看出这里没有做实质的内容,只是将发送的任务传递给SmsReceiverService,该service很重要,当你看到这时你不会觉得它的重要性,当你看完整个流程的时候你就知道它的重要性。它的会进行异步处理,将工作转交给ServiceHandler。
9-12步这里开始发送,9、10步太简单没有太多分析的必要,请看11步调用sendFirstQueuedMessage()方法,该方法查询数据库取出第一条数据,其核心代码如下:
String where = Sms.SUB_ID + "=" + subscription;
Cursor c = SqliteWrapper.query(this, resolver, uri,
SEND_PROJECTION, where, null, "date ASC"); // date ASC so we send out in
// same order the user tried
// to send messages.
if (c != null) {
try {
if (c.moveToFirst()) {
String msgText = c.getString(SEND_COLUMN_BODY);
String address = c.getString(SEND_COLUMN_ADDRESS);
int threadId = c.getInt(SEND_COLUMN_THREAD_ID);
int status = c.getInt(SEND_COLUMN_STATUS);
int msgId = c.getInt(SEND_COLUMN_ID);
Uri msgUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
这段代码没有太多复杂的逻辑可以看出取出第一条中的各个数据 ;
然后调用SmsSingleRecipentSender的sendMessage方法发送
sender = new SmsSingleRecipientSender(this,
address, msgText, threadId, status == Sms.STATUS_PENDING,
msgUri, subscription);
sender.sendMessage(SendingProgressTokenManager.NO_TOKEN);;
关于
public boolean sendMessage(long token) throws MmsException {
SmsManager smsManager = SmsManager.getDefault();
ArrayList<String> messages = null;
if ((MmsConfig.getEmailGateway() != null) &&
(Mms.isEmailAddress(mDest) || MessageUtils.isAlias(mDest))) {
String msgText;
msgText = mDest + " " + mMessageText;
mDest = MmsConfig.getEmailGateway();
messages = smsManager.divideMessage(msgText);
} else {
messages = smsManager.divideMessage(mMessageText);
// remove spaces from destination number (e.g. "801 555 1212" -> "8015551212")
mDest = mDest.replaceAll(" ", "");
}
int messageCount = messages.size();
if (messageCount == 0) {
// Don't try to send an empty message.
throw new MmsException("SmsMessageSender.sendMessage: divideMessage returned " +
"empty messages. Original message is \"" + mMessageText + "\"");
}
boolean moved = Sms.moveMessageToFolder(mContext, mUri, Sms.MESSAGE_TYPE_OUTBOX, 0);
if (!moved) {
throw new MmsException("SmsMessageSender.sendMessage: couldn't move message " +
"to outbox: " + mUri);
}
ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>(messageCount);
ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>(messageCount);
for (int i = 0; i < messageCount; i++) {
if (mRequestDeliveryReport) {
// TODO: Fix: It should not be necessary to
// specify the class in this intent. Doing that
// unnecessarily limits customizability.
deliveryIntents.add(PendingIntent.getBroadcast(
mContext, 0,
new Intent(
MessageStatusReceiver.MESSAGE_STATUS_RECEIVED_ACTION,
mUri,
mContext,
MessageStatusReceiver.class),
0));
}
Intent intent = new Intent(SmsReceiverService.MESSAGE_SENT_ACTION,
mUri,
mContext,
SmsReceiver.class);
int requestCode = 0;
if (i == messageCount -1) {
// Changing the requestCode so that a different pending intent
// is created for the last fragment with
// EXTRA_MESSAGE_SENT_SEND_NEXT set to true.
requestCode = 1;
intent.putExtra(SmsReceiverService.EXTRA_MESSAGE_SENT_SEND_NEXT, true);
intent.putExtra(SUBSCRIPTION, mSubscription);
}
sentIntents.add(PendingIntent.getBroadcast(mContext, requestCode, intent, 0));
}
smsManager.sendMultipartTextMessage(mDest, mServiceCenter, messages, sentIntents,
deliveryIntents, mSubscription);
return false;
}
该方法会做4件事情:
1、长短信的拆分;
2、添加sentintent和delieveryIntent;
说明sentIntent是指本条短信发送完后将会执行的一个intent,这里这样解释有的同学可能会不太明白,简单的说 就是发送完一条短信后会回调的一个intent
delieveryIntent :传输报告的intent
3、将本条短信保存到发件箱;
这里一定要看清楚了这里其实是对sms表type字段的更新,会将6改成4,4表示发件箱,正在发送的短信,6表示的是待发送的短信
4、调用中间层SmsManager api的sendMultipartTextMessage方法将短信发送出去
这里通过framework的一系列处理后交由rild,rild解析后通过at指令将短信发送出去,这里就没有太多必要分析
3.3 发送完一条后继续发送未发送的短信
当发送完一条短信后怎么继续发送了,大家肯定疑惑了,可以发现前面只是发送待发送列表中的第一条短信,其他的短信怎么发送出去?
在分析之前,要不厌其烦的说一下,当我们请求rild发送一条短信,那rild会给我们反馈,这在前面讲解单条短信发送的时候有提及到,反馈的事件就是RIL_REQUEST_SEND_SMS,然后会回调到SmsDispatcher类的handmessage方法,至于这个handler的介绍关于短信发送的中间层已经详细描述了,不知道的同学可以回顾一下。
SmsDipatcher的handmessage方法里对发送完信息有一个专门的处理,handleSendComplete方法,这里会提到前面说的sentintent,这里sentIntent会发送com.android.mms.transaction.MESSAGE_SENT的广播到应用层去,记住这里有一个SEND_NEXT_MSG_EXTRA的变量值,该值应用层会使用它来判断是否发送下一条短信;
Intent sendNext = new Intent();
sendNext.putExtra(SEND_NEXT_MSG_EXTRA, true);
sentIntent.send(mContext, Activity.RESULT_OK, sendNext);
可以看在应用层声明sentintent:
Intent intent = new Intent(SmsReceiverService.MESSAGE_SENT_ACTION,
mUri,
mContext,
SmsReceiver.class);
int requestCode = 0;
if (i == messageCount -1) {
// Changing the requestCode so that a different pending intent
// is created for the last fragment with
// EXTRA_MESSAGE_SENT_SEND_NEXT set to true.
requestCode = 1;
intent.putExtra(SmsReceiverService.EXTRA_MESSAGE_SENT_SEND_NEXT, true);
intent.putExtra(SUBSCRIPTION, mSubscription);
}
sentIntents.add(PendingIntent.getBroadcast(mContext, requestCode, intent, 0))
这样的话,在Mms中有注册com.android.mms.transaction.MESSAGE_SENT这个广播的接收器就有且仅有SmsReceiver
它接收不做任何处理然后转手扔给SmsReceiverService,“这个是给你的我是打酱油的“;SmsReceiverService该类我想大家应该不陌生了,前面有提到说它的重要性,不管是发送还是接收都逃不了它的控制,它起到了一个承前继后的作用。它是一个服务,服务启动后会发送message给ServiceHandler进行异步处理,handler会根据intent的action进行sent处理,这里会调用handleSmsSent方法;以下是核心的实现:
boolean sendNextMsg = intent.getBooleanExtra(EXTRA_MESSAGE_SENT_SEND_NEXT, false);
if (mResultCode == Activity.RESULT_OK) {
if (!Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT, error)) {
Log.e(TAG, "handleSmsSent: failed to move message " + uri + " to sent folder");
}
if (sendNextMsg) {
if (TelephonyManager.isMultiSimEnabled()) {
sendFirstQueuedMessage(intent.getIntExtra(SUBSCRIPTION , 0));
} else {
sendFirstQueuedMessage();
}
}
我想看到这,你肯定会恍然大悟吧,首先会获取sendNextMsg前面有提到该值,这里会去根据该值来发送下一条信息,调用的方法仍然是前面提到的sendFirstQueuedMessage方法,该方法会去获取当前数据库中是否有需要发送的短信,如果有继续发送第一条,如果没有直接返回。注意这里有一个Sms.moveMessageToFolder(this, uri, Sms.MESSAGE_TYPE_SENT, error)这样的操作,这里仍然是前面提到的更新sms表中type字段,会将其值设置成2,表示已发送成功,这时界面会将正在发送改成已发送状态,界面的实现是监听了数据库,那一旦数据库有改变就会更新界面,这里就不累述了,在短信的接收里有提到。
4、总结
短信群发其实和但条短信发送没有什么区别,前一条短信发送完成后,紧接着再发送第二条短信,这里由于担心前面发送单条短信的流程讲解的不是很清楚,这里做了一些简单的补充,希望有助于大家对短信发送的理解。