Flutter通话保持插件callkeep的使用

发布于 1周前 作者 phonegap100 来自 Flutter

Flutter通话保持插件callkeep的使用

简介

callkeep 是一个用于 Flutter 的插件,它作为您的通话系统(如 RTC、VOIP 等)和用户之间的中介,提供原生的通话界面来处理应用程序中的通话。这使得您可以在设备锁定或应用程序终止的情况下接听电话。

初始设置

在 Android 上,callkeep 会在启动时显示一个弹窗以请求必要的权限。以下是一个基本的配置示例:

final callSetup = <String, dynamic>{
  'ios': {
    'appName': 'CallKeepDemo',
  },
  'android': {
    'alertTitle': 'Permissions required',
    'alertDescription':
        'This application needs to access your phone accounts',
    'cancelButton': 'Cancel',
    'okButton': 'OK',
    // Required to get audio in background when using Android 11
    'foregroundService': {
      'channelId': 'com.company.my',
      'channelName': 'Foreground service for my app',
      'notificationTitle': 'My app is running on background',
      'notificationIcon': 'mipmap/ic_notification_launcher',
    },
  },
};

callKeep.setup(callSetup);

这个配置应该在应用程序启动时定义,但请注意,如果尚未授予所需的权限,此弹窗将会出现。一种干净的替代方法是,在应用程序启动时自行控制所需的权限,并仅在这些权限被授予后调用 setup() 方法。

事件处理

callkeep 提供了一些事件来处理通话期间的原生操作。这些事件非常重要,因为它们充当原生通话 UI 和您的通话 P-C-M(Presenter/Controller/Manager)之间的中介。

假设您的应用程序已经实现了一个通话系统(如 RTC、VoIP 或其他),并有自己的通话 UI,您可能会使用一些基本的控制:

  • 挂断 -> presenter.hangUp()
  • 麦克风切换 -> presenter.microSwitch()

在使用 callkeep 后,这些操作将变为:

  • 挂断 -> callkeep.endCall(call_uuid)
  • 麦克风切换 -> callKeep.setMutedCall(uuid, true / false)

您可以处理这些事件如下:

Future<void> answerCall(CallKeepPerformAnswerCallAction event) async {
  print('CallKeepPerformAnswerCallAction ${event.callUUID}');
  // 通知您的通话 P-C-M 处理接听操作
}

Future<void> endCall(CallKeepPerformEndCallAction event) async {
  print('CallKeepPerformEndCallAction ${event.callUUID}');
  // 通知您的通话 P-C-M 处理挂断操作
}

Future<void> didPerformSetMutedCallAction(CallKeepDidPerformSetMutedCallAction event) async {
  print('CallKeepDidPerformSetMutedCallAction ${event.callUUID}');
  // 通知您的通话 P-C-M 处理静音切换操作
}

Future<void> didToggleHoldCallAction(CallKeepDidToggleHoldAction event) async {
  print('CallKeepDidToggleHoldAction ${event.callUUID}');
  // 通知您的通话 P-C-M 处理保持切换操作
}

initState 中注册这些事件:

[@override](/user/override)
void initState() {
  super.initState();
  callKeep.on<CallKeepDidDisplayIncomingCall>(didDisplayIncomingCall);
  callKeep.on<CallKeepPerformAnswerCallAction>(answerCall);
  callKeep.on<CallKeepPerformEndCallAction>(endCall);
  callKeep.on<CallKeepDidToggleHoldAction>(didToggleHoldCallAction);
}

显示来电(前台、后台或终止状态)

当“某事”在应用程序中接收到时,触发来电动作。以下是一个使用 FCM 推送消息的示例:

final FlutterCallkeep _callKeep = FlutterCallkeep();
bool _callKeepStarted = false;

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  if (!_callKeepStarted) {
    try {
      await _callKeep.setup(callSetup);
      _callKeepStarted = true;
    } catch (e) {
      print(e);
    }
  }

  // 处理远程消息中的呼叫 UUID 并显示来电
  var callerIdFrom = message.payload()["caller_id"] as String;
  var callerName = message.payload()["caller_name"] as String;
  var uuid = message.payload()["uuid"] as String;
  var hasVideo = message.payload()["has_video"] == "true";

  bool hasPhoneAccount = await _callKeep.hasPhoneAccount();
  if (!hasPhoneAccount) {
    hasPhoneAccount = await _callKeep.hasDefaultPhoneAccount(context, callSetup["android"]);
  }

  if (!hasPhoneAccount) {
    return;
  }

  await _callKeep.displayIncomingCall(uuid, callerIdFrom, localizedCallerName: callerName, hasVideo: hasVideo);
  _callKeep.backToForeground();
}

