android 通话记录相关

概述

CallLogUtils 是一个用于读取、导出和管理 Android 通话记录的工具类。它提供了完整的通话记录操作功能,包括查询、导出、导入、删除和统计分析。

核心特性

  • ✅ 动态字段发现,自动获取所有可用通话记录字段
  • ✅ 支持按类型筛选(来电、去电、未接、语音信箱等)
  • ✅ 支持按号码查询通话记录
  • ✅ 导出为 JSON 和 CSV 格式
  • ✅ 从 JSON 导入通话记录
  • ✅ 删除指定通话记录
  • ✅ 通话统计分析功能
  • ✅ 完善的权限检查
  • ✅ 使用 Kotlin Result 类型进行错误处理

架构设计

类结构

复制代码
CallLogUtils (核心工具类)
├── 数据模型
│   └── CallRecord (通话记录数据类)
│       └── CallStatistics (通话统计信息)
├── 读取功能
│   ├── getAllCallLogs() - 获取所有通话记录
│   ├── getCallLogsByNumber() - 按号码查询
│   ├── getMissedCalls() - 获取未接来电
│   ├── getOutgoingCalls() - 获取去电记录
│   └── getIncomingCalls() - 获取来电记录
├── 导出功能
│   ├── exportCallLogsToJson() - 导出为 JSON
│   └── exportCallLogsToCsv() - 导出为 CSV
├── 导入功能
│   └── importCallLogsFromJson() - 从 JSON 导入
├── 删除功能
│   ├── deleteCallLog() - 删除单条记录
│   ├── deleteCallLogsByNumber() - 删除指定号码的所有记录
│   └── clearAllCallLogs() - 清空所有记录
└── 统计功能
    ├── getCallLogsCount() - 获取记录数量
    └── getCallStatistics() - 获取统计信息

技术栈

  • 语言: Kotlin
  • 序列化: kotlinx.serialization
  • API: Android Telephony API (CallLog.Calls)
  • 目标 SDK: Android 14 (API 34)
  • 最低 SDK: Android 7.0 (API 24)

API 文档

数据模型

CallRecord

通话记录数据类,使用动态 Map 存储所有字段。

kotlin 复制代码
@Serializable
data class CallRecord(
    val fields: Map<String, String> = emptyMap()
)

属性

属性名 类型 说明
id Long 通话记录 ID
number String 电话号码
formattedNumber String 格式化后的号码
name String 联系人名称
type Int 通话类型(1=来电,2=去电,3=未接)
typeLabel String 通话类型标签(中文)
date Long 通话时间(毫秒时间戳)
formattedDate String 格式化后的日期时间
duration Int 通话时长(秒)
formattedDuration String 格式化后的时长
numberType Int 号码类型
numberTypeLabel String 号码类型标签
isRead String 是否已读

方法

方法名 返回类型 说明
getField(fieldName: String) String? 获取指定字段的值
getAllFieldNames() Set<String> 获取所有字段名
CallStatistics

通话统计信息数据类。

kotlin 复制代码
@Serializable
data class CallStatistics(
    val totalCalls: Int = 0,           // 总通话数
    val incomingCalls: Int = 0,         // 来电数
    val outgoingCalls: Int = 0,         // 去电数
    val missedCalls: Int = 0,           // 未接数
    val totalDuration: Int = 0,         // 总时长(秒)
    val averageDuration: Int = 0,       // 平均时长(秒)
    val formattedTotalDuration: String = "0秒"  // 格式化总时长
)

读取通话记录

getAllCallLogs()

获取所有通话记录,动态获取所有可用字段。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getAllCallLogs(limit: Int = -1): List<CallRecord>

参数

  • limit: 限制返回数量,-1 表示不限制

返回

  • List<CallRecord>: 通话记录列表

示例

kotlin 复制代码
val callLogUtils = CallLogUtils(context)

// 获取所有通话记录
val allCalls = callLogUtils.getAllCallLogs()

// 只获取最近 10 条
val recentCalls = callLogUtils.getAllCallLogs(limit = 10)

allCalls.forEach { call ->
    println("${call.formattedDate} - ${call.typeLabel} - ${call.name.ifEmpty { call.number }}")
}
getCallLogsByNumber()

根据电话号码查询通话记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getCallLogsByNumber(phoneNumber: String): List<CallRecord>

参数

  • phoneNumber: 电话号码

