概述
SmsReadUtils 是一个专门用于读取、查询、导出和导入 Android 系统短信的工具类。它提供了完整的短信数据管理功能,包括:
- 读取系统中的所有短信
- 按类型、号码、内容、日期范围查询短信
- 导出短信为 JSON 或 CSV 格式
- 从 JSON 文件导入短信
- 删除短信
- 获取短信统计信息
1. 权限要求
1.1 必需权限
在 AndroidManifest.xml 中添加:
xml
<!-- 读取短信权限(危险权限,需要运行时申请) -->
<uses-permission android:name="android.permission.READ_SMS" />
<!-- 写入外部存储权限(Android 9 及以下需要) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
1.2 运行时权限申请
Android 6.0+(API 23+)需要在代码中动态申请 READ_SMS 权限:
kotlin
// 检查权限
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_SMS
) != PackageManager.PERMISSION_GRANTED) {
// 申请权限
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_SMS),
REQUEST_READ_SMS_PERMISSION
)
}
// 处理权限结果
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_READ_SMS_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予,可以读取短信
} else {
// 权限被拒绝
}
}
}
2. 初始化
2.1 创建实例
kotlin
// 在 Activity 或 Fragment 中创建实例
val smsReadUtils = SmsReadUtils(context)
// 或者使用示例类(推荐)
val smsReadExample = SmsReadUtilsExample(activity, lifecycleScope)
2.2 检查权限
kotlin
// 检查是否有读取短信权限
val hasPermission = SmsReadUtils.hasReadSmsPermission(context)
if (hasPermission) {
// 可以读取短信
} else {
// 需要申请权限
}
3. 短信数据模型
3.1 SmsMessage 数据类
kotlin
@Serializable
data class SmsMessage(
val id: Long = 0, // 短信 ID
val address: String = "", // 电话号码
val body: String = "", // 短信内容
val date: Long = 0, // 接收时间(时间戳)
val dateSent: Long = 0, // 发送时间(时间戳)
val type: Int = 0, // 短信类型
val typeLabel: String = "", // 类型标签
val read: Int = 0, // 是否已读(0=未读,1=已读)
val status: Int = 0, // 状态
val threadId: Long = 0, // 会话 ID
val person: String? = null, // 联系人
val protocol: Int = 0, // 协议
val replyPathPresent: Int = 0, // 回复路径
val serviceCenter: String? = null, // 短信中心
val locked: Int = 0, // 是否锁定
val errorCode: Int = 0, // 错误码
val seen: Int = 0, // 是否已查看
val subId: Int = 0, // SIM 卡 ID
val creator: String? = null, // 创建者
val extraFields: Map<String, String> = emptyMap() // 动态字段
)
3.2 辅助方法
获取任意字段值:
kotlin
val value = smsMessage.getField("thread_id")
格式化日期:
kotlin
// 默认格式:yyyy-MM-dd HH:mm:ss
val formattedDate = smsMessage.getFormattedDate()
// 自定义格式
val customDate = smsMessage.getFormattedDate("yyyy/MM/dd HH:mm")
// 格式化发送日期
val sentDate = smsMessage.getFormattedDateSent()
判断短信类型:
kotlin
// 是否已读
val isRead = smsMessage.isRead()
// 是否为收件箱短信
val isInbox = smsMessage.isInbox()
// 是否为已发送短信
val isSent = smsMessage.isSent()
// 是否为草稿
val isDraft = smsMessage.isDraft()
4. 读取短信
4.1 获取所有短信
kotlin
suspend fun getAllSms(): List<SmsMessage>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val messages = smsReadUtils.getAllSms()
Log.d(TAG, "获取到 ${messages.size} 条短信")
// 打印前 5 条短信
messages.take(5).forEach { sms ->
Log.d(TAG, "短信 #${sms.id}:")
Log.d(TAG, " 来自: ${sms.address}")
Log.d(TAG, " 内容: ${sms.body}")
Log.d(TAG, " 类型: ${sms.typeLabel}")
Log.d(TAG, " 时间: ${sms.getFormattedDate()}")
Log.d(TAG, " 已读: ${sms.isRead()}")
}
}
4.2 按类型获取短信
kotlin
suspend fun getSmsByType(type: Int): List<SmsMessage>
短信类型常量:
| 类型 | 常量 | 值 | 说明 |
|---|---|---|---|
| 收件箱 | MESSAGE_TYPE_INBOX |
1 | 接收的短信 |
| 已发送 | MESSAGE_TYPE_SENT |
2 | 已发送的短信 |
| 草稿 | MESSAGE_TYPE_DRAFT |
3 | 草稿箱 |
| 发件箱 | MESSAGE_TYPE_OUTBOX |
4 | 待发送的短信 |
| 失败 | MESSAGE_TYPE_FAILED |
5 | 发送失败的短信 |
| 排队 | MESSAGE_TYPE_QUEUED |
6 | 排队中的短信 |
使用示例:
kotlin
// 获取收件箱短信
val inboxSms = smsReadUtils.getInboxSms()
Log.d(TAG, "收件箱中有 ${inboxSms.size} 条短信")
// 获取已发送短信
val sentSms = smsReadUtils.getSentSms()
Log.d(TAG, "已发送 ${sentSms.size} 条短信")
// 获取草稿
val draftSms = smsReadUtils.getDraftSms()
Log.d(TAG, "草稿箱中有 ${draftSms.size} 条短信")
// 按类型获取
val typeSms = smsReadUtils.getSmsByType(Telephony.Sms.MESSAGE_TYPE_INBOX)
4.3 获取未读短信
kotlin
suspend fun getUnreadSms(): List<SmsMessage>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val unreadSms = smsReadUtils.getUnreadSms()
Log.d(TAG, "有 ${unreadSms.size} 条未读短信")
unreadSms.take(3).forEach { sms ->
Log.d(TAG, "未读短信: ${sms.address} - ${sms.body}")
}
}
4.4 按电话号码搜索短信
kotlin
suspend fun getSmsByPhoneNumber(phoneNumber: String): List<SmsMessage>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val phoneNumber = "18337104423"
val messages = smsReadUtils.getSmsByPhoneNumber(phoneNumber)
Log.d(TAG, "找到 ${messages.size} 条来自 $phoneNumber 的短信")
messages.forEach { sms ->
Log.d(TAG, "${sms.typeLabel}: ${sms.body}")
}
}
4.5 按内容搜索短信
kotlin
suspend fun searchSms(keyword: String): List<SmsMessage>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val keyword = "验证码"
val messages = smsReadUtils.searchSms(keyword)
Log.d(TAG, "找到 ${messages.size} 条包含 \"$keyword\" 的短信")
messages.take(5).forEach { sms ->
Log.d(TAG, "${sms.address}: ${sms.body}")
}
}
4.6 按日期范围获取短信
kotlin
suspend fun getSmsByDateRange(startDate: Long, endDate: Long): List<SmsMessage>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
// 获取最近 7 天的短信
val endTime = System.currentTimeMillis()
val startTime = endTime - (7 * 24 * 60 * 60 * 1000L)
val messages = smsReadUtils.getSmsByDateRange(startTime, endTime)
Log.d(TAG, "在指定日期范围内找到 ${messages.size} 条短信")
messages.take(5).forEach { sms ->
Log.d(TAG, "${sms.getFormattedDate()}: ${sms.body}")
}
}
5. 导出短信
5.1 导出为 JSON 格式
5.1.1 导出所有短信
kotlin
suspend fun exportAllSmsToJson(outputFile: File): Result<Int>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val outputFile = File(context.getExternalFilesDir(null), "sms_all.json")
val result = smsReadUtils.exportAllSmsToJson(outputFile)
result.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条短信")
Log.d(TAG, "文件路径: ${outputFile.absolutePath}")
}.onFailure { e ->
Log.e(TAG, "导出失败: ${e.message}")
}
}
5.1.2 导出指定类型的短信
kotlin
suspend fun exportInboxSmsToJson(outputFile: File): Result<Int>
suspend fun exportSentSmsToJson(outputFile: File): Result<Int>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
// 导出收件箱
val inboxFile = File(context.getExternalFilesDir(null), "sms_inbox.json")
val inboxResult = smsReadUtils.exportInboxSmsToJson(inboxFile)
// 导出已发送
val sentFile = File(context.getExternalFilesDir(null), "sms_sent.json")
val sentResult = smsReadUtils.exportSentSmsToJson(sentFile)
inboxResult.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条收件箱短信")
}
sentResult.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条已发送短信")
}
}
5.1.3 导出自定义短信列表
kotlin
suspend fun exportSmsToJson(messages: List<SmsMessage>, outputFile: File): Result<Int>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
// 获取特定号码的短信
val phoneNumber = "18337104423"
val messages = smsReadUtils.getSmsByPhoneNumber(phoneNumber)
// 导出为 JSON
val safeNumber = phoneNumber.replace("[^0-9]".toRegex(), "_")
val outputFile = File(context.getExternalFilesDir(null), "sms_${safeNumber}.json")
val result = smsReadUtils.exportSmsToJson(messages, outputFile)
result.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条来自 $phoneNumber 的短信")
Log.d(TAG, "文件路径: ${outputFile.absolutePath}")
}
}
5.2 导出为 CSV 格式
5.2.1 导出所有短信为 CSV
kotlin
suspend fun exportAllSmsToCsv(outputFile: File): Result<Int>
CSV 格式:
ID,Address,Body,Date,Date Sent,Type,Type Label,Read,Status,Thread ID
1,18337104423,这是一条测试短信,2024-04-28 10:30:00,2024-04-28 10:30:00,1,inbox,0,-1,123
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val outputFile = File(context.getExternalFilesDir(null), "sms_export.csv")
val result = smsReadUtils.exportAllSmsToCsv(outputFile)
result.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条短信为 CSV")
Log.d(TAG, "文件路径: ${outputFile.absolutePath}")
}.onFailure { e ->
Log.e(TAG, "导出失败: ${e.message}")
}
}
5.2.2 导出自定义短信列表为 CSV
kotlin
suspend fun exportSmsToCsv(messages: List<SmsMessage>, outputFile: File): Result<Int>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
// 获取收件箱短信
val inboxSms = smsReadUtils.getInboxSms()
// 导出为 CSV
val outputFile = File(context.getExternalFilesDir(null), "sms_inbox.csv")
val result = smsReadUtils.exportSmsToCsv(inboxSms, outputFile)
result.onSuccess { count ->
Log.d(TAG, "成功导出 $count 条短信为 CSV")
}
}
5.3 批量导出
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val outputDir = context.getExternalFilesDir(null)!!
// 导出收件箱
val inboxResult = smsReadUtils.exportInboxSmsToJson(File(outputDir, "sms_inbox.json"))
// 导出已发送
val sentResult = smsReadUtils.exportSentSmsToJson(File(outputDir, "sms_sent.json"))
// 导出草稿
val draftSms = smsReadUtils.getSmsByType(android.provider.Telephony.Sms.MESSAGE_TYPE_DRAFT)
val draftResult = smsReadUtils.exportSmsToJson(draftSms, File(outputDir, "sms_draft.json"))
// 导出未读
val unreadSms = smsReadUtils.getUnreadSms()
val unreadResult = smsReadUtils.exportSmsToJson(unreadSms, File(outputDir, "sms_unread.json"))
// 导出所有
val allResult = smsReadUtils.exportAllSmsToJson(File(outputDir, "sms_all.json"))
Log.d(TAG, "批量导出完成:")
Log.d(TAG, " 收件箱: ${inboxResult.getOrNull() ?: 0}")
Log.d(TAG, " 已发送: ${sentResult.getOrNull() ?: 0}")
Log.d(TAG, " 草稿: ${draftResult.getOrNull() ?: 0}")
Log.d(TAG, " 未读: ${unreadResult.getOrNull() ?: 0}")
Log.d(TAG, " 总计: ${allResult.getOrNull() ?: 0}")
Log.d(TAG, " 输出目录: ${outputDir.absolutePath}")
}
6. 导入短信
6.1 从 JSON 文件导入短信
kotlin
suspend fun importSmsFromJson(inputFile: File): Result<Int>
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val inputFile = File(context.getExternalFilesDir(null), "sms_backup.json")
val result = smsReadUtils.importSmsFromJson(inputFile)
result.onSuccess { count ->
Log.d(TAG, "成功导入 $count 条短信")
Log.d(TAG, "注意: 导入可能受限,部分设备不支持")
}.onFailure { e ->
Log.e(TAG, "导入失败: ${e.message}")
}
}
重要提示:
- 写入短信到系统数据库可能受限
- 部分设备/ROM(如 MIUI、EMUI)可能不支持导入
- Android 10+ 对短信写入有更严格的限制
7. 删除短信
7.1 删除单条短信
kotlin
fun deleteSms(smsId: Long): Boolean
使用示例:
kotlin
val smsId = 123L
val success = smsReadUtils.deleteSms(smsId)
if (success) {
Log.d(TAG, "成功删除短信 ID: $smsId")
} else {
Log.e(TAG, "删除短信失败 ID: $smsId")
}
7.2 删除指定号码的所有短信
kotlin
fun deleteSmsByPhoneNumber(phoneNumber: String): Int
使用示例:
kotlin
val phoneNumber = "18337104423"
val count = smsReadUtils.deleteSmsByPhoneNumber(phoneNumber)
Log.d(TAG, "已删除 $count 条来自 $phoneNumber 的短信")
7.3 清空所有短信
kotlin
fun clearAllSms(): Int
使用示例:
kotlin
// 警告:此操作危险,谨慎使用!
val count = smsReadUtils.clearAllSms()
Log.d(TAG, "已清空 $count 条短信")
安全建议:
- 调用前显示确认对话框
- 考虑添加双重确认机制
- 记录操作日志
8. 统计信息
8.1 获取短信统计信息
kotlin
suspend fun getSmsStatistics(): SmsStatistics
统计数据类:
kotlin
@Serializable
data class SmsStatistics(
val totalCount: Int = 0, // 总计
val inboxCount: Int = 0, // 收件箱数量
val sentCount: Int = 0, // 已发送数量
val draftCount: Int = 0, // 草稿数量
val unreadCount: Int = 0 // 未读数量
)
使用示例:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val stats = smsReadUtils.getSmsStatistics()
Log.d(TAG, "短信统计:")
Log.d(TAG, " 总计: ${stats.totalCount}")
Log.d(TAG, " 收件箱: ${stats.inboxCount}")
Log.d(TAG, " 已发送: ${stats.sentCount}")
Log.d(TAG, " 草稿: ${stats.draftCount}")
Log.d(TAG, " 未读: ${stats.unreadCount}")
}
9. 动态字段
9.1 访问所有字段
SmsMessage 使用动态字段存储,可以访问数据库中的所有字段:
kotlin
lifecycleScope.launch(Dispatchers.IO) {
val messages = smsReadUtils.getAllSms().take(1)
messages.forEach { sms ->
Log.d(TAG, "短信 ID: ${sms.id}")
Log.d(TAG, " 内容: ${sms.body}")
Log.d(TAG, " 类型: ${sms.typeLabel}")
// 访问动态字段
sms.extraFields.forEach { (key, value) ->
Log.d(TAG, " $key: $value")
}
// 使用 getField 方法访问特定字段
val threadId = sms.getField("thread_id")
Log.d(TAG, " 会话 ID: $threadId")
}
}
9.2 动态字段的优势
- 版本兼容性:自动适配不同 Android 版本
- OEM 兼容性:支持厂商自定义字段
- 未来兼容:新字段自动包含,无需修改代码
10. 完整使用示例
10.1 在 Activity 中使用
kotlin
class MainActivity : ComponentActivity() {
private lateinit var smsReadExample: SmsReadUtilsExample
private val TAG = "MainActivity"
private const val REQUEST_READ_SMS_PERMISSION = 1001
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化示例类
smsReadExample = SmsReadUtilsExample(this, lifecycleScope)
// 请求权限
requestReadSmsPermission()
// 设置 UI
setContent {
Column(modifier = Modifier.padding(16.dp)) {
Text("短信读取与导出", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(16.dp))
// 获取所有短信
Button(onClick = {
smsReadExample.example1_GetAllSms()
}) {
Text("获取所有短信")
}
// 获取收件箱
Button(onClick = {
smsReadExample.example2_GetInboxSms()
}) {
Text("获取收件箱短信")
}
// 导出所有短信
Button(onClick = {
smsReadExample.example8_ExportAllSmsToJson()
}) {
Text("导出所有短信为 JSON")
}
// 导出 CSV
Button(onClick = {
smsReadExample.example11_ExportSmsToCsv()
}) {
Text("导出所有短信为 CSV")
}
// 获取统计信息
Button(onClick = {
smsReadExample.example17_GetSmsStatistics()
}) {
Text("获取短信统计")
}
}
}
}
private fun requestReadSmsPermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_SMS
) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.READ_SMS),
REQUEST_READ_SMS_PERMISSION
)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_READ_SMS_PERMISSION) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "权限已授予", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "需要短信读取权限", Toast.LENGTH_SHORT).show()
}
}
}
}
10.2 复杂查询示例
kotlin
lifecycleScope.launch(Dispatchers.IO) {
// 示例:查询最近 7 天来自特定号码且包含关键词的未读短信
val phoneNumber = "18337104423"
val keyword = "验证码"
val endTime = System.currentTimeMillis()
val startTime = endTime - (7 * 24 * 60 * 60 * 1000L)
// 步骤 1: 获取指定号码的短信
val messagesByPhone = smsReadUtils.getSmsByPhoneNumber(phoneNumber)
// 步骤 2: 过滤日期范围
val messagesInDateRange = messagesByPhone.filter { it.date in startTime..endTime }
// 步骤 3: 过滤包含关键词
val messagesWithKeyword = messagesInDateRange.filter { it.body.contains(keyword) }
// 步骤 4: 过滤未读
val unreadMessages = messagesWithKeyword.filter { !it.isRead() }
Log.d(TAG, "符合条件的未读短信数量: ${unreadMessages.size}")
unreadMessages.forEach { sms ->
Log.d(TAG, "----------------------------------------")
Log.d(TAG, "时间: ${sms.getFormattedDate()}")
Log.d(TAG, "号码: ${sms.address}")
Log.d(TAG, "内容: ${sms.body}")
Log.d(TAG, "----------------------------------------")
}
}
10.3 批量操作示例
kotlin
lifecycleScope.launch(Dispatchers.IO) {
try {
// 1. 备份所有短信
val backupFile = File(context.getExternalFilesDir(null), "sms_backup_${System.currentTimeMillis()}.json")
val backupResult = smsReadUtils.exportAllSmsToJson(backupFile)
if (backupResult.isSuccess) {
Log.d(TAG, "备份成功: ${backupFile.name}")
// 2. 获取统计信息
val stats = smsReadUtils.getSmsStatistics()
Log.d(TAG, "备份前统计: 总计=${stats.totalCount}, 未读=${stats.unreadCount}")
// 3. 清理超过 30 天的已读短信
val thirtyDaysAgo = System.currentTimeMillis() - (30L * 24 * 60 * 60 * 1000)
val allSms = smsReadUtils.getAllSms()
val toDelete = allSms.filter { sms ->
sms.date < thirtyDaysAgo && sms.isRead() && sms.isInbox()
}
var deletedCount = 0
toDelete.forEach { sms ->
if (smsReadUtils.deleteSms(sms.id)) {
deletedCount++
}
}
Log.d(TAG, "已删除 $deletedCount 条超过 30 天的已读短信")
// 4. 再次获取统计信息
val newStats = smsReadUtils.getSmsStatistics()
Log.d(TAG, "清理后统计: 总计=${newStats.totalCount}, 未读=${newStats.unreadCount}")
} else {
Log.e(TAG, "备份失败,取消清理操作")
}
} catch (e: Exception) {
Log.e(TAG, "批量操作失败: ${e.message}", e)
}
}
11. 技术细节
11.1 动态字段发现
工具类使用动态字段发现模式,查询时不指定 projection,自动获取所有数据库字段:
kotlin
val cursor = context.contentResolver.query(
Telephony.Sms.CONTENT_URI,
null, // null 表示获取所有字段
null,
null,
Telephony.Sms.DATE + " DESC"
)
优势:
- 自动适配不同 Android 版本
- 支持 OEM 自定义字段
- 未来兼容性好
11.2 整数溢出预防
CRITICAL :读取时间戳等大整数字段时使用 getLong() 而不是 getInt():
kotlin
// ✅ 正确:使用 getLong()
cursor.getLong(columnIndex).toString()
// ❌ 错误:使用 getInt() 会导致溢出
cursor.getInt(columnIndex).toString()
示例: 时间戳 1714567890000(2024年)读取为 Int 会变成 -1027552036。
11.3 文件输出路径
Android 10+(API 29+):
kotlin
val outputFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),
"sms_export.json"
)
Android 9 及以下:
kotlin
val outputFile = File(
Environment.getExternalStorageDirectory(),
"sms_export.json"
)
统一方法(示例类中已实现):
kotlin
private fun getOutputDir(): File {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
?: activity.filesDir
} else {
Environment.getExternalStorageDirectory()
}
}
11.4 JSON 序列化配置
kotlin
private val json = Json {
ignoreUnknownKeys = true // 忽略未知字段
encodeDefaults = true // 编码默认值
prettyPrint = true // 美化输出
}
12. 最佳实践
12.1 权限处理
kotlin
class SmsPermissionManager(private val activity: AppCompatActivity) {
fun requestPermission(callback: (granted: Boolean) -> Unit) {
if (SmsReadUtils.hasReadSmsPermission(activity)) {
callback(true)
return
}
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.READ_SMS),
REQUEST_CODE
)
// 在 Activity 的 onRequestPermissionsResult 中调用 callback
}
companion object {
const val REQUEST_CODE = 1001
}
}
12.2 错误处理
kotlin
suspend fun safeGetAllSms(): Result<List<SmsMessage>> {
return try {
if (!SmsReadUtils.hasReadSmsPermission(context)) {
return Result.failure(SecurityException("缺少 READ_SMS 权限"))
}
val messages = smsReadUtils.getAllSms()
Result.success(messages)
} catch (e: SecurityException) {
Log.e(TAG, "权限错误", e)
Result.failure(e)
} catch (e: Exception) {
Log.e(TAG, "读取短信失败", e)
Result.failure(e)
}
}
12.3 大数据量处理
kotlin
suspend fun exportSmsInBatches(outputFile: File, batchSize: Int = 1000) {
val outputDir = outputFile.parentFile!!
val baseName = outputFile.nameWithoutExtension
val extension = outputFile.extension
var totalExported = 0
var batchNumber = 1
// 获取所有短信 ID
val allSms = smsReadUtils.getAllSms()
// 分批处理
allSms.chunked(batchSize).forEach { batch ->
val batchFile = File(outputDir, "${baseName}_batch${batchNumber}.$extension")
smsReadUtils.exportSmsToJson(batch, batchFile)
totalExported += batch.size
batchNumber++
Log.d(TAG, "已导出 $totalExported / ${allSms.size} 条短信")
}
Log.d(TAG, "分批导出完成,共 $batchNumber 个文件")
}
12.4 缓存优化
kotlin
class SmsCache(private val context: Context) {
private val cache = mutableMapOf<String, List<SmsMessage>>()
private val cacheTime = mutableMapOf<String, Long>()
companion object {
private const val CACHE_DURATION = 5 * 60 * 1000L // 5 分钟
}
suspend fun getAllSms(useCache: Boolean = true): List<SmsMessage> {
val key = "all_sms"
if (useCache) {
val cached = cache[key]
val cachedTime = cacheTime[key]
if (cached != null && cachedTime != null &&
System.currentTimeMillis() - cachedTime < CACHE_DURATION) {
Log.d(TAG, "使用缓存的短信数据")
return cached
}
}
// 重新读取
val smsReadUtils = SmsReadUtils(context)
val messages = smsReadUtils.getAllSms()
// 更新缓存
cache[key] = messages
cacheTime[key] = System.currentTimeMillis()
return messages
}
fun clearCache() {
cache.clear()
cacheTime.clear()
}
}
13. 常见问题
13.1 权限被拒绝
问题:读取短信时提示权限被拒绝
解决方案:
- 在
AndroidManifest.xml中声明权限 - 在代码中动态申请权限
- 处理权限申请结果
- 引导用户在设置中手动授权
kotlin
// 检查并引导用户
if (!SmsReadUtils.hasReadSmsPermission(context)) {
AlertDialog.Builder(context)
.setTitle("需要短信权限")
.setMessage("请授予短信读取权限以继续")
.setPositiveButton("去设置") { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}
.setNegativeButton("取消", null)
.show()
}
13.2 导出文件无法找到
问题:导出后找不到文件
解决方案:
kotlin
// 导出后显示文件路径
val outputFile = File(context.getExternalFilesDir(null), "sms_export.json")
smsReadUtils.exportAllSmsToJson(outputFile)
Log.d(TAG, "文件路径: ${outputFile.absolutePath}")
// 使用 File Provider 分享文件
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
outputFile
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/json"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, "分享导出文件"))
13.3 大数据量导出内存溢出
问题:导出大量短信时内存溢出
解决方案:使用分批导出(见 12.3 节)
13.4 导入短信失败
问题:从 JSON 导入短信失败
可能原因:
- 设备/ROM 不支持写入短信
- 缺少写入权限
- JSON 格式错误
解决方案:
kotlin
// 检查是否支持导入
suspend fun canImportSms(): Boolean {
return try {
val testSms = SmsMessage(
address = "test",
body = "test",
date = System.currentTimeMillis(),
type = Telephony.Sms.MESSAGE_TYPE_INBOX
)
smsReadUtils.importSmsFromJson(
File.createTempFile("test_sms", ".json")
)
false // 如果成功,说明可能支持
} catch (e: Exception) {
Log.w(TAG, "不支持导入短信", e)
false
}
}
13.5 时间戳显示错误
问题:导出的时间戳是数字而不是日期
解决方案:使用格式化方法
kotlin
// 在导出前格式化日期
val formattedMessages = messages.map { sms ->
sms.copy(
extraFields = sms.extraFields.toMutableMap().apply {
put("formatted_date", sms.getFormattedDate())
}
)
}
14. 性能优化建议
14.1 使用协程
所有读取和导出操作都是挂起函数,应在协程中执行:
kotlin
// ✅ 正确:在 IO 线程执行
lifecycleScope.launch(Dispatchers.IO) {
val messages = smsReadUtils.getAllSms()
}
// ❌ 错误:在主线程执行会导致 ANR
val messages = smsReadUtils.getAllSms() // 不要这样做!
14.2 限制查询数量
kotlin
// 只获取需要的数量
val recentSms = smsReadUtils.getAllSms().take(100)
14.3 使用投影(Projection)
如果只需要特定字段,可以优化查询(但工具类默认获取所有字段):
kotlin
// 自定义查询(需要修改工具类代码)
val projection = arrayOf(
Telephony.Sms._ID,
Telephony.Sms.ADDRESS,
Telephony.Sms.BODY,
Telephony.Sms.DATE
)
val cursor = context.contentResolver.query(
Telephony.Sms.CONTENT_URI,
projection,
null,
null,
Telephony.Sms.DATE + " DESC"
)
15. 技术支持
15.1 相关文件
- 核心工具类 :
app/src/main/java/com/zh/systemtest/utils/SmsReadUtils.kt - 示例类 :
app/src/main/java/com/zh/systemtest/utils/SmsReadUtilsExample.kt - Android 文档 :Telephony.Sms
16. 更新日志
| 版本 | 日期 | 更新内容 |
|---|---|---|
| 1.0.0 | 2026-04-28 | 初始版本 |
17. 附录
17.1 短信类型常量
kotlin
// 短信类型
const val MESSAGE_TYPE_ALL = 0 // 所有类型
const val MESSAGE_TYPE_INBOX = 1 // 收件箱
const val MESSAGE_TYPE_SENT = 2 // 已发送
const val MESSAGE_TYPE_DRAFT = 3 // 草稿
const val MESSAGE_TYPE_OUTBOX = 4 // 发件箱
const val MESSAGE_TYPE_FAILED = 5 // 失败
const val MESSAGE_TYPE_QUEUED = 6 // 排队
17.2 数据库字段
| 字段名 | 类型 | 说明 |
|---|---|---|
_id |
Long | 短信 ID |
address |
String | 电话号码 |
body |
String | 短信内容 |
date |
Long | 接收时间 |
date_sent |
Long | 发送时间 |
type |
Int | 短信类型 |
read |
Int | 是否已读 |
status |
Int | 状态 |
thread_id |
Long | 会话 ID |
person |
String | 联系人 |
protocol |
Int | 协议 |
reply_path_present |
Int | 回复路径 |
service_center |
String | 短信中心 |
locked |
Int | 是否锁定 |
error_code |
Int | 错误码 |
seen |
Int | 是否已查看 |
sub_id |
Int | SIM 卡 ID |
creator |
String | 创建者 |