ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Android来电监听与电话状态管理完整实践指南

Android来电监听与电话状态管理完整实践指南 来接电话——Android 来电监听与电话状态管理的完整实践平时做 Android 开发时一旦涉及电话相关的功能很多同学会习惯性认为“这不就是加个权限、注册个广播的事吗”。但真正动手后才发现来电监听、通话状态获取、自动接听、来电号码识别这些功能每一环都有不少坑权限版本兼容、API 废弃替换、厂商 ROM 限制、前台服务保活、隐私合规要求……网上的资料大多只讲一个点而且部分示例还停留在PhoneStateListener旧 API 上跑在新设备上直接失效。这篇文章就围绕“来接电话”这个场景完整梳理 Android 来电监听与通话管理的实现方案。内容会覆盖基础概念、权限配置、核心 API 使用、可运行的实战项目代码、常见报错排查以及上架前需要注意的合规问题。无论是初学者想了解电话功能开发还是开发者要做通话记录、来电秀、骚扰电话拦截、自动接听类 App都能从中找到可复用的方案。1. 来电监听是什么解决什么问题1.1 电话功能开发的核心需求在 Android 系统中“来电”并非简单的系统通知而是一系列状态变化的集合。电话从呼入到结束会经历以下几个阶段来电响铃RINGING接通OFFHOOK挂断IDLE开发者如果能拿到这些状态就可以做很多业务。最常见的应用场景包括来电号码展示与归属地识别骚扰电话拦截与标记通话录音注需要格外注意法律与隐私限制自动接听例如车载蓝牙场景通话记录统计与分析手机卫士类的“来电秀”功能从技术实现角度来说Android 提供了一套以TelephonyManager为核心的系统电话服务接口。通过注册监听器应用可以在电话状态发生变化时收到回调。1.2 两种主流实现方式实现来电监听主要有两条路线实现方式思路适用场景注册电话状态监听器通过 TelephonyManager 的 listen 方法监听通话状态获取状态变化、号码信息适合大多数普通应用注册广播接收器收听来电广播部分系统广播用于来电通知可用在早期版本或特定场景需要特别说明的是PhoneStateListener虽然在很多教程中仍然出现但它在 Android 12API 31之后已经被标记为废弃deprecated推荐使用TelephonyCallback替代。本文的实战部分会以新 API 为主同时也会给出兼容旧版本时的处理思路。1.3 自动接电话的边界标题里说“来接电话”很多开发者第一反应是“能不能做自动接听”。这里先泼一盆冷水普通第三方应用无法通过公开 API 直接自动接听来电。自动接听目前可行的方案主要有系统级应用需要系统签名权限普通开发者不具备条件。辅助功能AccessibilityService模拟点击接听按钮但受 Android 版本和厂商 ROM 限制稳定性一般。InCallService需要声明RoleManager.ROLE_CALL_SCREENING或相关角色只适用于通话界面替换类应用且使用门槛和审核要求较高。所以如果你的目标是做一个“全自动接听”的 App一定要提前评估系统限制和用户授权成本。本文会重点讲清楚“能做什么”和“做不到什么”避免你在错误的方向上浪费大量时间。2. 环境准备与版本说明2.1 开发环境本文示例使用以下环境读者可根据自己的项目实际情况调整操作系统Windows / macOS / Linux 均可IDEAndroid Studio建议使用较新稳定版本构建工具Gradle开发语言Kotlin最低支持的 Android 版本Android 6.0API 23编译和目标版本建议使用较新稳定版本例如 API 33 或 API 34由于 Android 版本碎片化严重不同厂商的 ROM 对电话权限的管理策略并不完全一致建议在真机上测试。模拟器对电话状态模拟能力有限部分场景无法完整验证。2.2 涉及的核心系统服务服务/类作用TelephonyManager获取电话服务、监听通话状态TelephonyCallback替代 PhoneStateListener 的状态回调接口PhoneStateListener旧版监听器API 31 起废弃READ_PHONE_STATE读取电话状态所需的运行时权限READ_CALL_LOG读取通话记录的敏感权限POST_NOTIFICATIONSAndroid 13 通知权限如果需要弹通知2.3 示例项目结构为了方便演示我们先规划一个最小项目结构CallPhoneDemo/ ├── app/ │ ├── src/ │ │ └── main/ │ │ ├── java/com/example/callphone/ │ │ │ ├── MainActivity.kt │ │ │ ├── PhoneStateReceiver.kt │ │ │ └── TelephonyListenerService.kt │ │ └── res/ │ │ └── layout/activity_main.xml │ └── build.gradle.kts └── build.gradle.kts后续实战章节会逐个文件给出完整代码。3. 电话状态监听的核心 API 与权限拆解3.1 READ_PHONE_STATE 权限监听来电状态首先需要申请READ_PHONE_STATE权限。这是一个危险权限dangerous permission需要在运行时动态申请。在 AndroidManifest.xml 中声明uses-permission android:nameandroid.permission.READ_PHONE_STATE / uses-permission android:nameandroid.permission.READ_CALL_LOG /需要注意READ_CALL_LOG用于读取通话记录在部分设备上申请时会和READ_PHONE_STATE一起弹出授权弹窗。如果业务上不需要读取历史通话记录建议不要申请READ_CALL_LOG因为它是敏感权限会增加隐私合规审核的成本。在 Android 6.0API 23之后危险权限必须运行时申请。示例代码private fun requestPhonePermission() { val permission if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { Manifest.permission.READ_PHONE_STATE } else { Manifest.permission.READ_PHONE_STATE } if (ContextCompat.checkSelfPermission(this, permission) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions( this, arrayOf(permission), REQUEST_PHONE_PERMISSION ) } }这里要注意READ_PHONE_STATE在 Android 12API 31之后仍然存在但部分与设备标识相关的使用场景被进一步限制。单纯用于电话状态监听不受太大影响。3.2 TelephonyManager 与获取实例TelephonyManager是访问电话服务的入口。获取方式val telephonyManager getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager在 Android 10API 29之后系统对非系统应用的电话服务访问做了更严格限制。但TelephonyManager的listen系列方法仍然被允许用于电话状态监听前提是具备READ_PHONE_STATE权限。3.3 PhoneStateListener 与 TelephonyCallback先看旧 API 的写法。网上大量资料使用的是这种写法// 旧写法不推荐在新项目中直接使用 val listener object : PhoneStateListener() { override fun onCallStateChanged(state: Int, phoneNumber: String?) { when (state) { TelephonyManager.CALL_STATE_IDLE - { /* 空闲 */ } TelephonyManager.CALL_STATE_RINGING - { /* 来电响铃 */ } TelephonyManager.CALL_STATE_OFFHOOK - { /* 通话中 */ } } } } telephonyManager.listen(listener, PhoneStateListener.LISTEN_CALL_STATE)这段代码在 Android 11 及以下版本可以工作但在 Android 12 及以上PhoneStateListener被标记为 deprecated虽然短期内仍能运行但未来版本可能移除。更重要的是部分厂商在新系统上已经不再回调旧监听器。推荐写法是使用TelephonyCallback// 新写法推荐 val executor ContextCompat.getMainExecutor(this) val callback object : TelephonyCallback(), TelephonyCallback.CallStateListener { override fun onCallStateChanged(state: Int) { when (state) { TelephonyManager.CALL_STATE_IDLE - { /* 空闲 */ } TelephonyManager.CALL_STATE_RINGING - { /* 来电响铃 */ } TelephonyManager.CALL_STATE_OFFHOOK - { /* 通话中 */ } } } } telephonyManager.registerTelephonyCallback(executor, callback)注意TelephonyCallback.CallStateListener的onCallStateChanged只返回状态不返回电话号码。如果想获取来电号码需要通过其他方式例如监听电话服务中的详情或者结合广播接收器。3.4 获取来电号码在新 API 中onCallStateChanged不再携带电话号码参数。如果需要获取来电号码可以通过TelephonyManager的getLine1Number()获取本机号码但这与来电号码无关。获取来电号码的思路有两种在CALL_STATE_RINGING状态时通过telephonyManager.getCallState()无法直接获取号码。使用TelephonyCallback中的CallStateListener与PreciseCallStateListener结合或者直接使用旧广播方案。这里需要提到一个比较隐蔽的细节Android 系统出于隐私保护从 Android 8.0API 26开始非系统应用在某些场景下获取到的来电号码可能为空。部分厂商 ROM 也会进一步限制号码读取。因此如果你的应用需要可靠地获取来电号码需要考虑系统限制或者引导用户开启“悬浮窗”等额外权限部分方案中可在通知栏显示归属地。3.5 一个容易被带偏的误区ACTION_PHONE_STATE_CHANGED 广播很多博客会推荐监听TelephonyManager.ACTION_PHONE_STATE_CHANGED广播receiver android:name.PhoneStateReceiver intent-filter action android:nameandroid.intent.action.PHONE_STATE / /intent-filter /receiver在旧版本中动态注册或静态注册该广播可以收到来电状态和号码。但从 Android 8.0API 26开始系统对隐式广播进行了限制清单注册静态注册的广播接收器无法再接收大部分隐式广播。PHONE_STATE广播虽然被列入例外名单的一部分但在实际测试中它在高版本系统上表现不稳定。结论广播方案可以作为辅助不建议作为主力方案。主力方案应该使用TelephonyManager.registerTelephonyCallback动态注册。4. 完整实战案例来电监听 自动接听辅助 来电记录这一节我们实现一个完整的 Demo功能包括实时监听电话状态变化在界面显示当前状态和来电号码通过辅助功能实现“自动接听”演示方案将来电状态变化写入日志文件4.1 创建项目并配置依赖先创建一个新的 Android 项目包名使用com.example.callphone。在app/build.gradle.kts中确认以下配置plugins { id(com.android.application) id(org.jetbrains.kotlin.android) } android { namespace com.example.callphone compileSdk 34 defaultConfig { applicationId com.example.callphone minSdk 23 targetSdk 34 versionCode 1 versionName 1.0 } buildFeatures { viewBinding true } kotlinOptions { jvmTarget 17 } } dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) }这里使用了viewBinding方便在 Activity 中免去findViewById。4.2 配置 AndroidManifest.xml打开app/src/main/AndroidManifest.xml添加权限和组件声明?xml version1.0 encodingutf-8? manifest xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:toolshttp://schemas.android.com/tools !-- 电话状态权限 -- uses-permission android:nameandroid.permission.READ_PHONE_STATE / !-- 辅助功能用于演示自动接听实际使用时不需要额外权限声明 -- !-- 用于写日志文件 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / application android:allowBackuptrue android:iconmipmap/ic_launcher android:labelstring/app_name android:themestyle/Theme.AppCompat.DayNight activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity !-- 辅助功能服务用于演示自动接听 -- service android:name.AutoAnswerService android:exportedtrue android:label自动接听辅助服务 android:permissionandroid.permission.BIND_ACCESSIBILITY_SERVICE intent-filter action android:nameandroid.accessibilityservice.AccessibilityService / /intent-filter meta-data android:nameandroid.accessibilityservice android:resourcexml/accessibility_service_config / /service /application /manifest4.3 编写 MainActivity 与界面布局先写一个简单的界面用来展示电话状态和授权入口。activity_main.xml?xml version1.0 encodingutf-8? LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical android:padding16dp TextView android:idid/tvStatus android:layout_widthmatch_parent android:layout_heightwrap_content android:text当前状态未知 android:textSize18sp / TextView android:idid/tvNumber android:layout_widthmatch_parent android:layout_heightwrap_content android:text来电号码未知 android:textSize18sp / Button android:idid/btnRequestPermission android:layout_widthmatch_parent android:layout_heightwrap_content android:text申请电话权限 / Button android:idid/btnStartMonitor android:layout_widthmatch_parent android:layout_heightwrap_content android:text开始监听电话状态 / Button android:idid/btnOpenAccessibility android:layout_widthmatch_parent android:layout_heightwrap_content android:text开启自动接听辅助 / TextView android:idid/tvAccessibilityStatus android:layout_widthmatch_parent android:layout_heightwrap_content android:text辅助功能未开启 android:textColor#666666 / /LinearLayoutMainActivity.ktpackage com.example.callphone import android.Manifest import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.provider.Settings import android.text.TextUtils import android.widget.Button import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat class MainActivity : AppCompatActivity() { companion object { private const val REQUEST_PHONE_PERMISSION 1001 } private lateinit var tvStatus: TextView private lateinit var tvNumber: TextView private lateinit var tvAccessibilityStatus: TextView private lateinit var phoneStateManager: PhoneStateManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tvStatus findViewById(R.id.tvStatus) tvNumber findViewById(R.id.tvNumber) tvAccessibilityStatus findViewById(R.id.tvAccessibilityStatus) phoneStateManager PhoneStateManager(this) phoneStateManager.setListener(object : PhoneStateManager.PhoneStateListener { override fun onStateChanged(state: String, number: String?) { runOnUiThread { tvStatus.text 当前状态$state tvNumber.text 来电号码${number ?: 未知} } } }) findViewByIdButton(R.id.btnRequestPermission).setOnClickListener { requestPhonePermission() } findViewByIdButton(R.id.btnStartMonitor).setOnClickListener { if (checkPhonePermission()) { phoneStateManager.startMonitor() Toast.makeText(this, 监听已启动, Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, 请先授予电话权限, Toast.LENGTH_SHORT).show() } } findViewByIdButton(R.id.btnOpenAccessibility).setOnClickListener { openAccessibilitySettings() } refreshAccessibilityStatus() } override fun onResume() { super.onResume() refreshAccessibilityStatus() } override fun onDestroy() { super.onDestroy() phoneStateManager.stopMonitor() phoneStateManager.clearListener() } private fun checkPhonePermission(): Boolean { return ContextCompat.checkSelfPermission( this, Manifest.permission.READ_PHONE_STATE ) PackageManager.PERMISSION_GRANTED } private fun requestPhonePermission() { if (checkPhonePermission()) { Toast.makeText(this, 已有权限, Toast.LENGTH_SHORT).show() return } ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.READ_PHONE_STATE), REQUEST_PHONE_PERMISSION ) } override fun onRequestPermissionsResult( requestCode: Int, permissions: Arrayout String, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode REQUEST_PHONE_PERMISSION) { if (grantResults.isNotEmpty() grantResults[0] PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, 权限已授予, Toast.LENGTH_SHORT).show() } else { Toast.makeText(this, 权限被拒绝, Toast.LENGTH_SHORT).show() } } } private fun openAccessibilitySettings() { val intent Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) startActivity(intent) } private fun refreshAccessibilityStatus() { val enabled isAccessibilityServiceEnabled(this, AutoAnswerService::class.java) tvAccessibilityStatus.text if (enabled) 自动接听辅助已开启 else 自动接听辅助未开启 } private fun isAccessibilityServiceEnabled(context: Context, serviceClass: Class*): Boolean { val expectedComponentName ComponentName(context.applicationContext, serviceClass) val enabledServices Settings.Secure.getString( context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES ) ?: return false return TextUtils.SimpleStringSplitter(:).apply { setString(enabledServices) }.any { ComponentName.unflattenFromString(it) expectedComponentName } } }4.4 编写 PhoneStateManager 核心监听类PhoneStateManager.ktpackage com.example.callphone import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import androidx.core.content.ContextCompat class PhoneStateManager(private val context: Context) { interface PhoneStateListener { fun onStateChanged(state: String, number: String?) } private var telephonyManager: TelephonyManager? null private var callback: TelephonyCallback? null private var listener: PhoneStateListener? null fun setListener(listener: PhoneStateListener?) { this.listener listener } fun clearListener() { this.listener null } fun startMonitor() { if (ContextCompat.checkSelfPermission( context, Manifest.permission.READ_PHONE_STATE ) ! PackageManager.PERMISSION_GRANTED ) { return } telephonyManager context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { callback object : TelephonyCallback(), TelephonyCallback.CallStateListener { override fun onCallStateChanged(state: Int) { val stateText formatState(state) val number getIncomingNumber() listener?.onStateChanged(stateText, number) } } telephonyManager?.registerTelephonyCallback( context.mainExecutor, callback!! ) } else { Suppress(DEPRECATION) telephonyManager?.listen( phoneStateListener, TelephonyManager.LISTEN_CALL_STATE ) } } fun stopMonitor() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { telephonyManager?.unregisterTelephonyCallback(callback!!) } else { Suppress(DEPRECATION) telephonyManager?.listen(phoneStateListener, TelephonyManager.LISTEN_NONE) } telephonyManager null callback null } Suppress(DEPRECATION) private val phoneStateListener object : android.telephony.PhoneStateListener() { override fun onCallStateChanged(state: Int, phoneNumber: String?) { val stateText formatState(state) listener?.onStateChanged(stateText, phoneNumber) } } private fun formatState(state: Int): String { return when (state) { TelephonyManager.CALL_STATE_IDLE - 空闲 TelephonyManager.CALL_STATE_RINGING - 来电响铃 TelephonyManager.CALL_STATE_OFFHOOK - 通话中 else - 未知 } } private fun getIncomingNumber(): String? { return try { Suppress(DEPRECATION) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 获取来电号码受限这里返回 null // 实际项目中可以通过 CallScreeningService 或通话记录获取 null } else { // 旧版本可以尝试读取 telephonyManager?.line1Number } } catch (e: Exception) { null } } }这一段代码做了两套兼容逻辑Android 12 使用新的TelephonyCallback旧版本使用PhoneStateListener。需要注意getIncomingNumber()在高版本系统上只是一个占位实现因为系统限制导致无法在这种监听方式中直接拿到号码。后续我们会讨论更可靠的号码获取方案。4.5 自动接听辅助功能服务普通 App 无法直接接听电话但可以借助AccessibilityService模拟点击接听按钮。这里给出一个演示用的辅助功能服务。先创建辅助功能配置res/xml/accessibility_service_config.xml?xml version1.0 encodingutf-8? accessibility-service xmlns:androidhttp://schemas.android.com/apk/res/android android:accessibilityEventTypestypeWindowStateChanged|typeWindowContentChanged android:accessibilityFeedbackTypefeedbackGeneric android:accessibilityFlagsflagDefault android:canPerformGesturestrue android:canRetrieveWindowContenttrue android:descriptionstring/accessibility_service_description android:notificationTimeout100 /AutoAnswerService.ktpackage com.example.callphone import android.accessibilityservice.AccessibilityService import android.accessibilityservice.GestureDescription import android.content.Context import android.graphics.Path import android.graphics.Rect import android.os.Build import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo class AutoAnswerService : AccessibilityService() { companion object { private const val TAG AutoAnswerService } override fun onAccessibilityEvent(event: AccessibilityEvent?) { if (event null) return if (event.eventType AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || event.eventType AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED ) { // 找到接听按钮 val root rootInActiveWindow ?: return val clickNode findClickNode(root) if (clickNode ! null clickNode.isVisibleToUser) { clickNode.performAction(AccessibilityNodeInfo.ACTION_CLICK) Log.d(TAG, 已模拟点击接听按钮) } } } private fun findClickNode(root: AccessibilityNodeInfo): AccessibilityNodeInfo? { // 先遍历查找文本包含“接听”或 contentDescription 包含“接听”的节点 val queue ArrayDequeAccessibilityNodeInfo() queue.add(root) while (queue.isNotEmpty()) { val node queue.removeFirst() val text node.text?.toString() ?: val desc node.contentDescription?.toString() ?: if (text.contains(接听) || text.contains(answer) || desc.contains(接听) || desc.contains(answer) ) { return node } for (i in 0 until node.childCount) { val child node.getChild(i) ?: continue queue.add(child) } } return null } override fun onInterrupt() { Log.d(TAG, onInterrupt) } }这个辅助功能服务只是一个演示思路。它的局限性很明显不同厂商的拨号界面尤其是系统电话 App层级结构不一致“接听”按钮可能无法被文本匹配找到。Android 系统对无障碍点击事件有频率限制如果频繁模拟点击可能被系统拦截。从 Android 12 开始部分系统界面不允许第三方服务模拟点击。因此在实际产品中自动接听更可靠的方案是通过系统合作或定制 ROM 实现。如果你只是做个人玩具项目辅助功能方案可以作为学习参考。4.6 运行与验证将项目安装到真机后按以下步骤验证点击“申请电话权限”授予权限。点击“开始监听电话状态”。使用另一台手机拨打本机号码。观察界面状态变化应该会依次显示当前状态来电响铃当前状态通话中当前状态空闲如果状态没有变化请检查App 是否被杀掉。是否在后台限制内运行。目标 SDK 是否影响到后台启动等行为。4.7 关于获取来电号码的替代方案前面提到在新版系统上通过TelephonyCallback很难直接拿到来电号码。在真实项目中可以考虑以下几种方案方案原理限制广播接收器动态注册在 Activity 或 Service 中动态注册PHONE_STATE广播intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER)获取号码动态注册有效需要保持组件存活后台情况下可能被系统延缓CallScreeningService拦截并筛选来电可拿到号码需要系统授权角色且主要是拦截场景通话记录查询在状态变为空闲后查询最新一条通话记录需要READ_CALL_LOG权限用户接受度低通知监听读取系统电话 App 的通知依赖系统电话 App 是否发送通知动态注册广播的代码示例// 在 PhoneStateManager 中增加广播注册 private val phoneStateReceiver object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { val state intent?.getStringExtra(TelephonyManager.EXTRA_STATE) val number intent?.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) if (state TelephonyManager.EXTRA_STATE_RINGING) { listener?.onStateChanged(来电响铃, number) } } } fun registerReceiver() { val filter IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED) ContextCompat.registerReceiver( context, phoneStateReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED ) }这里的核心逻辑是监听状态变化 通过广播携带的EXTRA_INCOMING_NUMBER补充号码。两者配合使用。5. 常见问题与排查思路电话监听功能的开发和调试过程中最常见的问题通常集中在权限、系统限制、保活和 API 兼容这几个方面。下面整理成表格方便你快速定位。问题现象常见原因解决思路点击“开始监听”后没有任何状态回调未授予 READ_PHONE_STATE 权限检查权限是否授予动态申请权限后重新监听高版本设备上接不到回调使用了废弃的 PhoneStateListener切换为 TelephonyCallback来电号码始终为 null新版系统隐私限制厂商 ROM 限制动态注册广播辅助获取号码或使用 CallScreeningServiceApp 在后台运行一段时间后监听失效系统杀掉了应用进程使用前台服务引导用户加入电池白名单自动接听功能没有反应辅助功能未开启电话界面节点结构不同检查辅助功能状态打印节点树调试部分手机上权限弹窗没有出现厂商默认权限策略限制进入系统设置手动授予电话权限在模拟器中测试没有效果模拟器不支持真实的电话状态变化使用真机测试或者使用 adb 模拟广播有限编译报错 TelephonyCallback 找不到compileSdk 低于 31将 compileSdk 提升到 31 或更高5.1 一个容易被忽略的问题目标 SDK 与后台限制当 targetSdk 大于等于 29 时系统对后台应用获取电话状态相关信息的限制更加严格。虽然registerTelephonyCallback不要求应用处于前台但应用如果被系统按“后台限制”策略冻结回调会延迟甚至丢失。解决方案是使用前台服务例如在监听开始时启动一个前台服务并在通知栏显示常驻通知。这样可以显著提高监听的稳定性。5.2 adb 辅助测试命令仅限模拟器或调试设备如果开发条件有限可以使用 adb 模拟电话状态变化但需要设备支持。命令仅供参考adb shell am broadcast -a android.intent.action.PHONE_STATE --es state RINGING --es incoming_number 10086 adb shell am broadcast -a android.intent.action.PHONE_STATE --es state OFFHOOK adb shell am broadcast -a android.intent.action.PHONE_STATE --es state IDLE注意这类模拟广播在高版本系统真机上可能广播不过去厂商限制较多不宜作为唯一验证手段。6. 最佳实践与工程建议6.1 不要在 onDestroy 中忘记注销监听很多开发者在使用TelephonyManager时只注册、不注销导致内存泄漏。正确做法是在组件销毁时注销override fun onDestroy() { super.onDestroy() phoneStateManager.stopMonitor() phoneStateManager.clearListener() }如果你在 Service 中注册注意在onDestroy或onTaskRemoved中做清理。6.2 电话监听必须配合前台服务前台服务可以显著提高TelephonyCallback回调的稳定性。建议将监听逻辑放至service android:name.PhoneMonitorService android:foregroundServiceTypephoneCall /在 Android 14API 34上声明foregroundServiceTypephoneCall需要同时声明权限uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_PHONE_CALL /不过要说明的是phoneCall类型的前台服务有严格的启动条件与场景限制普通应用不一定能随便使用。更通用的方案是使用dataSync或specialUse类型如果你的应用符合相关场景具体需要根据项目实际情况选择。这里给出的是思路版本差异较大务必以官方文档为准。6.3 不要一次申请太多权限很多项目为了省事把READ_PHONE_STATE、READ_CALL_LOG、READ_CONTACTS一起申请。这会导致用户反感且应用市场审核时会重点审查敏感权限的使用场景。建议只申请业务真正需要的权限并在申请前通过弹窗说明用途。例如只为检测来电状态 → 只申请READ_PHONE_STATE必须读取历史来电记录 → 再申请READ_CALL_LOG需要展示联系人备注 → 再申请READ_CONTACTS6.4 隐私合规是上架前的高压线随着个人信息保护法规的完善国内应用市场对电话权限的审核非常严格。以下几点务必注意应用商店审核时必须如实说明电话权限的使用场景。不能收集与功能无关的电话信息。获取的号码只能用于当前功能不能上传到自己的服务器除非有明确授权和隐私政策说明。隐私政策中要明确列出收集哪些信息、用途、保存周期。6.5 日志与排查建议开发阶段建议将电话状态、时间、号码等信息写入本地日志方便回溯问题private fun writeLog(content: String) { val time SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.getDefault()).format(Date()) val logLine $time $content\n try { val file File(context.getExternalFilesDir(null), call_log.txt) file.appendText(logLine) } catch (e: Exception) { Log.e(TAG, writeLog error, e) } }发布版本时注意关闭 debug 日志避免敏感信息泄露。6.6 关注 API 生命周期变化Android 系统 API 变化周期很快。写电话功能代码时建议养成查看 Android 官方版本兼容文档的习惯。PhoneStateListener的废弃就是一个例子旧写法虽然还能跑但迟早会被完全移除。长期维护的项目应该尽早迁移到新 API。6.7 测试清单上线前建议做一轮系统性的真机测试至少测试 Android 10、12、14 三个版本区间。至少测试小米、华为、OPPO、vivo 四个主流厂商 ROM。测试场景包括息屏来电、亮屏来电、通话中再来电、连接蓝牙耳机时来电、双卡手机副卡来电。注意双卡设备上不同卡槽的来电状态回调可能行为不一致。7. 总结与学习路线本文通过一个“来接电话”的实战场景梳理了 Android 来电监听的关键知识点。你至少应该掌握以下内容TelephonyManager是获取电话服务状态的核心类。推荐使用TelephonyCallback替代废弃的PhoneStateListener。来电监听需要动态申请READ_PHONE_STATE权限。高版本系统获取来电号码受限需要结合广播或其他方案。自动接听需要辅助功能或系统级能力普通应用直接实现有限。App 进程存活是监听稳定性的前提前台服务是关键手段。隐私合规定不能触碰敏感权限要谨慎申请。如果继续深入下一步可以学习的方向包括CallScreeningService拦截与筛选来电的完整实践。TelecomManager与通话管理 API了解系统级电话应用的开发方式。来电秀、骚扰拦截、通话录音等垂直场景的合规方案。各大厂商推送与保活策略提升应用稳定性。电话功能开发的难点不在语法而在于“系统限制”和“设备碎片化”。多在不同机型上测试多整理厂商差异才能逐步积累出可靠的经验。希望这篇文章能帮你少走一些弯路。如果文中有不清楚的地方欢迎在评论区留言交流。
返回列表