简介:“发送短信程序”是一款基于Android系统开发的应用,具备向手机或其他设备发送短信的核心功能。该程序已在Android模拟器和真实设备上成功运行,功能稳定,适用于学习与二次开发参考。项目涉及Android权限管理、SmsManager系统服务调用、短信发送逻辑实现及运行时权限申请等关键技术,覆盖从用户交互到系统底层通信的完整流程。作为一款实用型Android应用示例,它为开发者提供了短信功能集成的完整实践方案,适合用于掌握Android通信功能开发的关键技能。
1. Android短信发送功能概述
在移动应用开发中,短信发送功能作为一项基础且实用的通信手段,广泛应用于用户验证、消息提醒、营销推广等场景。本章将从整体视角介绍Android平台下实现短信发送的技术背景与核心机制,重点剖析其系统级支持能力与开发者接口设计逻辑。通过理解Android操作系统对SMS(Short Message Service)协议的支持方式,读者能够建立起对短信功能底层原理的初步认知。
// 示例:获取SmsManager实例的基本代码
SmsManager smsManager = (SmsManager) getSystemService(Context.SMS_SERVICE);
该调用背后涉及系统服务的跨进程绑定机制,体现了Android框架层与Telephony服务的深度集成。同时,本章还将探讨短信功能在现代应用架构中的定位,分析其相较于网络通信的优势与局限性,并引出后续章节所涉及的关键技术点,如权限控制、API调用流程与状态反馈机制,为深入学习打下坚实理论基础。
2. 项目结构解析(my_sendMessage)
在现代Android应用开发中,合理的项目结构不仅是代码可维护性的基础,更是团队协作、持续集成与后期扩展的关键支撑。本章节以名为 my_sendMessage 的短信发送功能模块为例,深入剖析其工程组织方式、核心组件构成、资源管理策略以及构建配置逻辑。通过系统性地拆解该模块的目录层级和文件布局,读者将掌握如何设计清晰、高内聚低耦合的Android项目架构,并理解各组成部分之间的协作机制。尤其针对初学者或中级开发者而言,掌握标准项目结构不仅有助于快速定位问题,还能提升对Android Studio IDE行为的理解。
2.1 工程目录组织与模块划分
Android项目的目录结构遵循Gradle构建系统的规范,具备高度标准化的特点。 my_sendMessage 模块作为典型的单模块应用,其结构体现了Google官方推荐的最佳实践。通过对主模块路径 app/src/main 的细致分析,可以揭示出源码、资源与原生代码的分层设计理念。
2.1.1 主模块结构(app/src/main)详解
进入 app/src/main 目录后,可以看到以下主要子目录:
| 目录 | 功能说明 |
|---|---|
java/ | 存放Java/Kotlin源代码,按包名组织类文件 |
res/ | 资源文件目录,包括布局、字符串、图片等 |
AndroidManifest.xml | 应用清单文件,声明组件与权限 |
assets/ | 原始资产文件,如数据库、JSON配置等 |
aidl/ | AIDL接口定义文件(若使用跨进程通信) |
jniLibs/ | 预编译的本地库(.so文件) |
proto/ | Protocol Buffer定义(若启用Proto DataStore) |
其中, java/com/example/mysendmessage/ 是默认包路径,包含 MainActivity.java 等核心Activity类; res/layout/ 存放UI布局XML文件,如 activity_main.xml ; res/values/ 则集中管理字符串、颜色、尺寸等资源常量。
该结构的优势在于职责分离明确:业务逻辑位于 java/ ,界面表现由 res/layout/ 控制,而国际化文本统一归口于 res/values/strings.xml 。这种模块化设计使得后续维护更加高效,例如更换主题时只需调整 res/values/styles.xml ,无需修改Java代码。
此外, src/main 下还可创建 debug/ 和 release/ 源集(source sets),用于差异化配置调试与发布版本的行为,比如启用日志输出或禁用敏感功能。这是高级项目常用的技巧,在 my_sendMessage 中虽未体现,但为未来演进预留了空间。
graph TD
A[app/src/main] --> B[java/]
A --> C[res/]
A --> D[AndroidManifest.xml]
A --> E[assets/]
A --> F[jniLibs/]
B --> G[com.example.mysendmessage.MainActivity]
C --> H[layout/activity_main.xml]
C --> I[values/strings.xml]
C --> J[drawable/ic_launcher.png]
style A fill:#f9f,stroke:#333;
style B fill:#bbf,stroke:#333;
style C fill:#bbf,stroke:#333;
上述流程图展示了 app/src/main 的典型目录关系,强调了“代码-资源-清单”三位一体的结构模型。
2.1.2 Java代码包与资源文件分离原则
良好的包结构设计是大型项目可持续发展的前提。在 my_sendMessage 中,尽管当前仅有一个Activity,但仍应遵循分层架构思想进行包划分。常见的做法是采用MVC或MVVM模式组织Java代码。
例如,建议将原始单一包结构调整为如下形式:
com.example.mysendmessage
├── activity/
│ └── MainActivity.java
├── util/
│ └── SmsUtils.java
└── receiver/
└── SmsStatusReceiver.java
-
activity/:存放所有Activity类,负责生命周期管理和用户交互。 -
util/:封装工具方法,如权限检查、手机号校验等。 -
receiver/:广播接收器专用包,处理短信发送状态回调。
这种方式实现了关注点分离(Separation of Concerns),避免将所有逻辑堆积在Activity中,符合单一职责原则(SRP)。同时,当项目规模扩大时,可通过添加 fragment/ 、 adapter/ 、 model/ 等子包进一步细化。
资源文件方面, res/ 目录下的分类也需遵循命名规范:
| 资源类型 | 存放路径 | 示例 |
|---|---|---|
| 布局文件 | res/layout/ | activity_main.xml |
| 字符串 | res/values/strings.xml | <string name="app_name">My SMS App</string> |
| 颜色值 | res/values/colors.xml | <color name="primary">#008577</color> |
| 尺寸定义 | res/values/dimens.xml | <dimen name="text_size">16sp</dimen> |
| 样式主题 | res/values/styles.xml | <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> |
特别值得注意的是, 不应在Java代码中硬编码字符串 ,而应全部引用 R.string.xxx 。这不仅便于多语言适配,也有助于减少重复文本带来的维护成本。
下面是一个错误示例与正确实践的对比:
❌ 错误写法(硬编码):
Toast.makeText(this, "短信发送成功!", Toast.LENGTH_SHORT).show();
✅ 正确写法(资源引用):
String message = getString(R.string.sms_sent_success);
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
对应的 strings.xml 定义:
<resources>
<string name="app_name">my_sendMessage</string>
<string name="sms_sent_success">短信发送成功!</string>
<string name="permission_denied">请授予短信发送权限。</string>
</resources>
通过这种方式,即使未来需要支持英文或其他语言,只需新增 res/values-en/strings.xml 即可完成国际化切换,极大提升了应用的可扩展性。
2.2 核心组件构成分析
Android应用的核心由四大组件构成:Activity、Service、BroadcastReceiver 和 ContentProvider。在 my_sendMessage 中,最主要的交互载体是 MainActivity ,它承载了用户输入、权限请求与短信发送逻辑的调度任务。
2.2.1 Activity类的设计与生命周期绑定
MainActivity 继承自 AppCompatActivity ,是整个应用的入口点。其职责包括:
- 加载布局并初始化控件;
- 注册按钮点击事件监听;
- 处理运行时权限请求结果;
- 调用
SmsManager发送短信; - 接收并展示发送状态反馈。
以下是 MainActivity 的简化代码框架:
public class MainActivity extends AppCompatActivity {
private EditText etPhone, etMessage;
private Button btnSend;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化视图
etPhone = findViewById(R.id.et_phone);
etMessage = findViewById(R.id.et_message);
btnSend = findViewById(R.id.btn_send);
// 设置点击监听
btnSend.setOnClickListener(v -> handleSendClick());
}
private void handleSendClick() {
if (checkSelfPermission(Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.SEND_SMS}, 1001);
} else {
sendSms();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1001 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
sendSmb();
} else {
Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_LONG).show();
}
}
private void sendSms() {
String phoneNumber = etPhone.getText().toString().trim();
String message = etMessage.getText().toString();
SmsManager smsManager = SmsManager.getDefault();
try {
smsManager.sendTextMessage(phoneNumber, null, message, null, null);
Toast.makeText(this, R.string.sms_sent_success, Toast.LENGTH_SHORT).show();
} catch (Exception e) {
Toast.makeText(this, "发送失败:" + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
}
代码逐行解析:
- 第1行 :定义
MainActivity类,继承AppCompatActivity以兼容旧版Android UI特性。 - 第6–8行 :声明UI控件引用变量,延迟初始化。
- 第10–15行 :重写
onCreate()方法,调用setContentView()加载布局,并通过findViewById()绑定控件。 - 第18–24行 :设置发送按钮点击事件,先判断权限状态,若未授权则发起请求。
- 第26–34行 :权限回调方法,根据请求码和结果决定是否执行发送操作。
- 第36–46行 :实际发送逻辑,获取输入内容,调用
SmsManager.sendTextMessage()发送短信。
⚠️ 注意:此处缺少 PendingIntent 回调处理,实际生产环境应补充发送状态监听机制(详见第七章)。
该设计将UI控制与业务逻辑初步分离,但仍存在改进空间——例如可将 sendSms() 抽象为独立服务类,提升测试性和复用性。
2.2.2 布局文件(layout/activity_main.xml)控件映射关系
activity_main.xml 使用线性布局( LinearLayout )组织两个输入框和一个按钮,结构简洁明了:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_phone" />
<EditText
android:id="@+id/et_phone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_phone_number"
android:inputType="phone" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_message" />
<EditText
android:id="@+id/et_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_message_content"
android:inputType="textMultiLine"
android:minLines="3" />
<Button
android:id="@+id/btn_send"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/btn_send_label" />
</LinearLayout>
参数说明:
| 属性 | 含义 |
|---|---|
android:id="@+id/..." | 为控件分配唯一标识,供Java代码查找 |
android:layout_width/height | 控件尺寸,“match_parent”填满父容器,“wrap_content”包裹内容 |
android:inputType | 指定软键盘类型, phone 显示数字键盘, textMultiLine 支持换行 |
android:hint | 输入提示文字,用户输入后消失 |
android:text="@string/xxx" | 引用字符串资源,实现UI与文本解耦 |
该布局采用垂直排列,确保在小屏设备上也能完整显示所有元素。同时, padding="16dp" 提供适当的边距,符合Material Design间距规范。
为了验证控件映射是否正确,可在Java代码中添加断言:
if (etPhone == null) throw new IllegalStateException("et_phone not found in layout");
这类防御性编程能有效防止因ID拼写错误导致的空指针异常。
2.3 字符串资源与UI解耦实践
将用户可见文本从代码中剥离,集中管理于 res/values/strings.xml ,是Android开发的重要最佳实践之一。
2.3.1 strings.xml中定义可复用文本常量
在 res/values/strings.xml 中定义如下字符串资源:
<resources>
<string name="app_name">my_sendMessage</string>
<string name="label_phone">手机号:</string>
<string name="hint_phone_number">请输入接收方手机号</string>
<string name="label_message">短信内容:</string>
<string name="hint_message_content">请输入要发送的短信内容</string>
<string name="btn_send_label">发送短信</string>
<string name="sms_sent_success">短信已发送</string>
<string name="permission_denied">应用未获得短信权限,请前往设置开启。</string>
</resources>
这些资源被多个组件共用,例如:
-
EditText的android:hint属性引用提示语; -
Toast显示成功或失败信息; -
TextView显示标签文字。
通过统一管理,一旦需要修改文案(如更正式的表达),只需更改一处即可全局生效,极大降低了维护难度。
此外,字符串资源支持格式化参数,适用于动态内容插入:
<string name="greeting_template">你好,%s!欢迎使用%s。</string>
Java中使用方式:
String welcome = getString(R.string.greeting_template, "张三", getString(R.string.app_name));
输出结果:“你好,张三!欢迎使用my_sendMessage。”
2.3.2 多语言适配支持初探
Android支持基于资源配置限定符(qualifiers)实现自动语言切换。只需创建对应语言的资源目录:
res/
├── values/ # 默认(中文)
│ └── strings.xml
├── values-en/ # 英文
│ └── strings.xml
└── values-es/ # 西班牙文
└── strings.xml
values-en/strings.xml 内容示例:
<resources>
<string name="app_name">Send Message</string>
<string name="label_phone">Phone Number:</string>
<string name="hint_phone_number">Enter recipient's phone number</string>
<string name="btn_send_label">Send SMS</string>
<string name="sms_sent_success">Message sent successfully!</string>
</resources>
当系统语言设为英语时,Android会自动加载 values-en 中的资源,无需任何代码干预。这一机制为全球化部署提供了强大支持。
💡 提示:可借助 Google Play Console 的翻译服务批量生成多语言资源,提升效率。
2.4 构建配置文件解析
build.gradle 文件控制着项目的编译行为、依赖管理和目标平台配置。 my_sendMessage 的 app/build.gradle 配置如下:
2.4.1 build.gradle中SDK版本设置策略
android {
compileSdk 34
defaultConfig {
applicationId "com.example.mysendmessage"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
关键参数解释:
| 参数 | 推荐值 | 说明 |
|---|---|---|
compileSdk | 34 | 编译时使用的API级别,建议保持最新 |
minSdk | 21 | 最低支持版本(Android 5.0),覆盖约95%设备 |
targetSdk | 34 | 目标运行版本,影响权限行为与新特性启用 |
versionCode | 自增整数 | 应用内部版本号,用于市场更新判断 |
versionName | 字符串 | 用户可见版本,如“1.0”、“2.1.3” |
选择 minSdk 21 是因为在Android 5.0之后引入了运行时权限机制(Runtime Permissions),这对 SEND_SMS 权限的请求至关重要。低于此版本的设备无法正确处理动态授权流程。
targetSdk 设置为34(Android 14)意味着应用承诺适配最新安全与隐私限制,例如后台启动Activity的管控、精确位置权限优化等。
2.4.2 依赖库引入规范与编译选项优化
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
依赖说明:
| 依赖项 | 用途 |
|---|---|
appcompat | 提供向后兼容的Activity与Toolbar支持 |
material | 实现Material Design组件(如TextInputLayout) |
constraintlayout | 高性能布局容器,适合复杂UI |
junit / espresso | 单元测试与UI自动化测试框架 |
建议始终使用稳定版本号,避免使用 + 符号(如 1.6.+ ),以防意外升级引入不兼容变更。
此外,可启用视图绑定(View Binding)替代 findViewById() ,提高类型安全性:
android {
...
viewBinding true
}
启用后,系统会为每个布局生成对应的Binding类,例如 ActivityMainBinding ,从而实现类型安全的控件访问。
3. Android权限配置与运行时请求(SEND_SMS)
在现代Android应用开发中,安全机制的演进使得权限管理成为不可或缺的一环。尤其对于涉及用户隐私或设备核心功能的操作——如发送短信(SMS),系统通过多层防护机制确保此类行为必须经过明确授权。本章将深入剖析Android平台的权限模型设计哲学,并以 SEND_SMS 权限为核心案例,系统性地讲解从静态声明到动态申请、再到结果处理的完整流程。重点聚焦于如何在保障用户体验的前提下,合规实现敏感权限的获取与使用。
3.1 Android权限模型基础理论
Android自6.0(API级别23)起引入了运行时权限机制,标志着权限控制由“安装时一次性授予”向“使用时按需申请”的重大转变。这一变革旨在提升用户对应用行为的掌控力,防止恶意应用在后台滥用高危权限。权限被划分为 普通权限 (Normal Permissions)和 危险权限 (Dangerous Permissions)两大类。前者仅需在清单文件中声明即可自动授予;后者则必须在运行时显式请求并由用户确认。
3.1.1 危险权限分类与保护机制
危险权限主要涵盖设备硬件访问、个人数据读取及通信控制等高风险操作。这些权限被归入六个权限组(Permission Groups),每个组包含若干具体权限。例如:
| 权限组 | 包含的关键权限 |
|---|---|
android.permission-group.CALENDAR | READ_CALENDAR, WRITE_CALENDAR |
android.permission-group.CAMERA | CAMERA |
android.permission-group.CONTACTS | READ_CONTACTS, WRITE_CONTACTS |
android.permission-group.LOCATION | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION |
android.permission-group.MICROPHONE | RECORD_AUDIO |
android.permission-group.SMS | SEND_SMS, RECEIVE_SMS, READ_SMS |
当应用请求某一组中的任意一个危险权限时,若用户已同意该组其他权限,则系统可能自动授予权限而不再弹窗提示(取决于厂商定制策略)。但首次请求仍需用户手动确认。
graph TD
A[应用启动] --> B{是否需要危险权限?}
B -- 否 --> C[正常执行]
B -- 是 --> D[检查当前权限状态]
D --> E{已授权?}
E -- 是 --> F[调用敏感功能]
E -- 否 --> G[显示解释性说明(可选)]
G --> H[发起requestPermissions()]
H --> I[系统弹出权限对话框]
I --> J{用户点击允许?}
J -- 是 --> K[onRequestPermissionsResult回调返回GRANTED]
J -- 否 --> L[返回DENIED或DENIED_SHOW_RATIONALE]
K --> M[执行业务逻辑]
L --> N[引导用户前往设置页面]
上述流程图清晰展示了危险权限请求的标准路径。值得注意的是,即使用户拒绝一次,开发者仍可通过合理引导再次尝试请求,但需避免频繁打扰用户,否则可能导致应用被卸载。
3.1.2 SEND_SMS权限的敏感级别与使用限制
SEND_SMS 属于 SMS 权限组下的典型危险权限,允许应用主动向指定号码发送文本消息。由于其潜在滥用风险(如未经用户同意发送付费短信),Google Play商店对此类权限的审核极为严格。自2019年起,Google实施政策限制非默认短信应用申请 SEND_SMS 权限,除非能证明其核心功能依赖此能力(如双因素认证服务、紧急报警应用等)。
此外,部分国产ROM(如MIUI、EMUI)进一步增强了权限管控,即使应用获得了 SEND_SMS 权限,在某些省电模式或后台限制场景下仍可能无法成功发送短信。因此,开发者不仅需关注权限本身,还需考虑设备兼容性和系统级拦截策略。
3.2 清单文件中的权限声明
尽管运行时权限要求在代码中动态申请,但所有权限仍需在 AndroidManifest.xml 中进行静态声明,否则系统会抛出 SecurityException 。这是Android安全架构的第一道防线——未声明即无权。
3.2.1 在AndroidManifest.xml中添加uses-permission标签
要在项目中启用短信发送能力,必须在 <manifest> 根节点内添加如下声明:
<uses-permission android:name="android.permission.SEND_SMS" />
完整的 AndroidManifest.xml 示例片段如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mysendmessage">
<!-- 声明发送短信权限 -->
<uses-permission android:name="android.permission.SEND_SMS" />
<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
参数说明:
-
android:name: 指定权限名称,android.permission.SEND_SMS是系统预定义常量。 - 位置要求:
<uses-permission>必须位于<manifest>根元素下,不能嵌套在<application>或其他标签内部。
⚠️ 注意:仅声明权限不足以获得使用权。在API 23+设备上,还需在运行时调用
ActivityCompat.requestPermissions()完成动态授权。
3.2.2 权限声明位置与作用域影响范围
权限的作用域由其声明位置决定。 <uses-permission> 标签全局有效,意味着整个应用组件均可请求该权限。然而,实际使用权限的功能模块(如某个Activity)才应负责发起请求。
此外,Android还支持更细粒度的权限控制,例如:
- <uses-permission-sdk-23> :仅在API 23及以上生效。
- 权限分组(Permission Groups)用于简化用户理解,但不影响技术实现。
表格对比不同权限声明方式的影响:
| 声明方式 | 支持版本 | 是否需要运行时请求 | 典型用途 |
|---|---|---|---|
<uses-permission> | 所有版本 | API ≥ 23 需要 | 标准做法 |
<uses-permission-sdk-23> | API ≥ 23 | 是 | 向后兼容旧版APK |
| 无声明 | —— | 不可用 | 编译报错或运行崩溃 |
由此可见,正确的权限声明是构建稳定短信功能的前提条件。
3.3 运行时权限请求流程实现
随着Android安全机制的强化,开发者不能再假设权限始终可用。必须在执行敏感操作前主动检测并申请权限。以下以 SEND_SMS 为例,详细展示完整的运行时权限请求流程。
3.3.1 检查权限状态(ContextCompat.checkSelfPermission)
在发起请求前,首先应判断当前是否已拥有相应权限。可通过 ContextCompat.checkSelfPermission() 方法查询:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)
!= PackageManager.PERMISSION_GRANTED) {
// 权限未授予,需申请
} else {
// 已授权,可直接发送短信
}
代码逻辑逐行解读:
-
this:传入当前Activity上下文。 -
Manifest.permission.SEND_SMS:系统定义的权限字符串常量。 - 返回值为
PackageManager.PERMISSION_GRANTED或PERMISSION_DENIED。
该方法兼容低版本Android(自动降级处理),推荐始终使用 ContextCompat 工具类而非直接调用 Context.checkSelfPermission() 。
3.3.2 发起权限申请(ActivityCompat.requestPermissions)
若权限未获授权,应调用 ActivityCompat.requestPermissions() 触发系统对话框:
private static final int REQUEST_SEND_SMS = 1001;
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.SEND_SMS},
REQUEST_SEND_SMS
);
参数说明:
-
this:Activity实例,用于接收回调。 -
new String[]{...}:权限数组,支持同时请求多个权限。 -
REQUEST_SEND_SMS:请求码,用于区分不同的权限请求来源。
系统将弹出原生权限对话框,样式统一且不可自定义内容(出于安全考虑)。用户选择“允许”或“拒绝”后,结果会回调至 onRequestPermissionsResult() 。
3.3.3 用户授权结果回调处理(onRequestPermissionsResult)
重写 onRequestPermissionsResult() 方法以捕获用户决策:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_SEND_SMS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 授权成功,执行发送短信逻辑
sendSms("13800138000", "测试短信");
} else {
// 授权失败,判断是否应提示理由
if (!shouldShowRequestPermissionRationale(Manifest.permission.SEND_SMS)) {
// 用户勾选“不再询问”,需跳转设置页
Toast.makeText(this, "请在设置中开启短信权限", Toast.LENGTH_LONG).show();
} else {
// 可再次请求
Toast.makeText(this, "需要短信权限才能发送消息", Toast.LENGTH_SHORT).show();
}
}
}
}
逻辑分析:
-
requestCode匹配请求标识,防止误处理。 -
grantResults[0]对应第一个权限的结果(数组顺序与请求一致)。 -
shouldShowRequestPermissionRationale()用于判断是否应向用户解释为何需要该权限(仅在用户曾拒绝后返回true)。
此机制允许开发者在用户初次拒绝后提供额外说明,提升接受率。
3.4 权限拒绝后的用户体验设计
良好的权限管理不仅是技术实现,更是产品体验的重要组成部分。盲目频繁弹窗会导致用户反感甚至卸载应用。因此,合理的引导策略至关重要。
3.4.1 引导用户手动开启权限的提示对话框
当用户拒绝且勾选“不再询问”时,应用无法再次弹出请求框。此时应主动引导用户进入系统设置页:
private void goToAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
可结合AlertDialog增强交互:
new AlertDialog.Builder(this)
.setTitle("权限必需")
.setMessage("请开启短信权限以使用发送功能")
.setPositiveButton("去设置", (dialog, which) -> goToAppSettings())
.setNegativeButton("取消", null)
.show();
3.4.2 持久化判断逻辑避免重复弹窗
为防止每次启动都请求权限,建议使用 SharedPreferences 记录用户选择:
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
boolean hasAsked = prefs.getBoolean("asked_for_sms_permission", false);
if (!hasAsked) {
ActivityCompat.requestPermissions(...);
prefs.edit().putBoolean("asked_for_sms_permission", true).apply();
}
结合 shouldShowRequestPermissionRationale() ,可构建智能判断逻辑:
| 场景 | shouldShowRequestPermissionRationale() 返回值 | 应对策略 |
|---|---|---|
| 首次请求 | false | 直接请求 |
| 第一次拒绝 | true | 显示解释后再次请求 |
| 勾选不再询问 | false | 跳转设置页 |
综上所述, SEND_SMS 权限的完整处理流程不仅涉及技术实现,还需兼顾合规性、兼容性与用户体验,是现代Android开发中典型的综合性问题。
4. SmsManager获取与使用方法
在Android系统中,短信发送功能的实现依赖于一个核心系统服务—— SmsManager 。该类作为操作系统提供的底层接口封装,直接对接电信协议栈(RIL层),允许应用以标准化方式访问设备的短信能力。相比早期通过反射调用私有API的方式, SmsManager 提供了稳定、安全且受官方支持的编程模型。本章节将深入解析其服务获取机制、核心发送方法对比、参数传递规则以及异常边界处理策略,帮助开发者构建高可用、可维护的短信通信模块。
4.1 SmsManager服务获取机制
4.1.1 通过getSystemService(Context.SMS_SERVICE)实例化对象
SmsManager 是Android系统服务体系中的重要组成部分,遵循标准的服务注册与检索模式。开发者不能通过构造函数直接创建其实例,而必须借助上下文环境调用 getSystemService() 方法获取全局唯一的单例引用。
// 获取SmsManager实例
SmsManager smsManager = (SmsManager) context.getSystemService(Context.SMS_SERVICE);
上述代码展示了典型的获取流程。其中, context 可以是Activity、Service或Application等具备上下文能力的对象。 Context.SMS_SERVICE 是一个预定义常量,指向系统内部注册的短信服务名称。系统会根据此标识查找并返回对应的Binder代理对象。
逻辑分析:
- 类型转换 :
getSystemService()返回的是Object类型,因此需要强制转换为SmsManager。 - 空值检查必要性 :某些定制ROM或受限环境中可能禁用短信服务,导致返回null,故应在调用前进行判空。
- 线程安全性 :
SmsManager本身是无状态的轻量级门面类,所有操作最终由系统进程完成,因此多线程并发调用不会引发内部冲突。
参数说明:
| 参数 | 类型 | 含义 |
|---|---|---|
context | Context | 应用运行上下文,用于绑定系统服务通道 |
Context.SMS_SERVICE | String 常量 | 系统服务名,标识请求的是短信管理器 |
⚠️ 注意:自Android 4.4(API Level 19)起,系统引入了默认短信应用机制(Default SMS App),非默认应用即使拥有SEND_SMS权限也无法修改短信数据库,但仍可通过
SmsManager发送短信。
4.1.2 系统服务绑定过程与跨进程通信原理简析
Android采用Binder IPC机制实现组件间隔离与安全交互。 SmsManager 作为客户端代理,其背后实际运行在 telephony-process (通常是 com.android.phone 进程)中的服务端才是真正执行短信收发逻辑的实体。
sequenceDiagram
participant App as 应用进程
participant SystemServer
participant TelephonyService
App->>SystemServer: getSystemService("phone")
SystemServer-->>App: 返回ISms.Stub.Proxy
App->>TelephonyService: sendTextMessage(dest, scAddr, text, sentPI, deliveryPI)
TelephonyService->>RIL: RIL_REQUEST_SEND_SMS
RIL-->>Modem: AT+CMGS
Modem-->>Network: 发送SMS PDU
Network-->>Recipient: 接收短信
如上图所示,整个调用链涉及多个层次:
1. Java层调用 :应用通过 SmsManager.sendTextMessage() 发起请求;
2. AIDL跨进程调用 :该方法内部通过 ISms AIDL接口转发至远程服务;
3. RIL层适配 :Radio Interface Layer将指令转化为AT命令;
4. 基带处理器执行 :Modem芯片通过GSM/CDMA网络发送PDU数据包。
这种分层架构确保了应用程序无法直接操控硬件,提升了系统的安全性与稳定性。
关键特性总结:
- 透明性 :开发者无需关心底层通信细节,只需关注API语义;
- 权限控制 :每次调用都会触发SELinux策略和权限校验;
- 异步响应 :发送结果通过
PendingIntent回调通知,避免阻塞主线程。
下面表格列出不同Android版本中 SmsManager 的行为变化:
| Android 版本 | API Level | 行为特征 |
|---|---|---|
| 4.0–4.3 | 14–18 | 可自由读写短信数据库 |
| 4.4+ | 19+ | 非默认应用仅能发送,不能写入收件箱 |
| 5.0+ | 21+ | 引入 SubscriptionManager 支持双卡选择 |
| 7.0+ | 24+ | 提供 sendTextMessage() 重载支持子ID指定SIM卡 |
该机制的设计体现了Android平台逐步强化隐私保护的趋势,也要求开发者合理规划短信功能的权限使用路径。
4.2 发送接口核心方法对比
4.2.1 sendTextMessage() vs sendMultipartTextMessage()
SmsManager 提供了两类主要的文本短信发送方法: sendTextMessage() 适用于短消息(≤160字符GSM-7编码),而 sendMultipartTextMessage() 则用于超长内容的自动拆分与重组。
标准单条发送示例:
try {
SmsManager.getDefault().sendTextMessage(
"13800138000", // 目标号码
null, // SC地址(自动填充)
"这是一条测试短信", // 消息正文
PendingIntent.getBroadcast(context, 0, new Intent("SENT"), 0),
PendingIntent.getBroadcast(context, 0, new Intent("DELIVERED"), 0)
);
} catch (Exception e) {
Log.e("SMS", "发送失败", e);
}
多段长短信发送示例:
String longMessage = "这是一段非常长的文本内容..." + "...超过160个字符";
ArrayList<String> parts = SmsManager.getDefault().divideMessage(longMessage);
SmsManager.getDefault().sendMultipartTextMessage(
"13800138000",
null,
parts,
createSentIntents(parts.size()),
createDeliveryIntents(parts.size())
);
方法签名对比表:
| 方法 | 参数数量 | 是否自动分片 | 最大支持长度 | 适用场景 |
|---|---|---|---|---|
sendTextMessage() | 5 | 否 | 160 (GSM-7) / 70 (UCS-2) | 短验证码、提醒 |
sendMultipartTextMessage() | 5 | 是(需先调用 divideMessage ) | 理论无限(通常≤10段) | 客服通知、公告 |
逻辑分析:
-
divideMessage()是关键前置步骤,它依据当前编码规则计算每段最大容量,并返回ArrayList<String>; - 所有片段共享相同的TP-Messages Reference字段,在接收端由系统自动拼接;
- 若某一段丢失,则整条消息视为无效,存在可靠性风险。
分片算法伪代码示意:
输入:原始字符串 s
输出:分片列表 result
encoding = detectEncoding(s)
maxPerPart = (encoding == GSM7) ? 153 : 67
parts = ceil(length(s) / maxPerPart)
for i in 0 to parts-1:
start = i * maxPerPart
end = min(start + maxPerPart, length(s))
result.add(s.substring(start, end))
return result
此算法保证每段预留7字节用于UDH头信息(User Data Header),包含序列号与总数信息。
4.2.2 数据短信与彩信接口差异说明
除普通文本短信外,Android还支持数据短信(Data SMS)和多媒体短信(MMS)。但 SmsManager 仅提供对数据短信的支持。
数据短信发送示例(WAP Push):
byte[] data = new byte[]{0x01, 0x02, 0x03};
SmsManager.getDefault().sendDataMessage(
"10657500", // 目标号码(通常为服务端短号)
"12345", // 源端口号(SCA)
(short) 9200, // 目标端口号(用于区分应用)
data, // 二进制负载
sentIntent,
deliveryIntent
);
📌 注:WAP Push常用于OTA配置更新、浏览器书签推送等场景。
相比之下,彩信(MMS)由于涉及HTTP传输、SMIL排版、附件编码等复杂流程, SmsManager 不提供原生支持 ,必须通过以下方式之一实现:
- 调用系统 Intent.ACTION_SEND 打开默认信息应用;
- 使用运营商特定API;
- 自行构造M-Send.req协议包并通过HTTP POST提交至MMSC。
以下是三类短信的技术对比:
| 类型 | 编码格式 | 传输协议 | 最大尺寸 | 是否支持富媒体 |
|---|---|---|---|---|
| 文本短信 | GSM-7 / UCS-2 | SS7/CSD | ≤140B per segment | ❌ |
| 数据短信 | 二进制 | SS7 | ≤140B | ❌(仅二进制数据) |
| 彩信 | Base64-encoded | HTTP+WSP | 数MB级 | ✅(图片、音频、视频) |
建议优先使用文本短信满足基本需求,对富媒体支持应结合推送服务(如FCM)替代传统MMS。
4.3 参数传递规则详解
4.3.1 目标号码格式合法性校验(E.164标准参考)
目标号码(destinationAddress)必须符合国际电信联盟ITU-T E.164规范,即国家代码+地区码+用户号码,总长度不超过15位数字。
public boolean isValidPhoneNumber(String number) {
if (number == null || number.isEmpty()) return false;
// 移除非数字字符
String normalized = number.replaceAll("[^\\d]", "");
// 匹配中国大陆手机号(1开头,第二位3-9,共11位)
return Pattern.matches("^1[3-9]\\d{9}$", normalized) &&
normalized.length() == 11;
}
正则表达式解释:
-
^1[3-9]:以1开头,第二位为3~9(符合运营商分配规则); -
\d{9}:后续9位任意数字; -
$:匹配结尾,防止多余字符。
更通用的做法是使用Google开源库 libphonenumber 进行国际化验证:
implementation 'com.googlecode.libphonenumber:phone-number-util:8.12.47'
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
try {
Phonenumber.PhoneNumber phoneNumber = phoneUtil.parse("+8613800138000", "CN");
return phoneUtil.isValidNumber(phoneNumber);
} catch (NumberParseException e) {
Log.e("PHONE", "解析失败", e);
}
该库支持200多个国家的号码格式识别,推荐在跨国业务中使用。
4.3.2 短信正文编码方式与长度限制(GSM-7与UCS-2)
短信正文的最大长度取决于所使用的字符编码方案:
| 编码 | 字符集 | 单段上限 | 多段上限(每段) |
|---|---|---|---|
| GSM-7 | 英文字母、数字、常用符号(共128个) | 160字符 | 153字符 |
| UCS-2 | Unicode全字符(含中文、表情) | 70字符 | 67字符 |
public int getMaxSmsLength(String message) {
for (char c : message.toCharArray()) {
if (c > 0x7F) { // 非ASCII字符
return isMultiPart(message) ? 67 : 70;
}
}
return isMultiPart(message) ? 153 : 160;
}
private boolean isMultiPart(String msg) {
int maxLen = 0;
for (char c : msg.toCharArray()) {
maxLen += (c > 0x7F) ? 1 : 0;
}
return maxLen > (msg.length() > 70 ? 70 : 160);
}
实际影响:
- 输入“你好”(2汉字) → 使用UCS-2 → 单条最多容纳70个汉字;
- 输入混合内容(如“Hi你好”)→ 因含非GSM-7字符 → 全部转为UCS-2 → 长度骤降。
开发者应提前截断或提示用户:“您已输入120字,超出单条限制,请确认是否分段发送”。
4.4 异常处理与边界情况应对
4.4.1 空指针异常预防措施
尽管 SmsManager 大多数参数允许传null(如SC地址自动填充),但仍需防范潜在NPE。
if (TextUtils.isEmpty(destination) || TextUtils.isEmpty(body)) {
Toast.makeText(context, "号码或内容不能为空", Toast.LENGTH_SHORT).show();
return;
}
SmsManager manager = context.getSystemService(SmsManager.class);
if (manager == null) {
Log.e("SMS", "SmsManager获取失败,系统服务不可用");
return;
}
此外, PendingIntent 若未正确初始化也可能导致崩溃,应始终包裹在try-catch中:
PendingIntent sentPI = null;
try {
sentPI = PendingIntent.getBroadcast(context, 0, new Intent("SMS_SENT"),
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
} catch (SecurityException e) {
Log.w("SMS", "无法创建PendingIntent", e);
}
🔒 Android 12+要求显式声明
FLAG_IMMUTABLE或FLAG_MUTABLE,否则抛出IllegalArgumentException。
4.4.2 SIM卡未插入或信号缺失时的行为表现
当设备处于无SIM卡、飞行模式或弱信号状态下, sendTextMessage() 并不会立即抛出异常,而是将任务交由底层队列缓存,待条件恢复后重试。
| 状态 | sendTextMessage()行为 | 返回值 | 是否触发PendingIntent |
|---|---|---|---|
| 无SIM卡 | 成功调用 | 不抛异常 | RESULT_ERROR_GENERIC_FAILURE |
| 飞行模式开启 | 成功调用 | 不抛异常 | RESULT_ERROR_RADIO_OFF |
| 信号极弱 | 成功调用 | 不抛异常 | RESULT_ERROR_NO_SERVICE |
| 权限未授予 | 抛出 SecurityException | 中断执行 | 不触发 |
因此, 仅靠方法调用成功不代表短信真正发出 ,必须依赖 PendingIntent 回调才能准确判断最终状态。
// 注册发送状态广播接收器
context.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context ctx, Intent intent) {
switch (getResultCode()) {
case Activity.RESULT_OK:
Log.d("SMS", "短信已提交至网络");
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
Log.e("SMS", "通用失败:可能是SIM卡问题");
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.e("SMS", "无线电关闭(飞行模式)");
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
Log.e("SMS", "PDU为空,内容非法");
break;
default:
Log.e("SMS", "未知错误码:" + getResultCode());
}
}
}, new IntentFilter("SMS_SENT"));
建议结合 TelephonyManager 实时监控网络状态,提前预警:
TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
boolean hasSim = tm.getSimState() == TelephonyManager.SIM_STATE_READY;
boolean hasSignal = tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN;
综上所述, SmsManager 虽简化了短信发送流程,但其背后的通信链条复杂,涉及权限、编码、网络、SIM卡等多重因素。唯有全面掌握这些细节,方可打造稳定可靠的移动通信模块。
5. sendTextMessage() API调用详解
Android平台为开发者提供了系统级的短信发送能力,其核心接口之一便是 SmsManager.sendTextMessage() 方法。该方法作为构建本地短信通信功能的关键入口,封装了底层电信协议(如GSM、CDMA)与运营商服务中心(Service Center, SC)之间的交互逻辑。深入理解该API的设计原理、参数机制以及在不同运行环境下的行为差异,对于开发稳定、可靠且具备版本兼容性的短信功能至关重要。尤其在涉及用户身份验证、订单通知等关键业务场景时,正确使用 sendTextMessage() 不仅关系到用户体验,更直接影响应用的安全性与合规性。
本章将围绕 sendTextMessage() 的完整方法签名展开深度解析,逐项剖析各参数的实际作用与常见误用情况,并结合代码实例展示标准调用流程。同时,针对 PendingIntent 在异步状态反馈中的角色进行机制性解读,辅以流程图说明事件驱动模型的工作路径。此外,还将探讨该API自Android 5.0以来所经历的重要变更,特别是后台执行限制对长期驻留服务的影响,提出切实可行的适配策略,确保应用在现代Android生态中仍能高效运作。
sendTextMessage() 方法签名与参数语义分析
sendTextMessage() 是 android.telephony.SmsManager 类提供的一个核心公共方法,用于发送单条文本短信。其完整定义如下:
public void sendTextMessage(
String destinationAddress,
String scAddress,
String text,
PendingIntent sentIntent,
PendingIntent deliveryIntent
)
该方法虽看似简洁,但五个参数各自承担着不同的职责,且部分参数具有高度敏感的行为特征,需谨慎处理。
参数一:目标号码(destinationAddress)
这是短信接收方的手机号码,通常以字符串形式传入。Android系统本身不对该字段做格式校验,因此开发者必须自行确保号码符合国际电信联盟推荐的 E.164 标准——即最多包含15位数字,可选前缀“+”表示国际区号。
例如:
- 国内手机号应规范化为: +8613912345678
- 或保留本地格式: 13912345678
若输入非法字符(如字母、空格未清除),可能导致发送失败或被运营商拦截。
代码示例与参数校验实践
String phone = editTextPhone.getText().toString().trim();
phone = phone.replaceAll("[^+\\d]", ""); // 清除非数字和加号字符
if (!isValidPhoneNumber(phone)) {
Toast.makeText(this, "请输入有效的手机号码", Toast.LENGTH_SHORT).show();
return;
}
private boolean isValidPhoneNumber(String number) {
return android.util.Patterns.PHONE.matcher(number).matches() &&
(number.startsWith("+") ? number.length() <= 15 : number.length() == 11);
}
逻辑分析 :
第一行获取用户输入并去除首尾空白;第二行通过正则表达式[^\+\d]删除所有非加号和非数字字符,防止恶意注入或误操作;随后调用自定义校验函数isValidPhoneNumber()判断是否匹配 Android 内置的电话模式并满足长度约束。此步骤是保障destinationAddress合法性的前置条件。
参数二:服务中心地址(scAddress)
服务中心地址(Service Center Address, SC)指代短信网关的路由节点,负责转发短信至目标设备。大多数情况下,开发者可传入 null ,系统会自动从SIM卡配置中读取默认SC地址。
smsManager.sendTextMessage("13912345678", null, "测试短信", sentPI, deliveryPI);
但在以下特殊场景中需手动指定:
- 多SIM卡设备需选择特定卡槽的服务中心;
- 海外漫游环境下原SC不可达;
- 运营商要求使用专用短信通道。
此时可通过 AT 命令或反射方式读取 SC 地址,或由服务器下发配置。
| 使用场景 | scAddress 设置建议 |
|---|---|
| 普通国内短信 | null (推荐) |
| 双卡双待指定卡1 | 获取卡1对应SC地址后填入 |
| 跨境短信通道 | 使用第三方短信网关SC |
| 系统调试/测试 | 固定测试SC地址 |
参数三:短信正文(text)
短信内容支持最大 160个GSM-7编码字符 或 70个UCS-2编码字符 (如含中文)。超过此限制将触发自动分片,生成多条短信(concatenated SMS)。
分段机制说明
当文本长度超出单条容量时,系统内部会调用 sendMultipartTextMessage() 实现拆分,每片段携带用户数据头(UDH)标识序号,终端自动拼接显示。
String message = "这是一条非常长的中文短信内容..."; // 超过70字
if (message.length() > 70) {
List<String> parts = smsManager.divideMessage(message);
smsManager.sendMultipartTextMessage(
destination, null, parts, sentIntents, deliveryIntents
);
} else {
smsManager.sendTextMessage(destination, null, message, sentPI, deliveryPI);
}
参数说明 :
divideMessage()返回List<String>,每个元素为一段合法长度的子消息;后续需为每段创建对应的PendingIntent列表,实现粒度化状态追踪。
参数四与五:PendingIntent(sentIntent 与 deliveryIntent)
这两个参数是实现异步状态回调的核心机制。
-
sentIntent:用于接收“是否成功提交至网络”的结果(即发送状态)。 -
deliveryIntent:用于接收“对方设备是否收到”的确认回执(即送达状态)。
两者均为延迟意图对象,由系统在特定广播事件发生时触发。
PendingIntent 创建示例
Intent sentIntent = new Intent("SMS_SENT_ACTION");
PendingIntent sentPI = PendingIntent.getBroadcast(
context, 0, sentIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Intent deliveredIntent = new Intent("SMS_DELIVERED_ACTION");
PendingIntent deliveryPI = PendingIntent.getBroadcast(
context, 0, deliveredIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
逻辑分析 :
使用getBroadcast()创建广播型 PendingIntent,绑定自定义 Action 字符串。注意从 Android 12(API 31)起,所有 PendingIntent 必须显式标记为不可变(IMMUTABLE)或可变(MUTABLE),否则抛出异常。此处采用FLAG_IMMUTABLE提高安全性,避免中间人篡改意图内容。
完整调用流程与状态监听注册
SmsManager smsManager = SmsManager.getDefault();
// 注册发送状态接收器
registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (getResultCode()) {
case Activity.RESULT_OK:
Log.d("SMS", "短信已成功发送");
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
Log.e("SMS", "通用错误");
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.e("SMS", "无线电关闭(无信号)");
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
Log.e("SMS", "PDU为空");
break;
default:
Log.w("SMS", "其他错误: " + getResultCode());
}
}
}, new IntentFilter("SMS_SENT_ACTION"));
// 发送短信
smsManager.sendTextMessage(
"13912345678",
null,
"您好,这是一条测试短信。",
sentPI,
deliveryPI
);
执行逻辑说明 :
先动态注册一个 BroadcastReceiver 监听SMS_SENT_ACTION广播;当系统完成发送尝试后,会发送该广播并携带结果码。开发者可根据getResultCode()判断具体失败原因,进而做出重试或提示操作。
mermaid 流程图:sendTextMessage 执行与回调流程
sequenceDiagram
participant App
participant SmsManager
participant RadioLayer
participant Network
participant BroadcastReceiver
App->>SmsManager: sendTextMessage(...)
SmsManager->>RadioLayer: 构造RIL请求
RadioLayer->>Network: 提交短信PDU
alt 发送成功
Network-->>RadioLayer: ACK
RadioLayer-->>SmsManager: RESULT_OK
else 发送失败
Network-->>RadioLayer: NACK (原因码)
RadioLayer-->>SmsManager: 错误码
end
SmsManager->>BroadcastReceiver: 触发sentIntent广播
BroadcastReceiver->>App: 接收RESULT_CODE并处理
流程图说明 :
展示了从调用sendTextMessage()到最终广播触发的全过程。强调跨进程通信(IPC)在SmsManager与底层 Radio Interface Layer(RIL)之间的作用,以及错误传播路径。有助于理解为何某些异常无法通过 try-catch 捕获,而必须依赖 PendingIntent 回调。
Android 版本兼容性挑战与适配策略
随着 Android 系统安全机制不断强化, sendTextMessage() 的可用性在不同版本间存在显著差异,尤其体现在后台执行权限与权限模型演进方面。
Android 5.0(Lollipop)及以后:禁止后台静默发送
自 Android 5.0 开始,Google 引入了对后台服务发送短信的限制。若应用处于后台状态(不在前台Activity运行),调用 sendTextMessage() 将可能被系统丢弃或延迟执行。
表格:不同Android版本下短信发送行为对比
| Android版本 | 是否允许后台发送 | 是否需要前台服务提示 | 建议处理方式 |
|---|---|---|---|
| 4.4及以下 | 是 | 否 | 直接调用 |
| 5.0 - 7.1 | 部分支持 | 是(推荐) | 绑定前台Service |
| 8.0+ | 否 | 强制要求 | 使用前台服务+Notification |
| 10+ | 否 | 强制要求 | 结合JobScheduler延迟任务 |
解决方案:使用前台服务保证执行优先级
Intent serviceIntent = new Intent(this, SmsForegroundService.class);
serviceIntent.putExtra("phone", phoneNumber);
serviceIntent.putExtra("message", message);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent);
} else {
startService(serviceIntent);
}
在 SmsForegroundService 中调用 startForeground() 并显示持续通知:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = buildNotification();
startForeground(1001, notification);
String phone = intent.getStringExtra("phone");
String msg = intent.getStringExtra("message");
SmsManager.getDefault().sendTextMessage(
phone, null, msg, sentPI, deliveryPI
);
stopSelf();
return START_NOT_STICKY;
}
参数说明 :
startForegroundService()在 Oreo 及以上版本强制要求在5秒内调用startForeground(),否则抛出 ANR。因此必须快速构造通知对象以维持服务存活。
Android 6.0(Marshmallow)运行时权限影响
尽管已在 AndroidManifest.xml 中声明 <uses-permission android:name="android.permission.SEND_SMS"/> ,但从 Android 6.0 起,必须在运行时再次请求权限。
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
new String[]{Manifest.permission.SEND_SMS},
REQUEST_SEND_SMS
);
} else {
performSend(); // 执行发送逻辑
}
扩展讨论 :
若用户拒绝授权,则sendTextMessage()调用将立即失败且不抛出异常,仅通过sentIntent返回RESULT_ERROR_GENERIC_FAILURE。因此应在权限检查阶段就阻断调用路径,提升用户体验。
Android 12+ 的 PendingIntent 不可变性要求
从 API 31 开始,所有 PendingIntent 必须明确设置可变性标志:
PendingIntent.getBroadcast(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
若遗漏 FLAG_IMMUTABLE 或 FLAG_MUTABLE ,将抛出 IllegalArgumentException 。
最佳实践建议与性能优化方向
为了确保 sendTextMessage() 在复杂环境中稳定工作,建议遵循以下最佳实践:
- 始终校验输入参数合法性 ,尤其是手机号与内容长度;
- 合理使用 null 作为 scAddress ,除非有明确需求;
- 超长文本务必调用 divideMessage() 分片发送 ;
- 必须注册 BroadcastReceiver 处理 sentIntent 和 deliveryIntent ;
- 在 Android 8.0+ 使用前台服务包装发送逻辑 ;
- 持久化记录发送日志以便追溯问题 ;
- 避免频繁调用以防触发运营商限流机制 。
此外,可结合 WorkManager 实现延迟、重试与网络感知调度,进一步提升鲁棒性。
综上所述, sendTextMessage() 虽为一行代码即可调用的方法,但其背后涉及通信协议、系统服务、权限控制与异步回调等多重复杂机制。唯有全面掌握其参数语义与运行环境约束,才能构建真正可靠的短信功能模块。
6. 短信内容与接收号码动态设置
在现代移动应用开发中,用户交互的灵活性和数据输入的准确性是决定功能可用性的关键因素。对于短信发送功能而言,如何实现短信内容与接收号码的 动态设置 ,不仅直接影响用户体验,还关系到系统的健壮性、安全性以及通信成功率。本章将围绕 Android 应用中通过 UI 控件获取用户输入、进行格式校验、预处理文本内容,并最终安全传递至业务逻辑层的全过程展开深入剖析,重点探讨从界面采集到后台调用之间的数据流转机制。
6.1 用户输入控件的设计与数据绑定
在 Android 开发中, EditText 是最常用的用于接收用户文本输入的视图组件。为了支持短信功能中的“目标号码”和“消息正文”输入,通常需要在布局文件中定义两个 EditText 实例,并通过 Java 或 Kotlin 代码与其建立引用关系,从而实现运行时的数据读取与状态控制。
6.1.1 布局文件中的 EditText 配置
在 activity_main.xml 中合理配置 EditText 组件,不仅能提升可读性,还能增强输入体验。以下是一个典型的布局结构示例:
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/editTextPhoneNumber"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_phone_number"
android:inputType="phone"
android:maxLines="1" />
<EditText
android:id="@+id/editTextMessageBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_message_body"
android:inputType="textMultiLine|textCapSentences"
android:minLines="3"
android:maxLines="5"
android:gravity="top|start"
android:scrollbars="vertical" />
</LinearLayout>
参数说明:
-
android:inputType="phone":提示系统使用数字键盘,适用于手机号码输入。 -
android:inputType="textMultiLine":允许多行输入,适合长文本编辑。 -
android:gravity="top|start":确保多行文本从左上角开始显示。 -
android:scrollbars="vertical":启用垂直滚动条,防止内容溢出。
该设计遵循 Material Design 规范,兼顾了功能性与视觉友好性。
6.1.2 Activity 中的数据绑定与初始化
在 MainActivity.java 中,需通过 findViewById() 方法获取对控件的引用,以便后续操作。
public class MainActivity extends AppCompatActivity {
private EditText editTextPhoneNumber;
private EditText editTextMessageBody;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editTextPhoneNumber = findViewById(R.id.editTextPhoneNumber);
editTextMessageBody = findViewById(R.id.editTextMessageBody);
}
}
逻辑分析:
-
setContentView(R.layout.activity_main)加载布局资源,完成视图树构建。 -
findViewById()根据 ID 查找对应控件并返回其对象引用。 - 初始化过程发生在
onCreate()生命周期方法内,符合 Android 组件生命周期规范。
此步骤为后续的数据提取奠定了基础,确保 UI 层与逻辑层之间存在明确的连接通道。
6.1.3 使用 Data Binding 提升效率(可选进阶)
为减少样板代码并提高类型安全,推荐采用 ViewBinding 或 DataBinding 框架替代传统 findViewById 。
启用 ViewBinding 后,上述代码可优化为:
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// 直接访问控件
String phoneNumber = binding.editTextPhoneNumber.getText().toString();
String messageBody = binding.editTextMessageBody.getText().toString();
}
优势对比:
| 方式 | 类型安全 | 性能 | 可维护性 |
|---|---|---|---|
| findViewById | ❌ 弱类型 | 中等 | 差(易出错) |
| ViewBinding | ✅ 强类型 | 高 | 好 |
| DataBinding | ✅ 支持双向绑定 | 略低(编译开销) | 极佳 |
流程图:UI 数据绑定流程
graph TD
A[加载布局 XML] --> B{是否启用 ViewBinding?}
B -- 是 --> C[生成 Binding 类]
B -- 否 --> D[使用 findViewById]
C --> E[调用 inflate 获取实例]
D --> F[按 ID 查找控件]
E & F --> G[建立 Java 对象引用]
G --> H[准备数据读取]
该流程清晰展示了不同绑定策略的技术路径差异,开发者可根据项目复杂度选择合适方案。
6.2 手机号码格式校验与正则表达式应用
有效的电话号码校验是防止无效短信发送的第一道防线。Android 并不自动验证号码合法性,因此必须由开发者手动实施校验逻辑。
6.2.1 正则表达式的选取原则
国际电信联盟(ITU-T)推荐使用 E.164 标准作为全球通用的电话号码格式,其规则如下:
- 最大长度为 15 位数字;
- 以国家代码开头(如 +86 表示中国);
- 不包含空格、横线或括号。
但实际应用中,用户可能输入本地格式(如 13812345678 ),故应支持多种变体。
常用正则表达式如下:
private static final String PHONE_PATTERN = "^(\\+?[0-9]{1,4})?[0-9]{6,14}$";
解释:
-
\\+?:可选的加号前缀; -
[0-9]{1,4}:国家区号部分(1~4 位); -
[0-9]{6,14}:主体号码(至少 6 位,最多 14 位); - 整体匹配中国大陆、美国及其他主流地区的常见格式。
6.2.2 实现即时校验反馈机制
可在 TextWatcher 中监听输入变化,实时提示错误:
editTextPhoneNumber.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String input = s.toString();
boolean isValid = Pattern.matches(PHONE_PATTERN, input.replaceAll("\\s+", ""));
if (!isValid && !input.isEmpty()) {
editTextPhoneNumber.setError("请输入有效的手机号码");
} else {
editTextPhoneNumber.setError(null);
}
}
});
参数说明:
-
replaceAll("\\s+", ""):去除所有空白字符,避免干扰匹配; -
setError():触发 UI 错误提示,红波浪线下划线+图标; - 仅当非空且无效时才报错,避免初次打开即提示错误。
该机制显著提升了用户体验,帮助用户及时纠正输入错误。
6.2.3 多号码分隔与群发识别逻辑
在营销或通知场景中,常需向多个号码批量发送相同内容。此时需支持逗号、分号或换行符分隔的号码列表。
public List<String> parsePhoneNumbers(String rawInput) {
if (rawInput == null || rawInput.trim().isEmpty()) return Collections.emptyList();
return Arrays.stream(rawInput.split("[,;\\n\\r]+"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.filter(s -> Pattern.matches(PHONE_PATTERN, s.replaceAll("\\s+", "")))
.collect(Collectors.toList());
}
逻辑逐行解析:
-
split("[,;\\n\\r]+"):按常见分隔符拆分字符串; -
map(String::trim):清除前后空格; -
filter(!isEmpty):排除空项; - 再次校验格式,仅保留合法号码;
- 返回标准化后的
List<String>。
此方法具备高容错性,适合作为群发功能的核心解析器。
表格:常见号码输入格式与处理结果
| 输入样例 | 分割后数量 | 是否全部有效 | 输出结果 |
|---------|------------|---------------|-----------|
|13812345678| 1 | ✅ |[13812345678]|
|+86 13812345678, 13987654321| 2 | ✅ |[+86138..., 139...]|
|abc123, 135xxxx| 2 | ❌(前者无效) |[135xxxx]|
|\n\n| 2 | ❌(全为空) |[]|
6.3 短信内容预处理与长度控制
GSM 网络对单条短信有严格限制: GSM-7 编码下最大 160 字符,UCS-2(Unicode)编码下最大 70 字符 。超出部分将被自动拆分为多条(multipart),影响费用与送达一致性。
6.3.1 编码检测与字符计数
Java 层无法直接判断编码类型,但可通过是否存在非 GSM-7 字符来估算:
public int calculateSmsCount(String message) {
if (message == null || message.isEmpty()) return 0;
boolean isUnicode = message.chars().anyMatch(ch -> ch > 127 || "!@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ ÷±≥≤π€≠§{}\\^[]~".indexOf(ch) == -1);
int limitPerPart = isUnicode ? 70 : 160;
int encodingSize = isUnicode ? 67 : 153; // 多段短信每部分实际容量(含头信息)
int parts = (int) Math.ceil((double) message.length() / encodingSize);
return Math.max(parts, 1);
}
关键参数说明:
-
isUnicode判断依据:ASCII > 127 或不在标准 GSM-7 字符集中; - 单段上限分别为 160(GSM-7)、70(UCS-2);
- 多段时每段实际可用字符更少(因携带分割头信息);
该算法能较准确预测短信计费条数。
6.3.2 内容截断与提示策略
为避免意外超长发送,可在 UI 上动态显示剩余字符数:
editTextMessageBody.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
int count = calculateSmsCount(s.toString());
int remaining = getCurrentPartRemaining(s.toString());
textViewCharCount.setText(String.format("已用:%d / 剩余:%d [%d 条]", s.length(), remaining, count));
if (count > 5) { // 设定最大允许条数
editTextMessageBody.setError("短信过长,请精简内容");
}
}
private int getCurrentPartRemaining(String text) {
boolean isUnicode = text.chars().anyMatch(ch -> ch > 127);
int perSegment = isUnicode ? 67 : 153;
return perSegment - (text.length() % perSegment);
}
// 其他方法略...
});
此交互设计让用户清楚了解当前输入状态,有助于控制成本与提升发送成功率。
6.4 安全传递数据至业务层的封装模式
将 UI 层数据传入 SmsManager 调用层时,必须避免直接暴露原始字符串,而应通过结构化对象进行封装。
6.4.1 创建数据传输对象(DTO)
public class SmsRequest {
private final List<String> recipients;
private final String messageBody;
private final long timestamp;
public SmsRequest(List<String> recipients, String messageBody) {
this.recipients = new ArrayList<>(recipients); // 防止外部修改
this.messageBody = messageBody.trim();
this.timestamp = System.currentTimeMillis();
}
// Getter 方法省略...
}
安全特性:
- 使用
ArrayList<>(...)防止引用泄露; - 构造时执行
trim()清理空白; - 时间戳可用于日志追踪与去重判断。
6.4.2 线程安全与内存管理建议
若涉及异步发送(如在 IntentService 或 WorkManager 中处理),务必注意:
- 不要在主线程长时间处理大数据;
- 使用 WeakReference<EditText> 避免内存泄漏;
- 对大文本内容考虑压缩或缓存策略。
new Handler(Looper.getMainLooper()).postDelayed(() -> {
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editTextMessageBody.getWindowToken(), 0);
}, 100);
以上代码在发送后自动收起软键盘,优化交互闭环。
流程图:完整数据流控制
graph LR
A[用户输入号码与内容] --> B[TextWatcher 实时校验]
B --> C{是否合法?}
C -- 否 --> D[ setError 提示错误 ]
C -- 是 --> E[点击发送按钮]
E --> F[创建 SmsRequest 对象]
F --> G[启动权限检查]
G --> H[调用 SmsManager.sendTextMessage]
H --> I[广播接收状态反馈]
I --> J[更新 UI 显示结果]
该流程体现了从输入 → 校验 → 封装 → 发送 → 回馈的完整生命周期,构成一个高内聚、低耦合的功能模块。
综上所述,短信内容与接收号码的动态设置不仅是简单的控件取值操作,更是涵盖 UI/UX 设计、数据校验、编码处理、性能优化与安全传递等多个维度的综合性工程实践。只有在每一个环节都做到严谨细致,才能构建出稳定可靠、用户友好的短信通信系统。
7. PendingIntent回调处理发送状态
在Android短信发送功能中,仅完成消息的发出并不足以构成完整的通信闭环。为了实现对每条短信生命周期的精确掌控,必须引入状态反馈机制。 PendingIntent 作为异步操作的关键载体,在短信发送与送达两个核心节点上发挥着不可替代的作用。
7.1 PendingIntent的作用机制与创建方式
PendingIntent 是一种特殊的Intent封装,允许外部系统(如SMS服务)在未来某个时刻以原应用的身份执行指定动作。其本质是通过Binder机制跨进程传递权限代理对象。
在短信场景中,通常需要定义两个 PendingIntent :
- 发送状态回调 :用于接收系统是否成功将短信提交至射频模块。
- 送达状态回调 :用于确认目标设备是否已接收到该短信(需对方手机支持并返回报告)。
// 定义广播Action常量
private static final String ACTION_SMS_SENT = "com.example.SMS_SENT";
private static final String ACTION_SMS_DELIVERED = "com.example.SMS_DELIVERED";
// 创建发送状态PendingIntent
Intent sentIntent = new Intent(ACTION_SMS_SENT);
PendingIntent sentPI = PendingIntent.getBroadcast(
this,
0,
sentIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE // Android 12+要求显式声明可变性
);
// 创建送达状态PendingIntent
Intent deliveredIntent = new Intent(ACTION_SMS_DELIVERED);
PendingIntent deliveredPI = PendingIntent.getBroadcast(
this,
0,
deliveredIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
);
参数说明 :
-requestCode:请求码,用于区分不同意图。
-flags:FLAG_UPDATE_CURRENT确保数据更新;FLAG_MUTABLE适配Android 12及以上版本的安全策略。
7.2 广播接收器注册与动态监听
为捕获状态变化,需注册 BroadcastReceiver 。建议采用动态注册方式,避免内存泄漏,并精准控制生命周期。
// 发送状态接收器
private BroadcastReceiver sentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int resultCode = getResultCode();
switch (resultCode) {
case Activity.RESULT_OK:
Log.d("SMS", "短信已成功提交至网络");
Toast.makeText(context, "发送成功", Toast.LENGTH_SHORT).show();
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
Log.e("SMS", "通用错误");
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
Log.e("SMS", "无线电关闭(无信号或飞行模式)");
break;
case SmsManager.RESULT_ERROR_NULL_PDU:
Log.e("SMS", "PDU为空");
break;
case SmsManager.RESULT_ERROR_LIMIT_EXCEEDED:
Log.e("SMS", "超出运营商限制");
break;
}
}
};
// 送达状态接收器
private BroadcastReceiver deliveredReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (getResultCode() == Activity.RESULT_OK) {
Log.d("SMS", "对方已成功接收短信");
} else {
Log.w("SMS", "对方未确认接收");
}
}
};
注册与注销应在Activity生命周期中同步处理:
@Override
protected void onResume() {
super.onResume();
registerReceiver(sentReceiver, new IntentFilter(ACTION_SMS_SENT));
registerReceiver(deliveredReceiver, new IntentFilter(ACTION_SMS_DELIVERED));
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(sentReceiver);
unregisterReceiver(deliveredReceiver);
}
7.3 状态码映射表与异常归因分析
下表列出常见发送结果码及其含义,便于快速定位问题:
| 结果码 | 常量名 | 含义描述 | 可能原因 |
|---|---|---|---|
| -1 | RESULT_OK | 成功提交 | 正常流程 |
| 1 | RESULT_ERROR_GENERIC_FAILURE | 通用失败 | 网络异常、SIM卡故障 |
| 2 | RESULT_ERROR_RADIO_OFF | 无线电关闭 | 飞行模式、无信号 |
| 3 | RESULT_ERROR_NULL_PDU | PDU编码为空 | 内容为空或编码错误 |
| 4 | RESULT_ERROR_LIMIT_EXCEEDED | 超出限制 | 单位时间内发送过多 |
| 5 | RESULT_ERROR_FDN_CHECK_FAILURE | FDN校验失败 | 号码不在固定拨号列表 |
| 6 | RESULT_ERROR_SHORT_CODE_NOT_ALLOWED | 短号码受限 | 运营商策略阻止 |
| 7 | RESULT_ERROR_SERVICE_UNAVAILABLE | 服务不可用 | 漫游状态下禁用 |
| 8 | RESULT_ERROR_NO_MEMORY | 内存不足 | 系统资源紧张 |
| 9 | RESULT_ERROR_INVALID_ARGUMENTS | 参数无效 | 号码格式错误 |
| 10 | RESULT_ERROR_INVALID_APPLICATION_PROTOCOL_ID | 协议ID非法 | 使用了保留字段 |
| 11 | RESULT_ERROR_INJECT_PENDING_INTENT_FAILED | 注入失败 | 权限或PendingIntent配置错误 |
通过结构化日志输出,可构建完整的追踪链路:
Log.i("SMS_TRACE",
String.format("SendStatus[%d] To:%s Len:%d Time:%tF %<tT",
resultCode, phoneNumber, message.length(), new Date()));
7.4 流程图:短信状态监听全链路
sequenceDiagram
participant App
participant SmsManager
participant RadioLayer
participant Network
participant TargetDevice
App->>SmsManager: sendTextMessage(..., sentPI, deliveredPI)
SmsManager->>RadioLayer: 提交PDU
alt 发送成功
RadioLayer-->>App: Broadcast(sentPI, RESULT_OK)
else 发送失败
RadioLayer-->>App: Broadcast(sentPI, ERROR_CODE)
end
alt 对方回送报告
Network->>TargetDevice: SMS Delivered
TargetDevice->>Network: ACK
Network->>App: Broadcast(deliveredPI, RESULT_OK)
else 未回送
Network->>App: Timeout → deliveredPI不触发
end
此流程揭示了 deliveredPI 并非总能被调用——它依赖于接收端是否启用“短信送达回执”功能,因此在实际业务中应设置超时重试机制或结合服务器侧信令补充判断。
7.5 多状态合并处理与UI反馈优化
为提升用户体验,可将多个状态整合为统一提示:
private void updateUiWithStatus(String phone, int status) {
Map<String, Object> logEntry = new HashMap<>();
logEntry.put("phone", phone);
logEntry.put("status", getStatusLabel(status));
logEntry.put("timestamp", System.currentTimeMillis());
// 存入本地数据库或上传至后台
MessageLogDatabase.insert(logEntry);
runOnUiThread(() -> adapter.notifyDataSetChanged());
}
private String getStatusLabel(int code) {
return getResources().getStringArray(R.array.sms_status_labels)[code + 1];
}
同时可在布局中添加进度指示器,在发送期间显示旋转动画,完成后根据状态切换图标颜色(绿色=成功,红色=失败),形成直观的视觉反馈体系。
简介:“发送短信程序”是一款基于Android系统开发的应用,具备向手机或其他设备发送短信的核心功能。该程序已在Android模拟器和真实设备上成功运行,功能稳定,适用于学习与二次开发参考。项目涉及Android权限管理、SmsManager系统服务调用、短信发送逻辑实现及运行时权限申请等关键技术,覆盖从用户交互到系统底层通信的完整流程。作为一款实用型Android应用示例,它为开发者提供了短信功能集成的完整实践方案,适合用于掌握Android通信功能开发的关键技能。

被折叠的 条评论
为什么被折叠?