示例

kotlin 复制代码
val callsByNumber = callLogUtils.getCallLogsByNumber("13800138000")
println("该号码共有 ${callsByNumber.size} 条通话记录")
getMissedCalls()

获取未接来电。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getMissedCalls(): List<CallRecord>
getOutgoingCalls()

获取去电记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getOutgoingCalls(): List<CallRecord>
getIncomingCalls()

获取来电记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getIncomingCalls(): List<CallRecord>

导出通话记录

exportCallLogsToJson()

导出通话记录为 JSON 文件。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun exportCallLogsToJson(outputFile: File): Result<Int>

参数

  • outputFile: 输出文件

返回

  • Result<Int>: 成功返回导出的记录数量,失败返回错误

示例

kotlin 复制代码
val outputFile = File(context.getExternalFilesDir(null), "calllogs_export.json")

callLogUtils.exportCallLogsToJson(outputFile)
    .onSuccess { count ->
        println("成功导出 $count 条通话记录")
    }
    .onFailure { error ->
        println("导出失败: ${error.message}")
    }

JSON 输出格式

json 复制代码
[
    {
        "fields": {
            "_id": "46",
            "number": "13800138000",
            "date": "1714567890000",
            "type": "2",
            "duration": "120",
            "name": "张三",
            "geocoded_location": "北京市"
            // ... 其他动态字段
        }
    }
]
exportCallLogsToCsv()

导出通话记录为 CSV 文件。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun exportCallLogsToCsv(outputFile: File): Result<Int>

CSV 输出格式

csv 复制代码
_id,number,date,type,duration,name,geocoded_location
46,13800138000,1714567890000,2,120,张三,北京市

导入通话记录

importCallLogsFromJson()

从 JSON 文件导入通话记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun importCallLogsFromJson(inputFile: File): Result<Int>

注意

  • 写入通话记录可能受到系统限制
  • 某些 ROM 可能限制第三方应用写入通话记录

示例

kotlin 复制代码
val inputFile = File(context.getExternalFilesDir(null), "calllogs_export.json")

callLogUtils.importCallLogsFromJson(inputFile)
    .onSuccess { count ->
        println("成功导入 $count 条通话记录")
    }
    .onFailure { error ->
        println("导入失败: ${error.message}")
    }

删除通话记录

deleteCallLog()

删除指定的通话记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun deleteCallLog(callId: Long): Boolean

参数

  • callId: 通话记录 ID

返回

  • Boolean: 删除是否成功

示例

kotlin 复制代码
val success = callLogUtils.deleteCallLog(46)
if (success) {
    println("删除成功")
}
deleteCallLogsByNumber()

删除指定号码的所有通话记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun deleteCallLogsByNumber(phoneNumber: String): Result<Int>

返回

  • Result<Int>: 成功返回删除的记录数量
clearAllCallLogs()

清空所有通话记录。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun clearAllCallLogs(): Result<Int>

统计功能

getCallLogsCount()

获取通话记录总数。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getCallLogsCount(): Int
getCallStatistics()

获取通话统计信息。

kotlin 复制代码
@SuppressLint("MissingPermission")
fun getCallStatistics(): CallStatistics

示例

kotlin 复制代码
val stats = callLogUtils.getCallStatistics()

println("总通话数: ${stats.totalCalls}")
println("来电: ${stats.incomingCalls}")
println("去电: ${stats.outgoingCalls}")
println("未接: ${stats.missedCalls}")
println("总时长: ${stats.formattedTotalDuration}")

辅助方法

权限检查
kotlin 复制代码
// 检查读取权限
fun hasReadCallLogPermission(context: Context): Boolean

// 检查写入权限
fun hasWriteCallLogPermission(context: Context): Boolean
格式化工具
kotlin 复制代码
// 获取通话类型标签
fun getCallTypeLabel(type: Int): String

// 获取号码类型标签
fun getNumberTypeLabel(type: Int): String

// 格式化通话时长
fun formatDuration(seconds: Int): String

// 格式化日期时间
fun formatDateTime(timestamp: Long): String

// 格式化电话号码
fun formatPhoneNumber(phoneNumber: String): String

使用示例

