android 短信读取与导出技术

概述

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 权限被拒绝

问题:读取短信时提示权限被拒绝

解决方案:

  1. AndroidManifest.xml 中声明权限
  2. 在代码中动态申请权限
  3. 处理权限申请结果
  4. 引导用户在设置中手动授权
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 导入短信失败

可能原因:

  1. 设备/ROM 不支持写入短信
  2. 缺少写入权限
  3. 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 创建者

相关推荐
dalancon2 小时前
Android LMKD 服务
android
迪普阳光开朗很健康2 小时前
告别繁琐!用ApkInfoQuick快速提取APK关键信息
android·rust·react
深度智能Ai2 小时前
GPT Image 2 图片生成 API 接口对接文档
android·gpt
VincentWei952 小时前
Compose:1.5 无状态与状态提升(State Hoisting)
android
xingpanvip2 小时前
星盘接口开发文档:天象盘接口指南
android·开发语言·python·php·lua
天涯海风2 小时前
写一个录音并保存到手机的工具 安卓工具类
android·java·智能手机
黄林晴3 小时前
Koin 开发者炸了!7 条规则根治运行时错误,自动扫描太香了
android
恋猫de小郭3 小时前
Flutter 3.41.8 又双叒修复调试问题,草台班子日常 hotfix
android·前端·flutter
火山上的企鹅3 小时前
QGC 二次开发(RTK):内置 NTRIP Client,实现 CORS 差分数据接入与 GPS_RTCM_DATA 转发
android·无人机·rtk·qgroundcontrol