Flutter通话保持插件callkeep的使用
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
更多关于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.plist
和AppDelegate.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
插件,并处理基本的通话开始和结束事件。根据你的具体需求,你可能需要添加更多的配置和处理逻辑。