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

如何用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 15:38:08