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 条件能否支持这一要求。
备注:内容仅供参考。