基础用法

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private val callLogUtils by lazy { CallLogUtils(this) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 检查权限
        if (!CallLogUtils.hasReadCallLogPermission(this)) {
            requestPermissions()
            return
        }

        // 获取最近 5 条通话记录
        val recentCalls = callLogUtils.getAllCallLogs(limit = 5)

        recentCalls.forEach { call ->
            println("${call.formattedDate}")
            println("  ${call.typeLabel}: ${call.name.ifEmpty { call.number }}")
            println("  时长: ${call.formattedDuration}")
        }
    }
}

导出并备份通话记录

kotlin 复制代码
fun backupCallLogs() {
    val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
        .format(Date())

    val backupFile = File(
        getExternalFilesDir(null),
        "calllogs_backup_$timestamp.json"
    )

    callLogUtils.exportCallLogsToJson(backupFile)
        .onSuccess { count ->
            Toast.makeText(this, "备份成功: $count 条记录", Toast.LENGTH_SHORT).show()
        }
        .onFailure { error ->
            Toast.makeText(this, "备份失败: ${error.message}", Toast.LENGTH_SHORT).show()
        }
}

分析未接来电

kotlin 复制代码
fun analyzeMissedCalls() {
    val missedCalls = callLogUtils.getMissedCalls()

    // 统计未接号码
    val missedNumbers = missedCalls
        .map { it.number }
        .groupBy { it }
        .mapValues { it.value.size }
        .toList()
        .sortedByDescending { it.second }

    println("=== 未接来电分析 ===")
    missedNumbers.take(10).forEach { (number, count) ->
        println("$number: $count 次未接")
    }
}

清空指定号码的通话记录

kotlin 复制代码
fun clearNumberFromCallLog(phoneNumber: String) {
    callLogUtils.deleteCallLogsByNumber(phoneNumber)
        .onSuccess { count ->
            Toast.makeText(this, "已删除 $phoneNumber 的 $count 条记录", Toast.LENGTH_SHORT).show()
        }
        .onFailure { error ->
            Toast.makeText(this, "删除失败: ${error.message}", Toast.LENGTH_SHORT).show()
        }
}

批量导出为 CSV

kotlin 复制代码
fun exportToCsv() {
    val csvFile = File(getExternalFilesDir(null), "calllogs_export.csv")

    callLogUtils.exportCallLogsToCsv(csvFile)
        .onSuccess { count ->
            // 分享 CSV 文件
            val uri = FileProvider.getUriForFile(
                this,
                "${packageName}.fileprovider",
                csvFile
            )

            val intent = Intent(Intent.ACTION_SEND).apply {
                type = "text/csv"
                putExtra(Intent.EXTRA_STREAM, uri)
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            }

            startActivity(Intent.createChooser(intent, "分享通话记录"))
        }
        .onFailure { error ->
            Toast.makeText(this, "导出失败", Toast.LENGTH_SHORT).show()
        }
}

权限要求

AndroidManifest.xml 声明

xml 复制代码
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />

运行时权限请求(Android 6.0+)

kotlin 复制代码
private val requestCallLogPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
        val allGranted = permissions.values.all { it }
        if (allGranted) {
            // 权限已授予,可以访问通话记录
            loadCallLogs()
        } else {
            // 权限被拒绝,显示提示
            Toast.makeText(this, "需要通话记录权限", Toast.LENGTH_SHORT).show()
        }
    }

fun requestPermissions() {
    requestCallLogPermissionLauncher.launch(
        arrayOf(
            Manifest.permission.READ_CALL_LOG,
            Manifest.permission.WRITE_CALL_LOG
        )
    )
}

重要注意事项

1. 数据读取方式

重要 : 所有整数类型字段都使用 getLong() 读取,避免大数值(如时间戳)被截断。

kotlin 复制代码
// ✅ 正确:使用 getLong()
cursor.getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER -> {
    cursor.getLong(columnIndex).toString()
}

// ❌ 错误:使用 getInt() 会导致时间戳溢出
cursor.getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER -> {
    cursor.getInt(columnIndex).toString()  // 时间戳可能变成负值
}

2. 时间戳格式

  • date 字段存储的是毫秒级 Unix 时间戳
  • 例如:1714567890000 对应 2024-05-01 12:31:30

3. 通话类型常量

kotlin 复制代码
CallLog.Calls.INCOMING_TYPE = 1        // 来电
CallLog.Calls.OUTGOING_TYPE = 2        // 去电
CallLog.Calls.MISSED_TYPE = 3          // 未接
CallLog.Calls.VOICEMAIL_TYPE = 4       // 语音信箱
CallLog.Calls.REJECTED_TYPE = 5        // 拒接
CallLog.Calls.BLOCKED_TYPE = 6          // 被拦截

