You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android ContentObserver监听通话结束后偶发无法匹配通话记录(5-10%失败率)的优化咨询

Android ContentObserver监听通话结束后偶发无法匹配通话记录(5-10%失败率)的优化咨询

问题根源先给你捋清楚

先直接回应你提到的几个疑问:

  1. 确实存在竞态条件:当通话进入IDLE状态时,系统并不是同步立即把通话日志写入CallLog数据库的——这个写入是异步操作,会有几十到几千毫秒的时间差。你的ContentObserver触发时,可能只是系统开始写入的信号,但实际记录还没完全落地,这时候自然查不到匹配项。
  2. 必须加带延迟的重试机制:当前查一次就放弃的逻辑是失败的核心原因,得给系统留足够的写入时间,多尝试几次。
  3. 替代方案推荐:除了优化现有Observer的重试逻辑,还可以用WorkManager实现延迟查询(更适合后台场景),或者Android 10+考虑TelecomManager的回调,但最直接有效的还是优化现有Observer的重试逻辑。
  4. 时间窗口与匹配策略:5分钟的窗口太大,容易匹配到无关记录,建议缩小到通话开始前后1-2分钟,同时结合通话时长范围匹配,既精准又能避免误判。

具体优化方案(附代码修改)

1. 给ContentObserver加递增延迟的重试逻辑

修改你的CallLogObserver,不要在第一次查询失败后就注销Observer,而是增加重试次数限制,每次重试的延迟时间递增,覆盖系统写入的常见延迟:

inner class CallLogObserver(
    handler: Handler,
    private val context: Context,
    private val phoneNumber: String?,
    private val callStartTime: Long,
    private val callType: Int,
    private val callEndTime: Long, // 新增:传入通话结束时间,用于时长匹配
    private val maxRetries: Int = 5, // 最多重试5次
    private var currentRetry: Int = 0
) : ContentObserver(handler) {

    // 每次重试延迟递增,总超时约3100ms,覆盖大部分系统写入延迟
    private val queryDelayMs = listOf(100L, 200L, 400L, 800L, 1600L)

    override fun onChange(selfChange: Boolean) {
        super.onChange(selfChange)
        // 避免重复触发(因为注册了notifyForDescendants=true)
        if (currentRetry > maxRetries) {
            context.contentResolver.unregisterContentObserver(this)
            Log.e("CallLog", "重试次数耗尽,未找到匹配通话记录")
            return
        }
        queryCallLog()
    }

    private fun queryCallLog() {
        if (!context.isPermissionGranted(Manifest.permission.READ_CALL_LOG)) {
            context.contentResolver.unregisterContentObserver(this)
            return
        }

        val normalizedTargetNumber = normalizePhoneNumber(phoneNumber)
        // 缩小时间窗口到通话开始前后1分钟,更精准
        val minTime = callStartTime - 60 * 1000
        val maxTime = callStartTime + 60 * 1000
        // 计算预期通话时长(秒),允许±5秒误差,避免系统写入的时长和我们计算的有细微差异
        val expectedDuration = (callEndTime - callStartTime) / 1000
        val minDuration = Math.max(0, expectedDuration - 5)
        val maxDuration = expectedDuration + 5

        // 优化查询条件:同时匹配号码、类型、时间范围、时长范围,减少误匹配
        val selection = """
            ${CallLog.Calls.DATE} BETWEEN ? AND ? 
            AND ${CallLog.Calls.TYPE}=? 
            AND ${CallLog.Calls.DURATION} BETWEEN ? AND ?
        """.trimIndent()
        val selectionArgs = arrayOf(
            minTime.toString(),
            maxTime.toString(),
            callType.toString(),
            minDuration.toString(),
            maxDuration.toString()
        )
        val sortOrder = "${CallLog.Calls.DATE} DESC"

        val cursor = context.contentResolver.query(
            CallLog.Calls.CONTENT_URI,
            arrayOf(CallLog.Calls.DURATION, CallLog.Calls.NUMBER, CallLog.Calls.DATE),
            selection,
            selectionArgs,
            sortOrder
        )

        cursor?.use {
            var found = false
            while (it.moveToNext()) {
                val logNumber = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
                val normalizedLogNumber = normalizePhoneNumber(logNumber)
                if (normalizedTargetNumber == normalizedLogNumber) {
                    val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
                    val callDate = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
                    Log.d("CallLog", "找到匹配通话:时长=$duration秒,时间=$callDate,重试次数=$currentRetry")
                    found = true
                    break
                }
            }
            if (found) {
                context.contentResolver.unregisterContentObserver(this)
            } else {
                currentRetry++
                if (currentRetry <= maxRetries) {
                    // 延迟执行下一次查询
                    handler.postDelayed({ queryCallLog() }, queryDelayMs[currentRetry - 1])
                    Log.d("CallLog", "未找到匹配,${queryDelayMs[currentRetry -1]}ms后重试(第$currentRetry/$maxRetries次)")
                } else {
                    context.contentResolver.unregisterContentObserver(this)
                    Log.e("CallLog", "no_matching_call_log - 所有重试已耗尽")
                }
            }
        } ?: run {
            // 查询出错,直接注销
            context.contentResolver.unregisterContentObserver(this)
            Log.e("CallLog", "查询通话日志失败,cursor为null")
        }
    }
}

