概述
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())
许可证
本项目遵循项目的许可证协议。