Android 彩信导出技术文档

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_STORAGEMANAGE_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: 附件 ID
  • contentType: 附件类型(如 "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 必须切换到主线程
  • 使用 runOnUiThreadwithContext(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 捕获异常
  • 通过回调函数返回错误信息
  • 记录详细的错误日志

最佳实践

  1. 在后台线程执行:使用协程或线程在后台执行导出操作
  2. 处理运行时权限:Android 6.0+ 需要动态申请权限
  3. UI 线程切换:更新 UI 时切换到主线程
  4. 提供用户反馈:使用 Toast 或进度条显示导出进度
  5. 错误处理:捕获异常并提供友好的错误提示
  6. 文件路径显示:导出成功后显示文件路径,方便用户查找
  7. 空指针检查:对可能为 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: 当前实现仅保存附件大小。如需保存附件内容,可以:

  1. 将附件保存为单独的文件
  2. 使用 Base64 编码保存到 JSON(会增大文件大小)

Q: 为什么导出的字段数量不同?

A: 不同 Android 版本和厂商 ROM 的 MMS 数据库字段可能不同。使用 null projection 可以导出所有可用字段。

Q: 如何处理大量数据?

A:

  • 使用分页查询(LIMIT 和 OFFSET)
  • 显示导出进度
  • 使用异步任务避免阻塞主线程

Q: 为什么有些字段是空字符串?

A: 这些字段在数据库中为 NULL 或空字符串。导出所有字段可以保持数据完整性。


更新日志

v2.0 (当前版本)

  • 修改为导出所有字段(使用 null projection)
  • 移除固定的字段定义
  • 动态获取数据库所有列
  • 添加详细的源码解释
  • 完善文档

v1.0

  • 基础 MMS 导出功能
  • 附件数据导出
  • 地址信息导出
  • 权限处理
  • 协程支持

相关推荐
sp42a2 小时前
安卓原生 MQTT 通讯 Java 实现
android·java·mqtt
Mr Lee_2 小时前
Apktool 反编译与回编译详解:enableOnBackInvokedCallback 属性缺失问题分析与解决
android
高梦轩8 小时前
MySQL高可用
android·运维·数据库
RATi GORI12 小时前
MySQL中的CASE WHEN语句:用法、示例与解析
android·数据库·mysql
MoFe112 小时前
【Mysql】创建IP授权用户并授权
android
冬奇Lab13 小时前
Camera2 API架构基础:Android视频系统的大门
android·音视频开发·源码阅读
hnlgzb14 小时前
安卓app kotlin语法,Hilt是什么东西?
android·开发语言·kotlin
Android系统攻城狮15 小时前
Android tinyalsa深度解析之pcm_params_get_periods_min调用流程与实战(一百七十三)
android·pcm·tinyalsa·音频进阶手册
Xempastissimo16 小时前
Android常见界面控件
android