示例代码

以下是一个完整的示例代码,展示了如何使用 callkeep 插件来处理来电和通话事件:

import 'dart:async';
import 'dart:io';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:callkeep/callkeep.dart';
import 'package:logger/logger.dart';
import 'package:uuid/uuid.dart';

/// 背景消息处理程序
final FlutterCallkeep _callKeep = FlutterCallkeep();
bool _callKeepInited = false;

Future<dynamic> myBackgroundMessageHandler(RemoteMessage message) async {
  Logger logger = Logger();
  logger.d('backgroundMessage: message => ${message.toString()}');

  // 处理数据消息
  var data = message.data;
  var callerId = data['caller_id'] ?? message.senderId ?? "No Sender Id";
  var callerName = data['caller_name'] as String;
  var callUUID = data['uuid'] ?? const Uuid().v4();
  var hasVideo = data['has_video'] == "true";

  _callKeep.on<CallKeepPerformAnswerCallAction>(
      (CallKeepPerformAnswerCallAction event) {
    logger.d(
        'backgroundMessage: CallKeepPerformAnswerCallAction ${event.callData.callUUID}');
    Timer(const Duration(seconds: 1), () {
      logger.d(
          '[setCurrentCallActive] $callUUID, callerId: $callerId, callerName: $callerName');
      _callKeep.setCurrentCallActive(callUUID);
    });
  });

  _callKeep.on<CallKeepPerformEndCallAction>((CallKeepPerformEndCallAction event) {
    logger.d('backgroundMessage: CallKeepPerformEndCallAction ${event.callUUID}');
  });

  if (!_callKeepInited) {
    _callKeep.setup(
      showAlertDialog: null,
      options: <String, dynamic>{
        'ios': {
          'appName': 'CallKeepDemo',
        },
        'android': {
          'additionalPermissions': [
            'android.permission.CALL_PHONE',
            'android.permission.READ_PHONE_NUMBERS'
          ],
          'foregroundService': {
            'channelId': 'com.example.call-kit-test',
            'channelName': 'callKitTest',
            'notificationTitle': 'My app is running on background',
            'notificationIcon': 'Path to the resource icon of the notification',
          },
        },
      },
    );
    _callKeepInited = true;
  }

  logger.d('backgroundMessage: displayIncomingCall ($callerId)');
  _callKeep.displayIncomingCall(
    uuid: callUUID,
    handle: callerId,
    callerName: callerName,
    hasVideo: hasVideo,
  );
  _callKeep.backToForeground();

  return Future.value(null);
}

void main() {
  Logger.level = Level.all;
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Welcome to Flutter',
      debugShowCheckedModeBanner: false,
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  [@override](/user/override)
  MyAppState createState() => MyAppState();
}

class Call {
  Call(this.number);
  String number;
  bool held = false;
  bool muted = false;
}

class MyAppState extends State<HomePage> {
  final FlutterCallkeep _callKeep = FlutterCallkeep();
  Map<String, Call> calls = {};
  String newUUID() => const Uuid().v4();
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
  Logger logger = Logger();

