
之前开车途中手机开了静音连续错过了好几个重要电话。等红灯时掏出来一看屏幕上有几条未接来电提醒那种心情真的很无奈。后来就想能不能做一个小工具手机来电时让电脑端自动播放一声提醒语音比如“来接电话”甚至条件允许时帮我自动接通电话。这个想法听起来有点小极客但实现起来并不复杂核心就是 Android 手机的 ADB 调试接口、Python 状态监听再加上文字转语音播报。本文就来完整拆解一套“来电语音提醒 辅助接听”的本地脚本方案。适合对 Python 有点基础、手头正好有一台 Android 手机的朋友。学完之后你就能给自己的手机做一个可定制的来电播报工具顺便能理解 ADB 是怎么与手机交互的。重要安全声明本文所有方案都只适用于你自己拥有并授权的测试设备不能用来监听他人电话也不能用于任何骚扰、侵犯隐私的用途。请在合法合规的前提下阅读和实验。1. 来电播报与辅助接听到底解决什么问题先解释一下这套功能是什么。传统手机上有个“来电提醒”业务一般是运营商提供的手机关机或不在服务区时系统帮你记录未接来电等开机后短信通知你。它本质上是“漏话提醒”而不是“实时提醒”。我们的方案不一样。它做的是“设备在身旁、但人没有注意到手机”的场景。比如手机开了静音或震动太弱人戴着耳机没听到手机放在客厅充电人在厨房做饭手机连了车载蓝牙但音量异常低手头正在敲代码手机倒扣在桌面上。这时候如果电脑端一直连着手机脚本检测到来电状态立刻用电脑音箱播放“来接电话”就能有效降低漏接概率。专业一点的叫法这属于“来电状态感知 本地通知播报”。实际工程里类似思路常用于手机自动化测试中的通话状态断言骚扰电话识别辅助工具智能家居场景中的来电联动提醒无障碍辅助设备。我们设计的核心链路如下手机来电 → USB/ADB 状态变化 → Python 轮询读取电话状态 → 状态为 RINGING → 播放 TTS 语音提醒 → 可选发送自动接听指令后面所有代码都是围绕这条链路展开的。把链路拆开每一部分都不复杂。2. 环境准备与版本说明本文示例使用如下环境但这不是唯一组合关键思路是通用的项目建议环境操作系统Windows 10/11、macOS、Linux 均可Python 版本3.9 及以上手机系统Android 8.0 及以上部分旧机型命令输出格式有差异电脑与手机连接USB 数据线开启 ADB 调试文字转语音edge-tts 或 pyttsx3二选一ADB 工具Android Platform Tools 中的 adb如果你的电脑还没安装 ADB需要先到 Android 官方开发者网站下载对应平台的 Platform Tools解压后把 adb 所在目录加入系统环境变量或者直接在脚本里写绝对路径。版本怎么选本文重点是演示思路依赖也不需要锁定到某个具体小版本。如果你的 Python 环境比较新建议在虚拟环境中安装依赖避免和系统其他项目冲突。手机端需要开启“开发者选项”里的“USB 调试”。不同品牌开启方式不完全一样一般是设置 → 关于手机 → 连续点击版本号 7 次开启后进入设置 → 开发者选项 → USB 调试然后使用数据线连接电脑在手机上允许“允许 USB 调试”的授权弹窗。建议顺便开启“仅充电模式下允许 ADB 调试”如果有这个选项这样日常连接不需要切换模式。项目目录结构建议如下call_helper/ ├── main.py ├── core/ │ ├── __init__.py │ ├── adb_checker.py │ └── speaker.py ├── requirements.txt └── README.md这样一个简单的包结构既方便以后扩展也不至于太复杂。3. 核心原理拆解这部分是文章的重点理解原理后代码只是表达方式。3.1 电话状态从哪来Android 系统底层维护着电话状态ADB 可以通过一个命令把状态托盘打印出来adb shell dumpsys telephony.registry输出里会包含大量电话模块信息其中最关键的是这一行mCallState1mCallState取值含义如下值含义0空闲状态没有来电也没有通话1振铃状态有来电正在响铃2通话中已接听或拨出后接通因此脚本的核心思路就是周期性执行这条命令在输出文本中匹配mCallState1。当检测到状态从 0 变成 1说明有新的来电等状态变成 0说明电话已经结束或未接挂断。这里有个小坑dumpsys telephony.registry的输出格式在不同 Android 版本、不同厂商 ROM 下会有差异。部分厂商定制系统可能不输出mCallState字段或者输出位置不同。实际开发时需要先手动跑一次命令确认你的设备能输出什么内容再决定正则表达式怎么写。另一个可选的读取方式是adb shell dumpsys telecom不过它输出的结构更复杂字段也因版本而异。对入门脚本来说telephony.registry更直观。3.2 语音提醒怎么实现语音提醒使用两种方案之一第一种是pyttsx3。它是离线本地语音库调用电脑系统自带的 TTS 引擎不依赖网络但音色取决于操作系统中文语音效果有时一般。第二种是edge-tts。它基于微软 Edge 在线语音服务网络正常时中文音色更自然比如zh-CN-XiaoxiaoNeural。但因为是网络请求播报前需要生成音频文件然后再播放实时性略低一点。考虑到来电提醒需要快速响应我建议优先尝试edge-tts因为它音色听起来更像真人识别度更高。如果处于完全离线环境再退回pyttsx3。文字转语音之后播放音频可以使用pygame或playsound。pygame更稳定但安装包较大playsound简单不过偶尔有阻塞问题。本文示例用pygame因为它在 Windows、macOS、Linux 下基本都能正常播放 mp3 文件。3.3 自动接听怎么实现ADB 模拟按键可以模拟摘下蓝牙耳机接听电话的操作adb shell input keyevent KEYCODE_HEADSETHOOKKEYCODE_HEADSETHOOK对应的键值常用于耳机线控按键很多手机在振铃状态下收到这个按键会接通电话。部分系统也支持adb shell input keyevent KEYCODE_CALL但KEYCODE_CALL在某些机型上只是点亮通话界面不一定会真正接通兼容性不如KEYCODE_HEADSETHOOK。需要提醒的是自动接听非常依赖手机厂商的按键实现同一个命令在不同手机上结果可能不同。强烈建议先手动测试手机来电时电脑执行一次adb shell input keyevent KEYCODE_HEADSETHOOK看是否接通。如果不行就不要开启自动接听只用语音提醒功能手动按手机接通即可。3.4 状态机设计为了避免“同一个电话播报十几次”这种问题不能简单地在检测到mCallState1时播放语音。要用一个状态变量记录上次状态。状态转移如下上一个状态 空闲 当前检测到 振铃 → 执行播报动作 → 状态更新为 振铃 当前检测到 空闲 → 状态更新为 空闲只有当状态从“空闲”变成“振铃”时才播报。如果一直在振铃脚本不应重复播报。通话结束后状态回到空闲下一次来电又能正常触发。4. 实战编写完整的来电语音提醒程序下面开始写代码。为了减少依赖复杂性我们拆成几个模块每个模块负责单一职责。4.1 创建项目并安装依赖打开终端创建项目目录和虚拟环境mkdir call_helper cd call_helper python -m venv venvWindows 下激活虚拟环境venv\Scripts\activatemacOS/Linux 下激活source venv/bin/activate安装依赖pip install edge-tts pygame如果走离线方案额外安装pip install pyttsx3在项目根目录生成requirements.txtedge-tts6.1.0 pygame2.5.0 pyttsx32.904.2 编写 ADB 电话状态检测模块文件路径call_helper/core/adb_checker.pyimport re import subprocess class AdbCallChecker: 通过 adb 获取手机电话状态。 状态说明 0 空闲 1 振铃来电 2 通话中 def __init__(self, adb_pathadb): self.adb_path adb_path def get_call_state(self): 执行 adb 命令解析 mCallState 值。 返回: int: 0 表示空闲1 表示振铃2 表示通话中。 如果解析失败返回 -1。 cmd [self.adb_path, shell, dumpsys, telephony.registry] try: result subprocess.run( cmd, capture_outputTrue, textTrue, timeout10, checkFalse, ) except FileNotFoundError: print([错误] 找不到 adb请检查 adb 是否已加入 PATH。) return -1 except subprocess.TimeoutExpired: print([错误] adb 命令执行超时请检查手机连接。) return -1 output result.stdout match re.search(rmCallState\s*\s*(\d), output) if match: return int(match.group(1)) return -1解释几个关键点。subprocess.run的capture_outputTrue用来捕获命令输出textTrue让输出以字符串形式返回而不是字节形式。timeout10防止 adb 长时间卡住导致脚本假死。re.search使用正则匹配mCallState后面的数字。由于不同版本输出里可能带有空格或不带空格所以使用\s*\s*这种写法兼容性更好。如果返回-1说明没有从输出中解析到有效状态。这种情况常见于部分厂商深度定制系统省略了该字段或者当前设备没有 SIM 卡信号。4.3 编写语音播报模块文件路径call_helper/core/speaker.pyimport asyncio import os import tempfile import edge_tts from pygame import mixer class VoiceSpeaker: 文字转语音播报工具。 def __init__(self, voicezh-CN-XiaoxiaoNeural, rate0%): self.voice voice self.rate rate self._initialized False def _init_mixer(self): if not self._initialized: mixer.init() self._initialized True async def _generate_audio(self, text, output_path): communicate edge_tts.Communicate(text, self.voice, rateself.rate) await communicate.save(output_path) def play(self, text): 生成语音并播放text 为要播报的文本。 try: self._init_mixer() tmp_dir tempfile.gettempdir() tmp_file os.path.join(tmp_dir, call_helper_voice.mp3) asyncio.run(self._generate_audio(text, tmp_file)) mixer.music.load(tmp_file) mixer.music.play() except Exception as exc: print(f[警告] 语音播报失败{exc}) def cleanup(self): 释放播放器资源。 if self._initialized: mixer.quit() self._initialized False这段代码用edge-tts生成 MP3 临时文件再用pygame.mixer播放。asyncio.run用来执行异步的save方法因为我们是在普通同步函数中调用。需要注意edge-tts需要网络通畅。如果你在离线环境可以把play方法中的逻辑替换为pyttsx3import pyttsx3 class OfflineVoiceSpeaker: 基于 pyttsx3 的离线播报示例。 def __init__(self): self.engine pyttsx3.init() def play(self, text): self.engine.say(text) self.engine.runAndWait()离线版本的音色和语速可以通过self.engine.setProperty(voice, ...)调整不同操作系统可选的语音名称不同这块需要按本机情况配置。4.4 编写主程序文件路径call_helper/main.pyimport time from core.adb_checker import AdbCallChecker from core.speaker import VoiceSpeaker STATE_IDLE 0 STATE_RINGING 1 STATE_CALLING 2 POLL_INTERVAL 2 def main(): checker AdbCallChecker() speaker VoiceSpeaker() last_state STATE_IDLE last_ring_alert_time 0 print(来电语音提醒已启动按 CtrlC 停止。) try: while True: current_state checker.get_call_state() if current_state -1: print([提示] 无法获取电话状态等待 5 秒后重试...) time.sleep(5) continue if current_state STATE_RINGING and last_state ! STATE_RINGING: now time.time() if now - last_ring_alert_time 10: print([来电] 检测到振铃状态播放提醒语音。) speaker.play(来接电话有电话打进来了。) last_ring_alert_time now if current_state ! last_state: print(f[状态] 电话状态变化{last_state} - {current_state}) last_state current_state time.sleep(POLL_INTERVAL) except KeyboardInterrupt: print(\n[退出] 用户手动停止。) finally: speaker.cleanup() if __name__ __main__: main()主程序是一个轮询循环每 2 秒读取一次电话状态。核心逻辑是状态从空闲变为振铃时播放语音连续振铃但已经播报过不会重复播放设置 10 秒冷却时间防止状态抖动导致多次播报用户按CtrlC可以安全退出并释放音频资源。这里的播报文本是“来接电话有电话打进来了”你可以随意改成“你的手机来电话了”“快看手机”等等。4.5 可选自动接听模块先不要直接放进主程序。建议单独写一个测试脚本确认你的手机支持后再集成。文件路径call_helper/test_auto_answer.pyimport subprocess import time def auto_answer(adb_pathadb): 模拟线控按键尝试自动接听。 cmd [adb_path, shell, input, keyevent, KEYCODE_HEADSETHOOK] print([自动接听] 发送 KEYCODE_HEADSETHOOK 按键。) subprocess.run(cmd, checkFalse) if __name__ __main__: print(请现在使用另一台手机拨打测试号码等待 3 秒后自动接听。) time.sleep(3) auto_answer()如果测试发现这个按键可以接通电话想集成到主程序里可以在main.py触发播报之后追加subprocess.run( [checker.adb_path, shell, input, keyevent, KEYCODE_HEADSETHOOK], checkFalse, timeout5, )但正如前面所说不同机型差异较大。我建议先不开自动接听只保留提醒功能等实际场景确实需要时再逐步测试。4.6 补充说明纯离线版本主程序如果你的电脑无法访问网络无法使用edge-tts可以参考下面的离线播报版本主程序核心循环import time from core.adb_checker import AdbCallChecker try: import pyttsx3 except ImportError: print(请先安装 pyttsx3pip install pyttsx3) raise engine pyttsx3.init() checker AdbCallChecker() last_state 0 while True: current_state checker.get_call_state() if current_state 1 and last_state ! 1: print([来电] 播放离线语音提醒。) engine.say(来接电话。) engine.runAndWait() if current_state ! last_state: print(f[状态] {last_state} - {current_state}) last_state current_state time.sleep(2)离线版好处是没有任何网络依赖坏处是engine.runAndWait()是同步阻塞的语音播报期间不会去检测电话状态。如果播报句子很长可能影响后续状态刷新。解决办法是让播报线程异步执行但作为入门例子这里已经够用了。5. 运行与验证先确认手机连接正常。在项目根目录执行adb devices正常情况下输出类似List of devices attached 0123456789ABCDEF device如果显示unauthorized需要在手机屏幕上点击“允许 USB 调试”授权。如果显示offline尝试重新插拔数据线或者重启手机。确认设备在线后先手动执行一次状态解析测试python -c from core.adb_checker import AdbCallChecker; checker AdbCallChecker(); print(checker.get_call_state())当前没有电话的状态下应输出0。然后启动主程序python main.py用另一台手机拨打测试号码观察输出来电语音提醒已启动按 CtrlC 停止。 [状态] 电话状态变化0 - 1 [来电] 检测到振铃状态播放提醒语音。这时电脑音箱应该会播放“来接电话有电话打进来了”。挂断电话后输出恢复[状态] 电话状态变化1 - 0如果一切正常说明整个链路已经打通。接下来你可以调整播报文本、语速、轮询间隔让它更符合你的习惯。6. 常见问题与排查问题现象常见原因解决思路adb找不到设备未开启 USB 调试 / 驱动问题 / 连接线不支持数据传输更换数据线重新授权执行adb kill-server后重试授权后仍显示unauthorized手机弹窗未确认或未勾选“始终允许”拔掉重插手机端再次确认授权输出一直为 -1厂商 ROM 不输出mCallState字段手动执行dumpsys telephony.registry查看实际字段改用正则适配有来电但未播报状态未从空闲变为振铃脚本未运行TTS 网络不可用查看终端状态输出确认网络临时切换到离线 TTS语音播报非常卡顿edge-tts 需要先联网生成语音文件预生成常见播报短语来电时直接播放本地文件自动接听按键无效手机厂商不支持KEYCODE_HEADSETHOOK改用KEYCODE_CALL测试或放弃自动接听手动接听轮询占用 CPU 过高每 2 秒执行一次 adb 命令属于正常现象可把间隔调整为 3 秒仅在校验时频繁轮询手机熄屏后 adb 断开部分手机开启了 USB 休眠优化在开发者选项中关闭“USB 休眠时自动断开调试”之类的选项排查时最重要的一步是“先人工验证命令”不要让脚本替你猜问题。比如觉得来电没触发播报就先手动在手机上拨号同时在电脑终端执行adb shell dumpsys telephony.registry | findstr mCallState如果是 Linux/macOS管道命令改成adb shell dumpsys telephony.registry | grep mCallState通过这一步你就能判断到底是脚本逻辑问题还是手机根本没有输出状态字段。7. 工程建议与安全边界这类工具虽然小但涉及手机权限和语音播放实际使用时有几点值得注意。第一权限最小化。ADB 调试本身就是高权限通道脚本只读取电话状态和模拟按键不要扩展多余能力。如果只是想听来电提醒不要写任何读取短信、通讯录的代码也不要让脚本长期后台驻留系统。第二明确授权边界。只在你自己的手机上使用。把手机通过 ADB 连到不信任的电脑本身就有风险因为 adb 可以执行很多高危命令。不要在公共电脑或共享办公环境的电脑上运行本脚本避免手机数据暴露。第三自动接听要非常克制。自动接听一旦误触可能会导致电话在会议中突然接通引发尴尬甚至安全问题。比较好的做法是先只打开语音提醒确认稳定运行一周后再考虑是否加入自动接听开关。第四日志与可观测性。建议把每次状态变化、播报结果写入日志文件而不仅仅打印在终端。尤其是长期运行时终端可能被关闭或滚动刷屏日志可以帮助你后续排查漏电话问题。第五异常兜底。脚本里所有 adb 命令都应该有超时和异常处理。接口调用失败时不要静默吞掉至少打印一条能看懂的错误信息。轮询间隔不要设置太短否则手机 CPU 和电脑进程都会频繁被唤醒耗电又占用资源。第六不要绕过安全限制。本文只是对自有设备进行辅助提醒。任何“远程监听他人电话”“截取通话内容”“在他人不知情时自动接听”的需求都不属于技术讨论范围请在法律允许的范围内使用技术。8. 后续可以怎么扩展这套方案的扩展空间其实很大。把播报文本改为“外卖电话”“快递电话”等分类提示思路是配合号码前缀或通讯录匹配接入企业微信或钉钉机器人 webhook手机来电时把提示推送到电脑端 IM结合智能音箱收到来电时让音箱播报把脚本封装成一个带图形界面的小工具让不懂命令行的家人也能使用增加白名单/黑名单逻辑只对重要联系人播报普通骚扰电话直接静默。如果你想继续深入建议先研究几个方向Androiddumpsys各模块的输出格式adb shell input支持的全部 keyevent 键值语音合成服务的音色定制和 SSML 语法如何用uiautomator dump获取当前界面元素辅助判断通话界面。这套脚本最大的价值并不是“自动接电话”这个动作本身而是让你理解了 Android 调试通道、状态轮询、语音合成这几个基础能力如何组合成一个有实际意义的工具。把链路打通之后你会发现很多类似的小需求都能用同一套模式去解决。如果你手头正好有一台闲置 Android 手机不妨按文章步骤跑一遍。过程中遇到任何报错优先按第 6 节的表格逐项排查大多数问题都出在连接和数据线上。祝实验顺利。