Android 上来电怎么判:7 个条件加 5 个中文词

Android 上来电怎么判:7 个条件加 5 个中文词

Andee 是一个跑在 Android 上、靠无障碍服务读屏和点按的 agent,他和你共用同一台手机、同一个麦克风。在这种前提下做一个常开的语音唤醒入口,第一个要解决的问题不是识别率,而是什么时候必须闭嘴 。结论先给:判断"是否在通话"不要用 AudioManager.getMode(),改用通知;通话状态用 Set<String> 而不是 Boolean;唤醒词能不能拿麦克风,交给一个七项合取的门。

这几天行业里在谈同一件事的另一面。11 月 5 日 Baseten 发布 Sandboxes,给每个 agent 一台独立 microVM,断开就快照、25 毫秒恢复;10 月 22 日 Google 的 Agent Compute Engine、10 月 23 日 AWS 的 Infinity 都在解"agent 空闲时怎么把机器收回去"。云端 agent 的架构核心问题是怎么在没人用的时候省钱 。本机 agent 恰好相反:机器一直有人用,核心问题是怎么在别人要用的时候让开。这条路上公开的写法很少,这篇把我踩过的顺序、状态形状和四个覆盖不到的洞摊开。

一、getMode() 为什么不行

第一版就是轮询 AudioManager.getMode(),真机上三个坑:

  • MODE_IN_CALL 在部分 OEM 上需要电话权限,不给就恒为 MODE_NORMAL。
  • MODE_IN_COMMUNICATION 的置位时机是本 App 已经打开自己的录音之后 。等它变成真,唤醒词那条 AudioRecord 早就把麦克风占住了------它描述的是既成事实,不是预警。
  • 响铃窗口完全不在它的语义里。电话在响、还没接通,系统模式仍是普通模式,但麦克风已经被来电 App 拿走了。

所以判据必须换成事件源。通知是系统在事件发生当下发出的,比任何轮询都早。

二、两层判定,五个中文词兜底

kotlin 复制代码
private fun isCallNotification(sbn: StatusBarNotification): Boolean {
    if (sbn.notification.category == Notification.CATEGORY_CALL) return true
    if (sbn.packageName in CN_VOICE_APPS) {
        val title = sbn.notification.extras
            ?.getCharSequence(Notification.EXTRA_TITLE)?.toString().orEmpty()
        val text = sbn.notification.extras
            ?.getCharSequence(Notification.EXTRA_TEXT)?.toString().orEmpty()
        for (pat in CN_CALL_KEYWORDS) if (pat in title || pat in text) return true
    }
    return false
}

第一层是平台契约,AOSP 电话、WhatsApp、Telegram 都老实标 CATEGORY_CALL,命中就不用看别的。

第二层是为微信和 QQ 准备的。这两个包不设 CATEGORY_CALL ------2026-09-22 在真机上抓到的那条语音通话通知,cat=null。它们改用文字区分:语音通话中 / 视频通话中 / 正在语音通话 / 正在视频通话 / 来电。这五个词在这两个 App 的普通聊天通知里不会出现(聊天通知是"张三: 在吗"或"13个联系人发来66条消息"这种),所以文案匹配在这里是安全的。

这五条串禁止走 AppLocale。 它们是对方 App 写在自己通知里的字,跟随对方的语言设置,不跟随我们的。把它们翻译过来,用户看到的字一个都没变,只会让检测在语言真的会切换的设备上静默失效------漏掉一通电话,没有任何日志。

三、状态形状:Set 而不是 Boolean

两个边缘会让布尔值出错:

边缘 现象 布尔值的错法
并发通话 WhatsApp 未接来电横幅弹出时,电话仍在响 第一个 onNotificationRemoved 就清零,麦克风在一通进行中的电话里被打开
通知更新 同一通电话从"正在呼叫"变成"通话 00:12",onNotificationPosted 再触发一次 按次数计数永远清不干净

解法和边界都被同一个选择吃掉:用 StatusBarNotification.key(平台给的唯一标识)做元素,放进并发集合。post 两次看起来像一次,remove 只摘掉那一个。

kotlin 复制代码
private val activeKeys = ConcurrentHashMap.newKeySet<String>()
val isActive: Boolean get() = activeKeys.isNotEmpty()