  void iOSPermission() async {
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false,
    );
    logger.d('Settings registered: $settings');
  }

  void removeCall(String callUUID) {
    setState(() {
      calls.remove(callUUID);
    });
  }

  void setCallHeld(String callUUID, bool held) {
    setState(() {
      calls[callUUID]?.held = held;
    });
  }

  void setCallMuted(String callUUID, bool muted) {
    setState(() {
      calls[callUUID]?.muted = muted;
    });
  }

  Future<void> answerCall(CallKeepPerformAnswerCallAction event) async {
    final callUUID = event.callData.callUUID;
    final number = calls[callUUID]?.number;
    if (callUUID == null) {
      logger.e("Tried to answer call but callUUID is null");
      return;
    }
    logger.d('[answerCall] $callUUID, number: $number');

    Timer(const Duration(seconds: 1), () {
      logger.d('[setCurrentCallActive] $callUUID, number: $number');
      _callKeep.setCurrentCallActive(callUUID);
    });
  }

  Future<void> endCall(CallKeepPerformEndCallAction event) async {
    final callUUID = event.callUUID;
    if (callUUID == null) {
      logger.e("Tried to endcall but callUUID is null");
      return;
    }
    logger.d('[endCall] $callUUID');
    removeCall(callUUID);
  }

  Future<void> didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async {
    logger.d('[didPerformDTMFAction] ${event.callUUID}, digits: ${event.digits}');
  }

  Future<void> didReceiveStartCallAction(CallKeepDidReceiveStartCallAction event) async {
    final callData = event.callData;
    if (callData.handle == null) {
      // @TODO: 有时我们收到 `didReceiveStartCallAction` 时 handle 为 undefined
      return;
    }
    final String callUUID = callData.callUUID ?? newUUID();
    final Call call = Call(callData.handle ?? "No Handle");
    setState(() {
      calls[callUUID] = call;
    });
    logger.d('[didReceiveStartCallAction] $callUUID, number: ${callData.handle}');

    _callKeep.startCall(
        uuid: callUUID, handle: call.number, callerName: call.number);

    Timer(const Duration(seconds: 1), () {
      logger.d('[setCurrentCallActive] $callUUID, number: ${callData.handle}');
      _callKeep.setCurrentCallActive(callUUID);
    });
  }

  Future<void> didPerformSetMutedCallAction(CallKeepDidPerformSetMutedCallAction event) async {
    final callUUID = event.callUUID;
    if (callUUID == null) {
      logger.e("Tried to mute call but callUUID is null");
      return;
    }
    final number = calls[callUUID]?.number ?? "No Number";
    final muted = event.muted ?? false;
    logger.d('[didPerformSetMutedCallAction] $callUUID, number: $number ($muted)');

    setCallMuted(callUUID, muted);
  }

  Future<void> didToggleHoldCallAction(CallKeepDidToggleHoldAction event) async {
    final callUUID = event.callUUID;
    if (callUUID == null) {
      logger.e("Tried to hold call but callUUID is null");
      return;
    }
    final number = calls[callUUID]?.number ?? "No Number";
    final hold = event.hold ?? false;
    logger.d('[didToggleHoldCallAction] $callUUID, number: $number ($hold)');

    setCallHeld(callUUID, hold);
  }

  Future<void> hangup(String callUUID) async {
    _callKeep.endCall(callUUID);
    removeCall(callUUID);
  }

  Future<void> setOnHold(String callUUID, bool held) async {
    _callKeep.setOnHold(uuid: callUUID, shouldHold: held);
    final String handle = calls[callUUID]?.number ?? "No Number";
    logger.d('[setOnHold: $held] $callUUID, number: $handle');
    setCallHeld(callUUID, held);
  }

  Future<void> setMutedCall(String callUUID, bool muted) async {
    _callKeep.setMutedCall(uuid: callUUID, shouldMute: muted);
    final String handle = calls[callUUID]?.number ?? "No Number";
    logger.d('[setMutedCall: $muted] $callUUID, number: $handle');
    setCallMuted(callUUID, muted);
  }

  Future<void> updateDisplay(String callUUID) async {
    final String number = calls[callUUID]?.number ?? "No Number";
    // Android 不会很好地显示 displayName,因此我们需要切换 ...
    if (Platform.isIOS) {
      _callKeep.updateDisplay(
          uuid: callUUID, callerName: 'New Name', handle: number);
    } else {
      _callKeep.updateDisplay(
          uuid: callUUID, callerName: number, handle: 'New Name');
    }

    logger.d('[updateDisplay: $number] $callUUID');
  }

  Future<void> displayIncomingCallDelayed(String number) async {
    Timer(const Duration(seconds: 3), () {
      displayIncomingCall(number);
    });
  }

  Future<void> displayIncomingCall(String number) async {
    final String callUUID = newUUID();
    setState(() {
      calls[callUUID] = Call(number);
    });
    logger.d('Display incoming call now');
    final bool hasPhoneAccount = await _callKeep.hasPhoneAccount();
    if (!hasPhoneAccount) {
      await _callKeep.hasDefaultPhoneAccount(<String, dynamic>{
        'alertTitle': 'Permissions required',
        'alertDescription':
            'This application needs to access your phone accounts',
        'cancelButton': 'Cancel',
        'okButton': 'OK',
        'foregroundService': {
          'channelId': 'com.company.my',
          'channelName': 'Foreground service for my app',
          'notificationTitle': 'My app is running on background',
          'notificationIcon': 'Path to the resource icon of the notification',
        },
      });
    }

    logger.d('[displayIncomingCall] $callUUID number: $number');
    _callKeep.displayIncomingCall(
        uuid: callUUID, handle: number, handleType: 'number', hasVideo: false);
  }

  void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) {
    final callUUID = event.callData.callUUID;
    final number = event.callData.handle ?? "No Number";
    if (callUUID == null) {
      logger.e("Tried to diplay incoming call but callUUID is null");
      return;
    }
    logger.d('[displayIncomingCall] $callUUID number: $number');
    setState(() {
      calls[callUUID] = Call(number);
    });
  }

  void onPushKitToken(CallKeepPushKitToken event) {
    logger.d('[onPushKitToken] token => ${event.token}');
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    _callKeep.on<CallKeepDidDisplayIncomingCall>(didDisplayIncomingCall);
    _callKeep.on<CallKeepPerformAnswerCallAction>(answerCall);
    _callKeep.on<CallKeepDidPerformDTMFAction>(didPerformDTMFAction);
    _callKeep.on<CallKeepDidReceiveStartCallAction>(didReceiveStartCallAction);
    _callKeep.on<CallKeepDidToggleHoldAction>(didToggleHoldCallAction);
    _callKeep.on<CallKeepDidPerformSetMutedCallAction>(didPerformSetMutedCallAction);
    _callKeep.on<CallKeepPerformEndCallAction>(endCall);
    _callKeep.on<CallKeepPushKitToken>(onPushKitToken);

    _callKeep.setup(
      showAlertDialog: () => showDialog<bool>(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return AlertDialog(
            title: const Text('Permissions Required'),
            content: const Text(
                'This application needs to access your phone accounts'),
            actions: <Widget>[
              TextButton(
                child: const Text('Cancel'),
                onPressed: () => Navigator.of(context).pop(false),
              ),
              TextButton(
                child: const Text('OK'),
                onPressed: () => Navigator.of(context).pop(true),
              ),
            ],
          );
        },
      ).then((value) => value ?? false),
      options: <String, dynamic>{
        'ios': {
          'appName': 'CallKeepDemo',
        },
        'android': {
          'additionalPermissions': [
            'android.permission.CALL_PHONE',
            'android.permission.READ_PHONE_NUMBERS'
          ],
          'foregroundService': {
            'channelId': 'com.example.call-kit-test',
            'channelName': 'callKitTest',
            'notificationTitle': 'My app is running on background',
            'notificationIcon': 'Path to the resource icon of the notification',
          },
        },
      },
    );

    if (Platform.isIOS) iOSPermission();

    if (Platform.isAndroid) {
      _firebaseMessaging.getToken().then((token) {
        logger.d('[FCM] token => $token');
      });

      FirebaseMessaging.onMessage.listen((RemoteMessage message) {
        print('Got a message whilst in the foreground!');
        print('Message data: ${message.data}');
        logger.d('onMessage: $message');

        // 处理数据消息
        var data = message.data;
        var callerId = data['caller_id'] ?? message.senderId ?? "No Sender Id";
        var callerName = data['caller_name'] as String;
        var callUUID = data['uuid'] ?? const Uuid().v4();
        var hasVideo = data['has_video'] == "true";

        setState(() {
          calls[callUUID] = Call(callerId);
        });
        _callKeep.displayIncomingCall(
          uuid: callUUID,
          handle: callerId,
          callerName: callerName,
          hasVideo: hasVideo,
        );

        if (message.notification != null) {
          print(
              'Message also contained a notification: ${message.notification}');
        }
      });
      FirebaseMessaging.onBackgroundMessage(myBackgroundMessageHandler);
    }
  }

  Widget buildCallingWidgets() {
    return Column(
        mainAxisAlignment: MainAxisAlignment.start,
        children: calls.entries
            .map((MapEntry<String, Call> item) =>
                Column(mainAxisAlignment: MainAxisAlignment.start, children: [
                  Text('number: ${item.value.number}'),
                  Text('uuid: ${item.key}'),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      ElevatedButton(
                        onPressed: () async {
                          setOnHold(item.key, !item.value.held);
                        },
                        child: Text(item.value.held ? 'Unhold' : 'Hold'),
                      ),
                      ElevatedButton(
                        onPressed: () async {
                          updateDisplay(item.key);
                        },
                        child: const Text('Display'),
                      ),
                      ElevatedButton(
                        onPressed: () async {
                          setMutedCall(item.key, !item.value.muted);
                        },
                        child: Text(item.value.muted ? 'Unmute' : 'Mute'),
                      ),
                      ElevatedButton(
                        onPressed: () async {
                          hangup(item.key);
                        },
                        child: const Text('Hangup'),
                      ),
                    ],
                  )
                ]))
            .toList());
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              ElevatedButton(
                onPressed: () async {
                  displayIncomingCall('10086');
                },
                child: const Text('Display incoming call now'),
              ),
              ElevatedButton(
                onPressed: () async {
                  displayIncomingCallDelayed('10086');
                },
                child: const Text('Display incoming call now in 3s'),
              ),
              buildCallingWidgets()
            ],
          ),
        ),
      ),
    );
  }
}

