Android 彩信导出技术文档
概述
本文档介绍如何使用 Android 系统 API 导出彩信(MMS)数据。提供了 MmsUtils 工具类和 MmsUtilsExample 示例类,简化彩信导出功能的实现。
功能特性
- 从 Android 系统数据库读取 MMS 彩信数据
- 从 Android 系统数据库读取 MMS 附件数据
- 导出为 JSON 格式文件(mms.json 和 part.json)
- 自动获取数据库中的所有字段(不指定字段,动态获取)
- 支持地址信息导出
- 支持附件数据大小统计
- 使用协程在后台线程执行导出操作
- 运行时权限处理
权限要求
AndroidManifest.xml 配置
xml
<uses-permission android:name="android.permission.READ_SMS" />
Android 6.0+ 运行时权限
需要在代码中动态申请 READ_SMS 权限。
存储权限
- Android 10 (API 29) 及以上:使用应用专属目录,无需额外权限
- Android 10 以下 :需要
WRITE_EXTERNAL_STORAGE或MANAGE_EXTERNAL_STORAGE权限
MmsUtils 工具类
MmsUtils 是一个 Kotlin object 单例,封装了所有彩信导出相关的方法。
方法 1:导出所有 MMS 数据和附件数据
功能:主入口方法,导出所有 MMS 数据和附件数据到 JSON 文件。
源码:
kotlin
suspend fun exportMmsData(
context: Context,
onResult: ((success: Boolean, message: String) -> Unit)? = null
) = withContext(Dispatchers.IO) {
try {
// 检查读取短信权限
if (!hasReadSmsPermission(context)) {
onResult?.invoke(false, "缺少 READ_SMS 权限")
return@withContext
}
// 检查存储权限
val outputDir = getOutputDirectory(context)
if (outputDir == null) {
onResult?.invoke(false, "无法获取存储目录")
return@withContext
}
// 导出 MMS 数据
val mmsFile = File(outputDir, MMS_FILE)
exportMmsTo(context, mmsFile)
// 导出附件数据
val partFile = File(outputDir, PART_FILE)
exportPartsTo(context, partFile)
Log.d(TAG, "MMS 数据导出成功")
Log.d(TAG, "MMS 文件: ${mmsFile.absolutePath}")
Log.d(TAG, "PART 文件: ${partFile.absolutePath}")
onResult?.invoke(
true,
"导出成功\nMMS: ${mmsFile.absolutePath}\nPART: ${partFile.absolutePath}"
)
} catch (e: Exception) {
Log.e(TAG, "导出 MMS 数据失败", e)
onResult?.invoke(false, "导出失败: ${e.message}")
}
}
参数说明:
context: 应用上下文onResult: 导出结果回调函数,返回成功状态和消息
使用示例:
kotlin
CoroutineScope(Dispatchers.IO).launch {
MmsUtils.exportMmsData(context) { success, message ->
if (success) {
Log.d(TAG, "导出成功: $message")
} else {
Log.e(TAG, "导出失败: $message")
}
}
}
原理说明:
- 使用
withContext(Dispatchers.IO)在 IO 线程执行导出操作 - 首先检查
READ_SMS权限 - 获取输出目录(Android 10+ 使用应用专属目录,Android 10 以下使用外部存储)
- 分别调用
exportMmsTo()和exportPartsTo()导出数据 - 通过回调函数返回导出结果和文件路径
方法 2:导出 MMS 数据到指定文件
功能:读取系统 MMS 数据库,导出所有彩信记录到 JSON 文件。
源码:
kotlin
private fun exportMmsTo(context: Context, outputFile: File) {
val mmsArray = JSONArray()
// 使用 null 作为 projection 参数,获取所有字段
val cursor = context.contentResolver.query(
Telephony.Mms.CONTENT_URI,
null, // null 表示获取所有字段
null,
null,
Telephony.Mms.DATE + " DESC"
)
cursor?.use {
val columnNames = it.columnNames
while (it.moveToNext()) {
val mmsJson = JSONObject()
// 遍历所有列,动态添加到 JSON 中
for (columnName in columnNames) {
val columnIndex = it.getColumnIndex(columnName)
if (columnIndex >= 0) {
val value = getColumnValue(it, columnIndex, it.getType(columnIndex))
mmsJson.put(columnName, value)
}
}
// 读取地址信息
val mmsId = it.getLong(it.getColumnIndexOrThrow(Telephony.Mms._ID))
val addresses = getMmsAddresses(context, mmsId)
mmsJson.put("addr", addresses)
mmsArray.put(mmsJson)
}
}
// 写入文件
writeJsonFile(outputFile, mmsArray)
Log.d(TAG, "已导出 ${mmsArray.length()} 条 MMS 记录")
}
参数说明:
context: 应用上下文outputFile: 输出文件对象
原理说明:
- 查询
Telephony.Mms.CONTENT_URI获取所有彩信数据 projection = null表示获取所有字段(不指定字段)columnNames获取数据库所有列名- 遍历所有列,根据列类型获取对应的值
- 使用
getColumnValue()根据列类型获取值 - 为每条 MMS 获取对应的地址信息
- 按日期降序排列(最新的在前)
导出的字段示例(约50-60个字段):
json
{
"_id": 4,
"thread_id": 3,
"date": 1775109764,
"date_sent": 0,
"msg_box": 1,
"read": 1,
"m_id": "040214023793110224606",
"sub": "",
"sub_cs": "",
"ct_t": "application/vnd.wap.multipart.related",
"ct_l": "",
"exp": "",
"m_cls": "personal",
"m_type": 132,
"v": 16,
"m_size": 5508,
"pri": "",
"rr": "",
"rpt_a": "",
"resp_st": "",
"st": "",
"tr_id": "##x2qWE0u-z0",
"retr_st": "",
"retr_txt": "",
"retr_txt_cs": "",
"read_status": "",
"ct_cls": "",
"resp_txt": "",
"d_tm": 1775109757000,
"d_rpt": "",
"locked": 0,
"sub_id": 1,
"creator": "com.android.mms",
"seen": 1,
"st_ext": 0,
"is_encrypted": 0,
"time": 1775109764000,
"service_center": "",
"dirty": 1,
"text_only": 0,
"prepared_type": 3,
"prepared_body": "",
"prepared_width": 0,
"prepared_height": 0,
"risk_website": 2,
"message_mode": 0,
"phone_id": -1,
"subject_txt": "",
"block_mms_type": 0,
"timing_date": 0,
"timing_msgst": 0,
"thread_type": 0,
"recycle_date": 0,
"sch_priority": 0,
"show_msg_category": 1,
"addr": [...]
}
方法 3:导出附件数据到指定文件
功能:读取系统 MMS 附件数据库,导出所有附件记录到 JSON 文件。
源码:
kotlin
private fun exportPartsTo(context: Context, outputFile: File) {
val partsArray = JSONArray()
// 使用 null 作为 projection 参数,获取所有字段
val cursor = context.contentResolver.query(
Telephony.Mms.Part.CONTENT_URI,
null, // null 表示获取所有字段
null,
null,
Telephony.Mms.Part._ID
)
cursor?.use {
val columnNames = it.columnNames
while (it.moveToNext()) {
val partJson = JSONObject()
// 遍历所有列,动态添加到 JSON 中
for (columnName in columnNames) {
val columnIndex = it.getColumnIndex(columnName)
if (columnIndex >= 0) {
val value = getColumnValue(it, columnIndex, it.getType(columnIndex))
partJson.put(columnName, value)
}
}
// 读取附件数据(如果是文本或图片等)
val partId = it.getLong(it.getColumnIndexOrThrow(Telephony.Mms.Part._ID))
val contentTypeIndex = it.getColumnIndex(Telephony.Mms.Part.CONTENT_TYPE)
val contentType = if (contentTypeIndex >= 0) {
val type = it.getString(contentTypeIndex)
if (!it.isNull(contentTypeIndex) && type.isNotEmpty()) type else null
} else null
if (contentType != null) {
val partData = getPartData(context, partId, contentType)
if (partData != null) {
partJson.put("data_size", partData.size)
// 注意:不直接保存二进制数据到 JSON,仅保存大小
// 如需保存内容,可另外保存到文件
}
}
partsArray.put(partJson)
}
}
// 写入文件
writeJsonFile(outputFile, partsArray)
Log.d(TAG, "已导出 ${partsArray.length()} 条附件记录")
}
参数说明:
context: 应用上下文outputFile: 输出文件对象
原理说明:
- 查询
Telephony.Mms.Part.CONTENT_URI获取所有附件数据 projection = null获取所有字段- 遍历所有列,动态添加到 JSON
- 对于有内容的附件,额外添加
data_size字段(附件大小) - 不直接保存二进制数据到 JSON(避免文件过大)
方法 4:获取 MMS 地址信息
功能:获取指定 MMS 的所有地址信息(发送者、接收者等)。
源码:
kotlin
private fun getMmsAddresses(context: Context, mmsId: Long): JSONArray {
val addressesArray = JSONArray()
val uri = Telephony.Mms.CONTENT_URI.buildUpon()
.appendPath(mmsId.toString())
.appendPath("addr")
.build()
// 使用 null 作为 projection 参数,获取所有字段
val cursor = context.contentResolver.query(uri, null, null, null, null)
cursor?.use {
val columnNames = it.columnNames
while (it.moveToNext()) {
val addrJson = JSONObject()
// 遍历所有列,动态添加到 JSON 中
for (columnName in columnNames) {
val columnIndex = it.getColumnIndex(columnName)
if (columnIndex >= 0) {
val value = getColumnValue(it, columnIndex, it.getType(columnIndex))
addrJson.put(columnName, value)
}
}
addressesArray.put(addrJson)
}
}
return addressesArray
}
参数说明:
context: 应用上下文mmsId: MMS 记录的 ID
返回值:
- JSONArray:包含所有地址信息的数组
原理说明:
- 构建 URI:
content://mms/{mmsId}/addr - 查询该 URI 获取指定 MMS 的地址信息
- 遍历所有列,动态添加到 JSON
地址数据示例:
json
{
"_id": 9,
"msg_id": 4,
"address": "+8615720304196",
"type": 137,
"charset": 106
}
方法 5:获取附件数据
功能:读取附件的二进制数据。
源码:
kotlin
private fun getPartData(context: Context, partId: Long, contentType: String): ByteArray? {
return try {
val partUri = ContentUris.withAppendedId(Telephony.Mms.Part.CONTENT_URI, partId)
context.contentResolver.openInputStream(partUri)?.use { input ->
input.readBytes()
}
} catch (e: Exception) {
Log.w(TAG, "读取附件数据失败: $partId", e)
null
}
}
参数说明:
context: 应用上下文partId: 附件 IDcontentType: 附件类型(如 "image/jpeg")
返回值:
- ByteArray?: 附件的二进制数据,失败返回 null
原理说明:
- 使用
ContentUris.withAppendedId()构建附件 URI - 使用
openInputStream()打开输入流 - 读取所有字节为 ByteArray
- 使用
use确保输入流正确关闭
方法 6:获取输出目录
功能:根据 Android 版本获取合适的输出目录。
源码:
kotlin
private fun getOutputDirectory(context: Context): File? {
return try {
val baseDir = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
// Android 10+ 使用 MediaStore API 或应用专属目录
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
?: context.filesDir
}
else -> {
// Android 10 以下使用外部存储
Environment.getExternalStorageDirectory()
}
}
val outputDir = File(baseDir, OUTPUT_DIR)
if (!outputDir.exists()) {
outputDir.mkdirs()
}
outputDir
} catch (e: Exception) {
Log.e(TAG, "获取输出目录失败", e)
null
}
}
返回值:
- File?: 输出目录对象,失败返回 null
原理说明:
- Android 10 (API 29) 及以上 :
- 使用
getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) - 应用专属目录,无需额外权限
- 路径示例:
/storage/emulated/0/Android/data/com.zh.systemtest/files/Documents/systemtest/
- 使用
- Android 10 以下 :
- 使用
Environment.getExternalStorageDirectory() - 需要
WRITE_EXTERNAL_STORAGE权限 - 路径示例:
/storage/emulated/0/systemtest/
- 使用
方法 7:写入 JSON 文件
功能:将 JSON 数组写入文件,使用缩进格式化。
源码:
kotlin
private fun writeJsonFile(file: File, jsonArray: JSONArray) {
FileOutputStream(file).use { fos ->
fos.write(jsonArray.toString(2).toByteArray())
fos.flush()
}
}
参数说明:
file: 输出文件对象jsonArray: JSON 数组对象
原理说明:
- 使用
toString(2)格式化 JSON(2个空格缩进) - 使用
FileOutputStream写入文件 - 使用
use确保 FileOutputStream 正确关闭
方法 8:检查权限
功能:检查是否有读取短信权限。
源码:
kotlin
fun hasReadSmsPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_SMS
) == PackageManager.PERMISSION_GRANTED
}
参数说明:
context: 应用上下文
返回值:
- Boolean: 是否有权限
方法 9:获取输出文件路径
功能:获取导出文件的完整路径,用于显示给用户。
源码:
kotlin
fun getOutputFilePaths(context: Context): Pair<String?, String?> {
val outputDir = getOutputDirectory(context) ?: return null to null
return File(outputDir, MMS_FILE).absolutePath to File(outputDir, PART_FILE).absolutePath
}
参数说明:
context: 应用上下文
返回值:
- Pair<String?, String?>: MMS 文件路径和 Part 文件路径
方法 10:根据列类型获取对应的值
功能:根据数据库列类型(STRING, INTEGER, FLOAT, BLOB, NULL)获取对应的值。
源码:
kotlin
private fun getColumnValue(cursor: Cursor, columnIndex: Int, columnType: Int): Any {
return when (columnType) {
Cursor.FIELD_TYPE_STRING -> cursor.getString(columnIndex) ?: ""
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(columnIndex)
Cursor.FIELD_TYPE_FLOAT -> cursor.getDouble(columnIndex)
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(columnIndex).toString(Charsets.ISO_8859_1)
Cursor.FIELD_TYPE_NULL -> ""
else -> ""
}
}
参数说明:
cursor: 数据库游标columnIndex: 列索引columnType: 列类型
返回值:
- Any: 对应的值
原理说明:
FIELD_TYPE_STRING: 字符串类型,调用getString()FIELD_TYPE_INTEGER: 整数类型,调用getLong()FIELD_TYPE_FLOAT: 浮点类型,调用getDouble()FIELD_TYPE_BLOB: 二进制类型,调用getBlob()后转为字符串(主要用于调试)FIELD_TYPE_NULL: NULL 值,返回空字符串
MmsUtilsExample 示例类
示例 1:简单的 MMS 导出
功能:最简单的使用示例,直接导出 MMS 数据。
源码:
kotlin
fun example1(activity: Activity) {
CoroutineScope(Dispatchers.IO).launch {
MmsUtils.exportMmsData(activity) { success, message ->
activity.runOnUiThread {
if (success) {
Toast.makeText(activity, message, Toast.LENGTH_LONG).show()
Log.d(TAG, message)
} else {
Toast.makeText(activity, "导出失败: $message", Toast.LENGTH_LONG).show()
Log.e(TAG, "导出失败: $message")
}
}
}
}
}
使用场景:
- 快速测试导出功能
- 演示基本用法
示例 2:带权限检查的 MMS 导出
功能:先检查权限,未授权则申请权限。
源码:
kotlin
fun example2(activity: Activity) {
requestAndExportMms(activity)
}
fun requestAndExportMms(activity: Activity) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.READ_SMS
) == PackageManager.PERMISSION_GRANTED
) {
// 已有权限,直接导出
exportMmsInBackground(activity)
} else {
// 申请权限
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.READ_SMS),
REQUEST_READ_SMS_PERMISSION
)
}
}
使用场景:
- 生产环境使用
- 需要处理运行时权限
示例 3:在后台线程导出 MMS
功能:在后台线程执行导出操作,避免阻塞主线程。
源码:
kotlin
private fun exportMmsInBackground(activity: Activity) {
CoroutineScope(Dispatchers.IO).launch {
// 显示导出路径
val (mmsPath, partPath) = MmsUtils.getOutputFilePaths(activity)
Log.d(TAG, "MMS 文件将保存到: $mmsPath")
Log.d(TAG, "PART 文件将保存到: $partPath")
// 执行导出
MmsUtils.exportMmsData(activity) { success, message ->
activity.runOnUiThread {
if (success) {
Toast.makeText(activity, "导出成功!\n$message", Toast.LENGTH_LONG).show()
Log.d(TAG, "导出成功")
} else {
Toast.makeText(activity, "导出失败: $message", Toast.LENGTH_LONG).show()
Log.e(TAG, "导出失败: $message")
}
}
}
}
}
原理说明:
- 使用
CoroutineScope(Dispatchers.IO)在 IO 线程执行 - 使用
activity.runOnUiThread在主线程更新 UI - 避免在回调中直接操作 Toast(会导致异常)
示例 4:处理权限申请结果
功能:处理用户对权限申请的响应。
源码:
kotlin
fun handlePermissionResult(
activity: Activity,
requestCode: Int,
grantResults: IntArray
) {
if (requestCode == REQUEST_READ_SMS_PERMISSION) {
val granted = grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
// 权限已授予,执行导出
exportMmsInBackground(activity)
} else {
Toast.makeText(activity, "需要读取短信权限才能导出 MMS", Toast.LENGTH_LONG).show()
Log.w(TAG, "用户拒绝了 READ_SMS 权限")
}
}
}
使用方式:
在 Activity 的 onRequestPermissionsResult() 中调用:
kotlin
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
MmsUtilsExample.handlePermissionResult(this, requestCode, grantResults)
}
示例 5:完整的 Activity 集成
功能:完整的 Activity 集成示例。
源码:
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 导出 MMS
findViewById<Button>(R.id.btn_export_mms).setOnClickListener {
MmsUtilsExample.requestAndExportMms(this)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
MmsUtilsExample.handlePermissionResult(this, requestCode, grantResults)
}
}
示例 6:检查权限状态
功能:检查当前权限状态和输出路径。
源码:
kotlin
fun checkPermissionStatus(activity: Activity) {
val hasPermission = MmsUtils.hasReadSmsPermission(activity)
Log.d(TAG, "READ_SMS 权限: ${if (hasPermission) "已授权" else "未授权"}")
val (mmsPath, partPath) = MmsUtils.getOutputFilePaths(activity)
Log.d(TAG, "MMS 输出路径: $mmsPath")
Log.d(TAG, "PART 输出路径: $partPath")
}
示例 7:获取输出文件路径
功能:获取导出文件的完整路径。
源码:
kotlin
fun getOutputPaths(activity: Activity): Pair<String?, String?> {
return MmsUtils.getOutputFilePaths(activity)
}
返回值:
- Pair<String?, String?>: (MMS 文件路径, Part 文件路径)
核心概念解释
1. ContentResolver
Android 的内容提供者客户端,用于查询数据。
kotlin
val cursor = context.contentResolver.query(
Telephony.Mms.CONTENT_URI, // 内容 URI
null, // projection(null 表示所有字段)
null, // selection(筛选条件)
null, // selectionArgs
Telephony.Mms.DATE + " DESC" // sortOrder(排序)
)
2. Telephony.Mms.CONTENT_URI
MMS 彩信的内容 URI,用于查询彩信数据。
kotlin
Telephony.Mms.CONTENT_URI // content://mms
3. Telephony.Mms.Part.CONTENT_URI
MMS 附件的内容 URI,用于查询附件数据。
kotlin
Telephony.Mms.Part.CONTENT_URI // content://mms/part
4. Cursor
数据库查询结果的游标,用于遍历数据。
kotlin
cursor?.use {
while (it.moveToNext()) {
val value = it.getString(it.getColumnIndex("column_name"))
}
}
5. JSONArray 和 JSONObject
用于构建 JSON 数据。
kotlin
val jsonArray = JSONArray()
val jsonObject = JSONObject()
jsonObject.put("key", "value")
jsonArray.put(jsonObject)
6. 协程(Coroutine)
Kotlin 的协程,用于异步操作。
kotlin
CoroutineScope(Dispatchers.IO).launch {
// 在 IO 线程执行
MmsUtils.exportMmsData(context) { success, message ->
// 回调仍在 IO 线程
}
}
导出文件格式
mms.json 格式
json
[
{
"_id": 4,
"thread_id": 3,
"date": 1775109764,
"date_sent": 0,
"msg_box": 1,
"read": 1,
"m_id": "040214023793110224606",
"sub": "",
"sub_cs": "",
"ct_t": "application/vnd.wap.multipart.related",
"ct_l": "",
"exp": "",
"m_cls": "personal",
"m_type": 132,
"v": 16,
"m_size": 5508,
"pri": "",
"rr": "",
"rpt_a": "",
"resp_st": "",
"st": "",
"tr_id": "##x2qWE0u-z0",
"retr_st": "",
"retr_txt": "",
"retr_txt_cs": "",
"read_status": "",
"ct_cls": "",
"resp_txt": "",
"d_tm": 1775109757000,
"d_rpt": "",
"locked": 0,
"sub_id": 1,
"creator": "com.android.mms",
"seen": 1,
"st_ext": 0,
"is_encrypted": 0,
"time": 1775109764000,
"service_center": "",
"dirty": 1,
"text_only": 0,
"prepared_type": 3,
"prepared_body": "",
"prepared_width": 0,
"prepared_height": 0,
"risk_website": 2,
"message_mode": 0,
"phone_id": -1,
"subject_txt": "",
"block_mms_type": 0,
"timing_date": 0,
"timing_msgst": 0,
"thread_type": 0,
"recycle_date": 0,
"sch_priority": 0,
"show_msg_category": 1,
"addr": [
{
"_id": 9,
"msg_id": 4,
"address": "+8615720304196",
"type": 137,
"charset": 106
}
]
}
]
part.json 格式
json
[
{
"_id": 12,
"mid": 4,
"seq": 0,
"ct": "text/plain",
"name": null,
"chset": 106,
"cd": null,
"fn": null,
"cid": null,
"cl": null,
"ctt_s": null,
"ctt_t": null,
"text": "这是一条彩信的文本内容",
"data_size": 36
},
{
"_id": 13,
"mid": 4,
"seq": 1,
"ct": "image/jpeg",
"name": null,
"chset": null,
"cd": "attachment",
"fn": "image.jpg",
"cid": null,
"cl": null,
"ctt_s": null,
"ctt_t": null,
"text": null,
"data_size": 10240
}
]
注意事项
1. 权限要求
- Android 6.0(API 23)及以上版本需要在运行时申请
READ_SMS权限 - Android 10(API 29)以下需要
WRITE_EXTERNAL_STORAGE权限 - Android 10 及以上使用应用专属目录,无需额外权限
2. 线程安全
- 导出操作必须在后台线程执行(使用协程或线程)
- 回调函数在后台线程,更新 UI 必须切换到主线程
- 使用
runOnUiThread或withContext(Dispatchers.Main)切换线程
3. 文件路径
- Android 10+ 输出路径:
/storage/emulated/0/Android/data/{包名}/files/Documents/systemtest/ - Android 10 以下输出路径:
/storage/emulated/0/systemtest/ - 输出目录不存在时会自动创建
4. 附件数据
- 不直接保存二进制数据到 JSON 文件(避免文件过大)
- 仅保存附件大小(
data_size字段) - 如需保存附件内容,需要额外实现
5. 所有字段导出
- 使用
null作为 projection 参数,导出所有字段 - 包括空字符串字段
- 确保导出完整的数据(约50-60个字段)
6. 错误处理
- 使用
try-catch捕获异常 - 通过回调函数返回错误信息
- 记录详细的错误日志
最佳实践
- 在后台线程执行:使用协程或线程在后台执行导出操作
- 处理运行时权限:Android 6.0+ 需要动态申请权限
- UI 线程切换:更新 UI 时切换到主线程
- 提供用户反馈:使用 Toast 或进度条显示导出进度
- 错误处理:捕获异常并提供友好的错误提示
- 文件路径显示:导出成功后显示文件路径,方便用户查找
- 空指针检查:对可能为 null 的对象进行检查
常见问题
Q: 导出的 JSON 文件保存在哪里?
A:
- Android 10+ :
/storage/emulated/0/Android/data/{包名}/files/Documents/systemtest/ - Android 10 以下 :
/storage/emulated/0/systemtest/
Q: 为什么需要 READ_SMS 权限?
A: 读取系统 MMS 数据库需要 READ_SMS 权限,这是 Android 的安全机制。
Q: 可以导出附件的二进制数据吗?
A: 当前实现仅保存附件大小。如需保存附件内容,可以:
- 将附件保存为单独的文件
- 使用 Base64 编码保存到 JSON(会增大文件大小)
Q: 为什么导出的字段数量不同?
A: 不同 Android 版本和厂商 ROM 的 MMS 数据库字段可能不同。使用 null projection 可以导出所有可用字段。
Q: 如何处理大量数据?
A:
- 使用分页查询(LIMIT 和 OFFSET)
- 显示导出进度
- 使用异步任务避免阻塞主线程
Q: 为什么有些字段是空字符串?
A: 这些字段在数据库中为 NULL 或空字符串。导出所有字段可以保持数据完整性。
更新日志
v2.0 (当前版本)
- 修改为导出所有字段(使用 null projection)
- 移除固定的字段定义
- 动态获取数据库所有列
- 添加详细的源码解释
- 完善文档
v1.0
- 基础 MMS 导出功能
- 附件数据导出
- 地址信息导出
- 权限处理
- 协程支持