59 行,没有更多。但有一处必须写:onListenerDisconnected() 里要 reset()。原因是------监听器被解绑之后,再没有人会来报告"那通电话的通知没了",那个 key 会永久留在集合里,唤醒词从此拿不到麦克风。设置页开关看着是开的,人叫不醒它,也没有任何异常。

四、七项合取的门

真正限制唤醒词的是一个布尔表达式,不是一段流程:

kotlin 复制代码
gate = {
    !exitRequested && wakeListeningAllowed() && !asr.isActive() && !tts.isSpeaking() &&
            !meeting.isActive() && !CallState.isActive && !Earcon.isSounding()
}

七项里两项值得单独说:

  • !tts.isSpeaking():否则他会听见自己念出接近唤醒词的音,被自己叫醒。
  • !Earcon.isSounding():那两声"叮"是他自己合成的,但扬声器在出声、音频资源在被占,和"别人拿着麦克风"是同一类占用。

wakeListeningAllowed() 是 screenOn || wakeWhileLocked,后者来自设置里那行「锁屏时也听」。这条曾经只有"屏幕亮着",后果是口袋里那台手机永远叫不醒------注释里原话是"This term used to be the screen alone, and that is the whole reason a phone in a pocket answered nothing."

监管线程 POLL_MS = 100L,任何一项翻假,下一轮就 mic.stop()。麦克风被别的东西占住打不开时退 2_000L 再试,一次唤醒后 2_500L 内不响应第二次,防止一句拖长的话启动两个回合。没录过唤醒词时不持有麦克风------未配置的安装不该让状态栏绿点亮着。

消费这个状态的地方有三处,缺一处就会出现"电话里他突然开口":

  1. startTurn():命中 CallState.isActive 就只弹一句字幕后 return。
  2. speakCardText():meeting.isActive() || CallState.isActive 直接不出声。
  3. followUpWindow():通话中不打开"等下一句"的窗口。

还有一条不对称,写在这里因为它是设计决定:收听回合失去音频焦点时,只有来电终止回合 ;其他抢占者(视频自动播放、消息音)最多要回两次焦点(MAX_REGRABS = 2),要不到就继续听。半句话比丢掉焦点贵。

五、代价

四件事按严重程度排:

  1. 只覆盖通知层。 App 内的 VoIP(游戏语音、某些会议软件的临时通话)既没有 CATEGORY_CALL,也不在那两个包名里,检测不到,那段麦克风照样会抢。
  2. 五个中文词是硬编码的。 微信切换成英文界面,这层兜底失效。代码注释说明这是有意的:它只为微信和 QQ 存在,其他 App 要么走平台契约,要么本来就无法和聊天消息区分。
  3. 100 毫秒的粒度。 刚响就挂的电话可能落在两次轮询之间,留下半声噪声,或者干脆没被记上。
  4. 没有内核隔离。 云端那台 microVM 有独立内核;他和宿主共用同一个内核、同一个电量、同一个用户身份。这是"更近",不是"更安全"。

如果只看一句话:云端的 agent 在优化"不被使用时的成本",本机的 agent 在优化"被使用时的礼貌"。两套代码几乎不可能互相复用。

相关推荐
MardaWang16 分钟前
App 内嵌 H5 的信任边界设计(ArkWeb/HarmonyOS 实战)
android
石头慢慢说17 分钟前
不写 XPath,让 AI 在真机上跑测试:Appium MCP 全流程实战
android
90后的晨仔3 小时前
解决 Android 构建报错:Unable to delete file .../file-map.txt
android·前端
mg6683 小时前
安卓玩机工具----安卓手机变成桌面电脑!多窗口镜像+音频投射+游戏操控,无需 Root
android·智能手机·电脑
peakmain94 小时前
异常处理 - Native 层的崩溃捕获机制及实现 (一)
android·性能优化
木头左5 小时前
【Android】听写播放器改版:中英文书写速度拆项与「正在读」高亮
android·room·media3·jetpack compose·听写
好的佩奇6 小时前
Dart 之方法、操作符与可调用对象
android·flutter·dart
恋猫de小郭6 小时前
Flutter Impeller Vulkan 内存持续上涨,问题没想到是文字阴影
android·前端·flutter