之前『Android 实时监听短信』一文中给内部团队做了一个短信拦截功能,而最近团队内部又来了一个自动发送短信的需求,同样通过测试机,请求服务端,当返回有需要发送的短信时,则自动通过本机的 SIM 卡服务发送。

发送短信需要用到 SEND_SMS 权限:

<manifest ...>
    <uses-permission android:name="android.permission.SEND_SMS" />
    ...
</manifest>

不要忘了在代码中动态申请。

发送短信的思路很简单,只需要提供收件人号码、短信内容,就可以发送。

但是有一个需要注意的点,我们平时接发短信时应该都能发现,一些很长的短信,是不能够像我们常用的社交软件(比如微信和 QQ)一样以一条消息发送的,它会被分割成多条短信进行发送,一般每单位短信最多是 140 个英文字符或者是 70 个汉字符,并且按条收费。

不过不用担心自己处理起来会复杂,Android 系统提供了 API,我们直接调用就好。

/**
 * 发送短信
 *
 * @param tel  收件人号码
 * @param text 短信内容
 */
private void sendSms(String tel, String text) {
    if (TextUtils.isEmpty(tel)) {
        return;
    }
    if (TextUtils.isEmpty(text)) {
        return;
    }
    SmsManager manager = SmsManager.getDefault();
    ArrayList<String> messages = manager.divideMessage(text);
    for (String message : messages) {
        manager.sendTextMessage(tel, null, message, null, null);
    }
}

首先是对电话号码和短信内容进行判空,然后获取 SmsManager 实例,并且调用它的 divideMessage() 对短信进行分割,分割后会返回给我们一个存放了短信片段的 ArrayList 对象,我们遍历这个 ArrayList,对每个短信片段都执行 sendTextMessage() 发送。

我尝试过不调用 divideMessage() 对短信进行分割,直接传入长内容到 sendTextMessage() 方法,发现系统会自动帮我们做内容切割。

sendTextMessage() 有两个重载方法,内部都是调用 sendTextMessageInternal() 去处理:

public final class SmsManager {

    public void sendTextMessage(String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) {
        sendTextMessageInternal(
                destinationAddress,
                scAddress,
                text,
                sentIntent,
                deliveryIntent,
                true,
                getOpPackageName(),
                getAttributionTag(),
                0L
        );
    }
    
    public void sendTextMessage(@NonNull String destinationAddress, @Nullable String scAddress, @NonNull String text, @Nullable PendingIntent sentIntent, @Nullable PendingIntent deliveryIntent, long messageId) {
        sendTextMessageInternal(
                destinationAddress,
                scAddress,
                text,
                sentIntent,
                deliveryIntent,
                true,
                getOpPackageName(),
                getAttributionTag(),
                messageId
        );
    }
}

简单说下这几个参数的作用。destinationAddress 就是收件人号码;scAddress 指短信服务中心号码,如果为 null 就会使用当前默认的短信服务中心,text 就是要发送的内容;sentIntent 是一个 PendingIntent 对象,用于监听短信发送是否成功,如果不为 null,在短信发送成功或失败时系统会广播此 PendingIntentdeliveryIntent 也是个 PendingIntent 对象,用于监听短信接收是否成功,如果不为 null,在将消息传递给收件人时系统会广播此 PendingIntentmessageId 是唯一标识请求发送的消息 ID,用于记录和诊断,默认为 0

上方示例代码中,我对两个 PendingIntent 的参数都传入了 null,即不监听短信发送和接收结果,但上线过程中的确有遇到发送失败的情况,所以为了更好的排查问题,建议实际开发还是要把这两个监听加上,写个 BroadcastReceiver 判断结果处理即可,并不复杂,这里就不演示了。

短信分割的形式,实际上是 2G 时代的产物,当时大家基本上用的都是小灵通诺基亚,屏幕尺寸也就 3 英寸左右(第一代 Android 手机 HTC G1 为 3.17 英寸),显示的区域小无可厚非,但放到现在的 5G 时代来看,可以说是完全没必要的,现在市面上基本都是 5 英寸以上的大屏手机,短信分割交互非常不友好。

于是我查阅文档,发现系统在 Android 4.4(KitKat,API 19)提供了新的方法,允许我们将长信息以一条的样式发送,所以我们上面的方法可以改为:

/**
 * 发送短信
 *
 * @param tel  收件人号码
 * @param text 短信内容
 */
private void sendSms(String tel, String text) {
    if (TextUtils.isEmpty(tel)) {
        return;
    }
    if (TextUtils.isEmpty(text)) {
        return;
    }
    SmsManager manager = SmsManager.getDefault();
    ArrayList<String> messages = manager.divideMessage(text);
    manager.sendMultipartTextMessage(tel, null, messages, null, null);
}

主要是改用了 sendMultipartTextMessage() 方法来发送短信,参数及逻辑与 sendTextMessage() 比较类似,同样是需要对短信进行分割,分割后得到的 ArrayList 对象不需要我们手动遍历发送,直接交给 sendMultipartTextMessage() 方法处理。

这里注意,我上面的描述是以一条的「样式」发送,实际上,运营商的计费规则没有变化,当内容过长时,它依然会对内容进行分割,按分割的条数计费。只不过在短信 App 显示时,它会以一条的样式进行展示。