更多关于Flutter通话保持插件callkeep的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter通话保持插件callkeep的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter项目中,使用callkeep插件来管理通话保持功能,可以帮助开发者实现来电显示、接听/挂断电话等功能。以下是一个简单的示例,展示了如何在Flutter项目中集成和使用callkeep插件。

步骤 1: 添加依赖

首先,在pubspec.yaml文件中添加callkeep插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  callkeep: ^0.x.x  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

步骤 2: 配置iOS

在iOS项目中,你需要配置Info.plistAppDelegate.swift(或AppDelegate.m)。

配置Info.plist

添加以下权限请求:

<key>NSMicrophoneUsageDescription</key>
<string>We need your permission to use the microphone</string>
<key>NSCameraUsageDescription</key>
<string>We need your permission to use the camera</string>
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
    <string>voip</string>
</array>
<key>UIRequiresPersistentWiFi</key>
<true/>
<key>UIApplicationSupportsMultipleWindows</key>
<true/>

配置AppDelegate.swift

如果你使用的是Swift,添加以下代码来配置callkeep

import UIKit
import Flutter
import CallKeep

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    let callKeep = CallKeep()
    
    // Configure CallKeep
    callKeep.setup(options: [
      kCallKeepDidReceiveStartCallAction: handleStartCall,
      kCallKeepDidReceiveEndCallAction: handleEndCall,
      kCallKeepMinimumCallDuration: 2.0,
      kCallKeepSupportsVideo: false
    ]) { (error) in
      if let error = error {
        print("Error configuring CallKeep: \(error.localizedDescription)")
      } else {
        print("CallKeep configured successfully")
      }
    }
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  func handleStartCall(uuid: String, handle: String, handleType: CallHandleType, hasVideo: Bool) {
    // Handle incoming call
    print("Received start call action with UUID: \(uuid), Handle: \(handle), Has Video: \(hasVideo)")
  }
  
  func handleEndCall(uuid: String, error: Error?) {
    // Handle end call
    print("Received end call action with UUID: \(uuid), Error: \(String(describing: error))")
  }
}

