PAYATHON 2026

Android 非默认短信应用如何在无需用户操作的情况下自动发送 SMS?

数据小许

可以。Android 11 至 14 的系统 API 没有规定只有默认短信应用才能使用 SmsManager.sendTextMessage()。只要应用已获得有效的 SEND_SMS 授权,有可用的短信订阅,且运营商短信服务正常,就可以直接请求发送短信,无需每次打开短信编辑界面。当前的 RESULT_ERROR_GENERIC_FAILURE 只表明发送失败,不能据此认定原因是应用不是默认短信应用。Android SmsManager 文档

但这不能保证所有设备和安装渠道都能全程静默发送:权限仍需事先取得,系统或运营商限制也可能阻止发送。现有材料无法确定这几台设备失败的共同原因。

默认短信应用身份与发送权限有什么关系?

需要分别判断系统能力、权限状态和应用发布要求。

Android 4.4 引入了默认短信应用机制,但其他应用仍可发送短信。sendTextMessage() 文档描述了非默认短信应用发送后的行为:系统会将其发送的短信写入 SMS Provider;默认短信应用则需要自行写入。因此,添加 WRITE_SMS 不能解决这里的发送错误。sendTextMessage() 文档

SEND_SMS 是 dangerous permission,同时也属于 hard restricted permission,涉及安装器的权限许可名单。Flutter 插件显示已授权后,仍应核对原生权限状态、安装来源和设备策略。普通应用没有额外的隐藏权限或 Intent flag 可以绕过这些条件。SEND_SMS 权限定义

如果准备通过 Google Play 发布,还要满足其 SMS 权限政策。通常需要承担符合要求的默认处理程序角色;非默认应用只有满足政策列出的例外并通过审核,才可能使用相关权限。系统 API 可以调用,不代表应用必然符合上架条件。Google Play SMS 权限政策

第一步:保留完整错误信息

原代码把大部分失败都转换成了 Unknown error,丢失了排查依据。先在现有 onReceive() 中记录实际结果码和附加信息:

val code = resultCode
val noDefault = intent?.getBooleanExtra("noDefault", false)
val radioError = if (intent?.hasExtra("errorCode") == true) {
    intent.getIntExtra("errorCode", -1)
} else {
    null
}

Log.e(
    "SMS",
    "resultCode=$code, noDefault=$noDefault, errorCode=$radioError"
)

noDefault=true 是默认短信订阅选择失败的重要线索,这里的“默认”指 SIM 订阅。errorCode 可能包含无线通信层的诊断值,不能直接当作通用 Android 错误码解释。短信发送错误回调说明

原代码使用的 FLAG_IMMUTABLE 不会自动导致短信发送失败,但会阻止发送方通过填充 Intent 添加附加数据,可能影响上述诊断信息的接收;结果码本身仍可返回。为了读取系统补充的 extras,可以在这个特定回调中使用可变 PendingIntent,并将目标限制在自己的应用内:PendingIntent 可变性说明

val sentAction = "$packageName.SMS_SENT.${java.util.UUID.randomUUID()}"
val callbackIntent = Intent(sentAction).setPackage(packageName)

val callbackFlags = PendingIntent.FLAG_ONE_SHOT or
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        PendingIntent.FLAG_MUTABLE
    } else {
        0
    }

val sentPI = PendingIntent.getBroadcast(
    this,
    0,
    callbackIntent,
    callbackFlags
)

注册接收器时应使用同一个 sentAction。每次请求使用独立的 action,也能避免并发发送时复用相同的回调标识。

第二步:明确选择发送短信的 SIM 订阅

原代码使用 SmsManager.getDefault()。多 SIM 设备没有合适的默认短信订阅时,这条路径可能失败。应先检查系统默认短信 SIM;如果没有默认值,让用户在初次配置时选择发送卡,后续事件沿用该选择。

下面的代码根据系统默认短信订阅创建 SmsManager,适用于问题中的 Android 11 至 14 环境:

val subId = SubscriptionManager.getDefaultSmsSubscriptionId()
require(SubscriptionManager.isValidSubscriptionId(subId)) {
    "No valid default SMS subscription; configure a sending SIM first"
}

val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    requireNotNull(getSystemService(SmsManager::class.java))
        .createForSubscriptionId(subId)
} else {
    @Suppress("DEPRECATION")
    SmsManager.getSmsManagerForSubscriptionId(subId)
}

