如何用Flutter Dialog替换Android来电弹窗并实现来电监听
用Flutter Dialog替换Android原生来电弹窗的实现方案
核心问题排查
你之前监听PhoneState未达预期,大概率是这几个原因:
- 缺少必要权限或未完成动态申请
- APP后台运行时被系统回收,未使用前台Service维持监听
- 未拦截原生来电弹窗,导致自定义Dialog被覆盖
分步实现
1. 配置并申请权限
首先在android/app/src/main/AndroidManifest.xml中添加所需权限:
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_CALL_LOG" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" /> <uses-permission android:name="android.permission.END_CALL" />
在Flutter层用permission_handler插件动态申请权限:
import 'package:permission_handler/permission_handler.dart'; Future<void> requestPhonePermissions() async { final permissionStatuses = await [ Permission.phone, Permission.notification, Permission.callLogs, ].request(); if (permissionStatuses.values.any((status) => status.isDenied)) { // 处理权限拒绝逻辑,比如引导用户到设置页开启 } }
2. 后台来电状态监听(基于Android前台Service)
仅靠Flutter层监听会在后台失效,必须用Android前台Service维持监听,通过MethodChannel通知Flutter层:
原生Service实现(Kotlin)
创建PhoneStateService.kt:
package com.your.package.name import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Context import android.content.Intent import android.os.Build import android.os.IBinder import android.telephony.PhoneStateListener import android.telephony.TelephonyManager import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugin.common.MethodChannel class PhoneStateService : Service() { private lateinit var telephonyManager: TelephonyManager private lateinit var phoneStateListener: PhoneStateListener private var channel: MethodChannel? = null override fun onCreate() { super.onCreate() // 初始化MethodChannel val flutterEngine = FlutterEngine(this) flutterEngine.dartExecutor.executeDartEntrypoint( DartExecutor.DartEntrypoint.createDefault() ) channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.your.package/phone_state") // 监听来电状态 telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager phoneStateListener = object : PhoneStateListener() { override fun onCallStateChanged(state: Int, phoneNumber: String?) { super.onCallStateChanged(state, phoneNumber) when (state) { TelephonyManager.CALL_STATE_RINGING -> { channel?.invokeMethod("onIncomingCall", mapOf( "phoneNumber" to phoneNumber, "state" to "ringing" )) } TelephonyManager.CALL_STATE_OFFHOOK, TelephonyManager.CALL_STATE_IDLE -> { channel?.invokeMethod("onCallEnd", null) } } } } telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE) // 启动前台Service,避免被系统回收 createNotificationChannel() val notification = Notification.Builder(this, "phone_listener_channel") .setContentTitle("来电监听中") .setContentText("后台持续监听来电状态") .setSmallIcon(R.drawable.ic_notification) // 替换为你的通知图标 .build() startForeground(1, notification) } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( "phone_listener_channel", "来电监听", NotificationManager.IMPORTANCE_LOW ) val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) } } override fun onBind(intent: Intent?): IBinder? = null override fun onDestroy() { super.onDestroy() telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE) } }
在Manifest中注册Service:
<service android:name=".PhoneStateService" android:foregroundServiceType="phoneCall" />
Flutter层接收回调并显示Dialog
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class PhoneCallScreen extends StatefulWidget { const PhoneCallScreen({super.key}); @override State<PhoneCallScreen> createState() => _PhoneCallScreenState(); } class _PhoneCallScreenState extends State<PhoneCallScreen> { final MethodChannel _channel = const MethodChannel('com.your.package/phone_state'); bool _isDialogShowing = false; @override void initState() { super.initState(); _setupChannelHandler(); _startListeningService(); requestPhonePermissions(); // 调用之前的权限申请方法 } void _setupChannelHandler() { _channel.setMethodCallHandler((call) async { switch (call.method) { case 'onIncomingCall': if (!_isDialogShowing) { final phoneNumber = call.arguments['phoneNumber'] as String?; _showIncomingCallDialog(phoneNumber); } break; case 'onCallEnd': if (_isDialogShowing) { Navigator.pop(context); _isDialogShowing = false; } break; } }); } void _startListeningService() { _channel.invokeMethod('startService'); } void _showIncomingCallDialog(String? phoneNumber) { setState(() => _isDialogShowing = true); showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( backgroundColor: Colors.black87, contentPadding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( phoneNumber ?? '未知号码', style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.w500), ), const SizedBox(height: 40), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.redAccent, fixedSize: const Size(80, 80), shape: const CircleBorder(), ), onPressed: () { _channel.invokeMethod('endCall'); Navigator.pop(context); _isDialogShowing = false; }, child: const Icon(Icons.call_end, color: Colors.white, size: 36), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.greenAccent, fixedSize: const Size(80, 80), shape: const CircleBorder(), ), onPressed: () { _channel.invokeMethod('answerCall'); Navigator.pop(context); _isDialogShowing = false; }, child: const Icon(Icons.call, color: Colors.white, size: 36), ), ], ), ], ), ), ); } @override Widget build(BuildContext context) { return const Scaffold( body: Center(child: Text("来电监听已启动")), ); } }
3. 拦截原生来电弹窗
要替换原生弹窗,必须通过AccessibilityService拦截系统来电界面:
实现AccessibilityService
创建PhoneAccessibilityService.kt:
package com.your.package.name import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityServiceInfo import android.os.Build import android.view.accessibility.AccessibilityEvent import android.telecom.TelecomManager class PhoneAccessibilityService : AccessibilityService() { override fun onAccessibilityEvent(event: AccessibilityEvent?) { event?.let { if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { val packageName = event.packageName?.toString() ?: "" // 针对不同厂商的系统来电包名,可自行扩展 val systemCallPackages = listOf("com.android.incallui", "com.huawei.incallui", "com.miui.incallui") if (systemCallPackages.contains(packageName)) { // 关闭原生来电弹窗 performGlobalAction(GLOBAL_ACTION_BACK) // Android 10+ 可直接通过TelecomManager控制通话 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager // 这里可根据需求选择是否先暂停来电,确保自定义Dialog优先显示 } } } } } override fun onInterrupt() {} override fun onServiceConnected() { super.onServiceConnected() val serviceInfo = AccessibilityServiceInfo().apply { eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED packageNames = arrayOf("com.android.incallui", "com.huawei.incallui", "com.miui.incallui") feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC flags = AccessibilityServiceInfo.DEFAULT } setServiceInfo(serviceInfo) } }
在Manifest中注册:
<service android:name=".PhoneAccessibilityService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" /> </intent-filter> <meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibility_service_config" /> </service>
创建res/xml/accessibility_service_config.xml:
<?xml version="1.0" encoding="utf-8"?> <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android" android:accessibilityEventTypes="typeWindowStateChanged" android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFlags="flagDefault" android:canRetrieveWindowContent="true" android:description="@string/accessibility_desc" android:packageNames="com.android.incallui,com.huawei.incallui,com.miui.incallui" />
在strings.xml添加描述:
<string name="accessibility_desc">用于拦截原生来电弹窗,显示自定义来电界面</string>
注意:AccessibilityService需要用户手动在系统设置中开启,Flutter层可引导用户跳转设置页:
void openAccessibilitySettings() { const platform = MethodChannel('com.your.package/settings'); platform.invokeMethod('openAccessibilitySettings'); }
对应原生方法:
// 在MethodChannel中添加处理 channel.setMethodCallHandler { call, result -> when (call.method) { "openAccessibilitySettings" -> { val intent = Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) result.success(null) } // 其他方法... } }
4. 实现接听/挂断电话的原生方法
在PhoneStateService中添加以下方法,并通过MethodChannel暴露给Flutter:
fun answerCall() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager telecomManager.acceptRingingCall() } else { // Android 9及以下用反射实现 try { val telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager val method = telephonyManager.javaClass.getDeclaredMethod("getITelecomService") method.isAccessible = true val telecomService = method.invoke(telephonyManager) val telecomServiceClass = Class.forName("com.android.internal.telephony.ITelecomService") val answerMethod = telecomServiceClass.getDeclaredMethod("answerRingingCall") answerMethod.invoke(telecomService) } catch (e: Exception) { e.printStackTrace() } } } fun endCall() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager telecomManager.endCall() } else { // Android 8及以下用反射实现 try { val telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager val method = telephonyManager.javaClass.getDeclaredMethod("getITelecomService") method.isAccessible = true val telecomService = method.invoke(telephonyManager) val telecomServiceClass = Class.forName("com.android.internal.telephony.ITelecomService") val endMethod = telecomServiceClass.getDeclaredMethod("endCall") endMethod.invoke(telecomService) } catch (e: Exception) { e.printStackTrace() } } }
在MethodChannel中添加对应调用处理:
channel?.setMethodCallHandler { call, result -> when (call.method) { "startService" -> { startForeground(1, notification) // 这里可复用之前的notification result.success(null) } "answerCall" -> { answerCall() result.success(null) } "endCall" -> { endCall() result.success(null) } } }
常见问题解决
- 后台监听失效:确保前台Service正常启动,通知通道已创建,避免系统回收
- 原生弹窗未拦截:检查AccessibilityService是否开启,包名是否覆盖了对应厂商的来电界面包名
- 权限问题:所有危险权限必须动态申请,Accessibility权限需引导用户手动开启
内容的提问来源于stack exchange,提问作者Vignesh
相关产品推荐
相关产品推荐