4. 导入限制

  • Android 系统可能限制第三方应用写入通话记录
  • 某些 ROM(如 MIUI、EMUI)可能有额外的限制
  • 导入功能不是 100% 可靠的,建议仅用于备份/恢复场景

5. 隐私保护

  • 通话记录包含敏感的个人隐私信息
  • 导出时应存储在应用私有目录
  • 分享数据前应获得用户明确同意
  • 遵守相关法律法规(如《个人信息保护法》)

6. 性能考虑

  • 对于大量通话记录,建议使用 limit 参数分页查询
  • 导出大量数据时建议在后台线程执行
  • 可以使用 Kotlin Coroutines 或 RxJava 进行异步操作
kotlin 复制代码
// 使用协程在后台导出
lifecycleScope.launch(Dispatchers.IO) {
    callLogUtils.exportCallLogsToJson(outputFile)
        .onSuccess { count ->
            withContext(Dispatchers.Main) {
                Toast.makeText(this@MainActivity, "导出成功", Toast.LENGTH_SHORT).show()
            }
        }
}

动态字段说明

CallLogUtils 使用动态字段发现机制,会自动获取通话记录表中的所有字段。不同设备和 Android 版本可能包含不同的字段。

常见字段

字段名 类型 说明
_id Long 记录 ID
number String 电话号码
normalized_number String 规范化号码
formatted_number String 格式化号码
date Long 通话时间(毫秒)
duration Int 通话时长(秒)
type Int 通话类型
new Int 是否为新记录(1=新,0=已读)
name String 联系人名称
cached_name String 缓存的联系人名称
cached_number_label String 号码标签
cached_number_type Int 号码类型
countryiso String 国家代码
geocoded_location String 地理编码位置
subscription_id Int SIM 卡 ID
simid Int SIM 卡 ID(旧版)
phone_account_address String 电话账户地址
presentation Int 号码显示方式
data_usage String 数据使用量

获取任意字段

kotlin 复制代码
val callLog = callLogUtils.getAllCallLogs(limit = 1).first()

// 获取任意字段的值
val countryIso = callLog.getField("countryiso")
val geoLocation = callLog.getField("geocoded_location")

// 获取所有字段名
val allFields = callLog.getAllFieldNames()
println("可用字段: ${allFields.joinToString()}")

错误处理

所有可能失败的操作都返回 Result<T> 类型,便于进行错误处理。

示例

kotlin 复制代码
callLogUtils.exportCallLogsToJson(outputFile)
    .onSuccess { count ->
        // 成功处理
        println("成功导出 $count 条记录")
    }
    .onFailure { error ->
        // 错误处理
        when (error) {
            is SecurityException -> {
                println("权限不足: ${error.message}")
            }
            is IOException -> {
                println("文件操作失败: ${error.message}")
            }
            else -> {
                println("未知错误: ${error.message}")
            }
        }
    }

版本历史

v1.0.0

  • ✅ 初始版本
  • ✅ 支持读取通话记录
  • ✅ 支持导出为 JSON/CSV
  • ✅ 支持导入通话记录
  • ✅ 支持删除通话记录
  • ✅ 支持统计功能
  • ✅ 修复整数溢出问题(改用 getLong())

许可证

本项目遵循项目的许可证协议。

相关推荐
MonkeyKing2 小时前
蓝蓝牙核心基础概念详解:2.4GHz频段、跳频、信道、广播、连接、配对
android·ios
我命由我123452 小时前
Android 广播 - 显式广播与隐式广播
android·java·开发语言·java-ee·kotlin·android studio·android-studio
YaBingSec2 小时前
玄机网络安全靶场:JBoss 5.x_6.x 反序列化漏洞(CVE-2017-12149)
android·网络·笔记·安全·web安全·ssh
小妖6663 小时前
android studio安装中文语言插件
android·ide·android studio
Kapaseker3 小时前
为什么你写的 Compose 性能不好?
android·kotlin
帅次3 小时前
Android 高级工程师面试速记版
android·java·面试·kotlin·binder·zygote·android runtime
AI玫瑰助手3 小时前
Python基础:字典的键值对结构与增删改查操作
android·开发语言·python