smsManager.sendTextMessage(
    phoneNumber,
    null,
    message,
    sentPI,
    null
)

这里先不请求送达回执,单独验证发送链路。如果使用应用中保存的订阅选择,应在发送前确认它仍处于活动状态;ID 数值有效不代表对应 SIM 仍然可用。不要把卡槽编号 0、1 直接当成 subscriptionId。SmsManager 订阅选择接口

这是一项排查步骤,现有信息还不能证明 SIM 订阅是本次故障的原因。

第三步:修正 Target SDK 34 的接收器注册

SMS_SENT 是应用自定义 action。原代码的两参数 registerReceiver() 在 Android 14、Target SDK 34 下存在另一个兼容性问题:动态注册此类接收器需要指定导出行为,不能套用“仅接收系统广播”的例外。Android 14 接收器行为变更

对于前面限定到本应用的回调,可以使用支持该重载的 AndroidX Core:

ContextCompat.registerReceiver(
    this,
    receiver,
    IntentFilter(sentAction),
    ContextCompat.RECEIVER_NOT_EXPORTED
)

这类注册问题通常表现为异常,应保留异常类型和堆栈。它不能解释 Android 11、12 上已经收到的 generic failure,需要分别处理。

原函数还应补充以下处理:

  • 发送调用抛出异常时,注销已经注册的接收器。
  • 为未收到回调的请求设置超时,并保证 Flutter MethodChannel.Result 只完成一次。
  • 区分发送成功、送达回执和联系人实际确认。当前代码只处理 SMS_SENT,创建 deliveredPI 不代表已经处理送达状态。
  • 将关键告警的任务和状态持久化,避免把整个任务生命周期绑定在 MainActivity 的动态接收器上。

哪些尝试不会解决根因?

READ_SMS、RECEIVE_SMS 不是单纯发送短信的必要条件;READ_PHONE_STATE 可能用于查询订阅信息,但不能代替 SEND_SMS。继续添加读取权限,无法修复发送链路中的权限、订阅或运营商问题。

sendTextMessageWithoutPersisting() 也不是普通应用的替代入口。它从 API 28 就已提供,主要面向运营商或电话框架,需要特权条件,例如同时具备 SEND_SMS 与 MODIFY_PHONE_STATE,或满足运营商权限等要求。sendTextMessageWithoutPersisting() 文档

foreground service、android:stopWithTask="false" 和 WorkManager 都不会赋予额外的短信发送权限。它们涉及任务执行方式,不能消除持续存在的发送错误。foreground service 也不能保证进程永远存活。

建议使用同一张 SIM、同一目标号码和一条短文本做对照,并确认短信编辑界面实际使用的是运营商 SMS,而非其他消息通道。随后结合原生权限检查、应用操作限制、发送结果 extras,必要时查看 radio 日志,继续定位。排查日志应避免泄露手机号和短信正文。

“设为默认短信应用后成功”是一条有用线索,但角色切换也可能影响权限和应用操作状态。需要比较切换前后的具体变化,才能判断原因。

无需用户操作的关键告警应该选择什么方案?

选择取决于发送设备是否联网、是否必须使用本机号码,以及接收方是否安装应用。

方案适用条件主要限制
本机 SmsManager必须使用本机 SIM,且权限、分发条件允许依赖 SIM、运营商服务、资费与设备状态
后端调用云短信网关设备能够把告警事件上传到后端有服务成本,发送号码取决于服务配置
专用手机短信网关可以部署和维护专用设备网关手机仍需正常的 SIM、短信服务及电源;远程调用还需要通信链路
Push notification接收人安装应用且具备网络连接不能在接收设备完全离线时立即送达

FCM 等常规推送服务会根据消息有效期等条件处理离线设备的消息,恢复连接后才可能投递,因此不能作为离线即时告警通道。FCM 消息有效期说明

如果发送设备可能没有互联网,但仍有运营商短信服务,可以考虑本机 SMS。如果设备既没有互联网,也没有任何可用的远程通信链路,云网关和推送都无法立即通知远端联系人。

对于面向普通用户、无法取得相应短信权限使用资格的应用,可以在事件上传后由后端发送短信或推送,并记录提交、投递和确认状态。云网关凭据应由后端保管。如果业务要求从本机号码离线发送,需要先验证实际安装渠道、设备权限和 SIM 条件能否支持这一要求。

备注:内容仅供参考。