步骤 3: 配置Android

AndroidManifest.xml中添加必要的权限和配置:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>

<application
    ... >
    <service
        android:name="io.wazo.callkeep.VoiceConnectionService"
        android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
        <intent-filter>
            <action android:name="android.telecom.TelecomService" />
        </intent-filter>
    </service>
    ...
</application>

步骤 4: 使用callkeep插件

在你的Flutter代码中,你可以使用callkeep插件来管理通话。例如,发起一个通话:

import 'package:flutter/material.dart';
import 'package:callkeep/callkeep.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('CallKeep Example'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: _startCall,
            child: Text('Start Call'),
          ),
        ),
      ),
    );
  }

  void _startCall() async {
    // 初始化CallKeep
    await CallKeep().setup(
      options: {
        'ios': {
          'appName': 'MyApp',
        },
        'android': {
          'alertWindowPermission': true,
          'overlayPermission': true,
        },
      },
      eventHandler: {
        CallKeepStartCall: (String uuid, String handle, Map<dynamic, dynamic> handleInfo) async {
          print("Received start call with UUID: $uuid, Handle: $handle");
          // 处理来电
        },
        CallKeepEndCall: (String uuid, String error) async {
          print("Received end call with UUID: $uuid, Error: $error");
          // 处理挂断
        },
      },
    );

    // 发起一个通话
    String uuid = UUID().uuidString; // 需要导入 'dart:math' 包来使用 UUID()
    await CallKeep().startCall(uuid, handle: 'Caller Handle');
  }
}

确保你已经导入了必要的包,并处理了必要的权限请求。这个示例展示了如何初始化callkeep插件,并处理基本的通话开始和结束事件。根据你的具体需求,你可能需要添加更多的配置和处理逻辑。

回到顶部