然后修改调用处,传入callEndTime:

// ReadCallStatusReceiver中EXTRA_STATE_IDLE分支
TelephonyManager.EXTRA_STATE_IDLE -> {
    callEndTime = System.currentTimeMillis()
    registerCallLogObserver(context, callNumber, callStartTime, callEndTime, CallLog.Calls.OUTGOING_TYPE)
}

// 对应修改registerCallLogObserver方法
private fun registerCallLogObserver(context: Context, phoneNumber: String?, callStartTime: Long, callEndTime: Long, callType: Int) {
    if (context.isPermissionGranted(Manifest.permission.READ_CALL_LOG)) {
        val handler = Handler(Looper.getMainLooper())
        val observer = CallLogObserver(handler, context, phoneNumber, callStartTime, callType, callEndTime)
        context.contentResolver.registerContentObserver(CallLog.Calls.CONTENT_URI, true, observer)
    }
}

2. 优化号码匹配逻辑

确保normalizePhoneNumber能处理各种格式的号码,避免因为格式不一致导致匹配失败:

private fun normalizePhoneNumber(phoneNumber: String?): String? {
    // 只保留数字,去除空格、括号、+号等特殊字符
    return phoneNumber?.replace(Regex("[^0-9]"), "")
    // 如果需要处理国家码(比如统一去掉+86前缀),可以调整:
    // return phoneNumber?.replace(Regex("[^0-9]"), "")?.let {
    //     if (it.startsWith("86")) it.substring(2) else it
    // }
}

3. 后台场景替代方案:用WorkManager实现可靠查询

如果你的App经常在后台运行,担心ContentObserver被系统回收,可以改用WorkManager执行延迟查询,即使App被后台杀死也能继续执行:

// 在EXTRA_STATE_IDLE分支提交WorkRequest
private fun enqueueCallLogQueryWork(context: Context, phoneNumber: String?, callStartTime: Long, callEndTime: Long, callType: Int) {
    if (context.isPermissionGranted(Manifest.permission.READ_CALL_LOG)) {
        val data = Data.Builder()
            .putString("phoneNumber", phoneNumber)
            .putLong("callStartTime", callStartTime)
            .putLong("callEndTime", callEndTime)
            .putInt("callType", callType)
            .build()

        val workRequest = OneTimeWorkRequestBuilder<CallLogQueryWorker>()
            .setInitialDelay(1, TimeUnit.SECONDS) // 先延迟1秒再查询
            .setBackoffCriteria(
                BackoffPolicy.EXPONENTIAL,
                OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
                TimeUnit.MILLISECONDS
            )
            .setInputData(data)
            .build()

        WorkManager.getInstance(context).enqueue(workRequest)
    }
}

// 实现Worker类
class CallLogQueryWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val phoneNumber = inputData.getString("phoneNumber")
        val callStartTime = inputData.getLong("callStartTime", 0)
        val callEndTime = inputData.getLong("callEndTime", 0)
        val callType = inputData.getInt("callType", 0)

        // 最多重试5次
        repeat(5) { retry ->
            if (queryCallLog(applicationContext, phoneNumber, callStartTime, callEndTime, callType)) {
                return Result.success()
            }
            delay(200L * (retry +1)) // 每次重试延迟递增
        }
        return Result.failure()
    }

    private fun queryCallLog(context: Context, phoneNumber: String?, callStartTime: Long, callEndTime: Long, callType: Int): Boolean {
        val normalizedTargetNumber = normalizePhoneNumber(phoneNumber)
        val minTime = callStartTime - 60 * 1000
        val maxTime = callStartTime + 60 * 1000
        val expectedDuration = (callEndTime - callStartTime) / 1000
        val minDuration = Math.max(0, expectedDuration -5)
        val maxDuration = expectedDuration +5

        val selection = """
            ${CallLog.Calls.DATE} BETWEEN ? AND ? 
            AND ${CallLog.Calls.TYPE}=? 
            AND ${CallLog.Calls.DURATION} BETWEEN ? AND ?
        """.trimIndent()
        val selectionArgs = arrayOf(
            minTime.toString(), maxTime.toString(), callType.toString(), minDuration.toString(), maxDuration.toString()
        )

        context.contentResolver.query(CallLog.Calls.CONTENT_URI, arrayOf(CallLog.Calls.NUMBER), selection, selectionArgs, "${CallLog.Calls.DATE} DESC")?.use {
            while (it.moveToNext()) {
                val logNumber = it.getString(0)
                if (normalizePhoneNumber(logNumber) == normalizedTargetNumber) {
                    return true
                }
            }
        }
        return false
    }

    private fun normalizePhoneNumber(phoneNumber: String?): String? {
        return phoneNumber?.replace(Regex("[^0-9]"), "")
    }
}

总结

  • 最直接解决当前问题的是给ContentObserver加递增延迟的重试机制,同时优化查询条件缩小匹配范围,能把成功率提升到接近100%。
  • 如果需要更高的后台可靠性,结合WorkManager执行查询任务,避免Observer被系统回收。
  • 号码匹配和时间范围的优化能减少误匹配概率,进一步提升整体稳定性。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 10:53:03