Flutter通话保持插件flutter_callkeep的使用
Flutter通话保持插件flutter_callkeep的使用
flutter_callkeep
插件旨在通过iOS CallKit和Android自定义UI来显示来电通知/屏幕。Web端也支持,但用户需要自行构建Flutter中的来电界面。
Native setup
Android
在 AndroidManifest.xml
中设置主Activity的 launchMode
为 singleInstance
:
<manifest...>
<activity ...
android:name=".MainActivity"
android:launchMode="singleInstance">
...
</activity>
</manifest>
对于Android 13及以上版本,您需要请求通知权限以显示来电通知。可以通过 firebase_messaging
或者 permission_handler
包实现。
iOS
在 Info.plist
中添加以下配置:
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
<string>remote-notification</string>
<string>voip</string>
</array>
并且更新 AppDelegate.swift
来处理PushKit推送(由于iOS 13 PushKit VoIP限制必须通过原生代码处理)。
Usage
Dependency:
将依赖项添加到你的 pubspec.yaml
文件中:
dependencies:
flutter_callkeep: ^latest_version
或者运行:
flutter pub add flutter_callkeep
Setup:
在展示来电之前,你需要配置该包:
void configureCallkeep() {
final config = CallKeepConfig(
appName: 'CallKeep',
// 其他插件配置...
android: CallKeepAndroidConfig(
// Android 配置...
),
ios: CallKeepIosConfig(
// iOS 配置...
),
headers: {'apiKey': 'Abc@123!', 'platform': 'flutter'},
);
CallKeep.instance.configure(config);
}
Display incoming call:
final data = CallEvent(
uuid: uuid,
callerName: 'Test User',
handle: '0123456789',
hasVideo: false,
duration: 30000,
extra: {'userId': '1a2b3c4d'},
);
await CallKeep.instance.displayIncomingCall(data);
Start an outgoing call:
final data = CallEvent(
uuid: uuid,
callerName: 'Test User',
handle: '0123456789',
hasVideo: false,
duration: 30000,
extra: {'userId': '1a2b3c4d'},
);
CallKeep.instance.startCall(data);
Handling events and showing custom UI (Web):
Future<void> setEventHandler() async {
CallKeep.instance.handler = CallEventHandler(
onCallIncoming: (event) {
print('call incoming: ${event.toMap()}');
if (!CallKeep.instance.isIncomingCallDisplayed) {
showAdaptiveDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Incoming Call"),
content: Text("Incoming call from ${event.callerName}"),
actions: [
TextButton(
onPressed: () {
CallKeep.instance.acceptCall(event.uuid);
Navigator.pop(context);
},
child: Text("Accept"),
),
TextButton(
onPressed: () {
CallKeep.instance.endCall(event.uuid);
Navigator.pop(context);
},
child: Text("Decline"),
),
],
);
});
}
},
onCallStarted: (event) {
print('call started: ${event.toMap()}');
},
onCallEnded: (event) {
print('call ended: ${event.toMap()}');
},
onCallAccepted: (event) {
print('call answered: ${event.toMap()}');
NavigationService.instance.pushNamedIfNotCurrent(AppRoute.callingPage, args: event.toMap());
},
onCallDeclined: (event) async {
print('call declined: ${event.toMap()}');
},
);
}
Customization (Android):
你可以通过修改 colors.xml
和 strings.xml
来定制背景颜色和文本本地化。
colors.xml
<!-- A hex color value to be displayed on the top part of the custom incoming call UI -->
<color name="incoming_call_bg_color">#80ffffff</color>
strings.xml
<string name="accept_text">Accept</string>
<string name="decline_text">Decline</string>
<string name="text_missed_call">Missed call</string>
<string name="text_call_back">Call back</string>
<string name="call_header">Call from CallKeep</string>
示例代码
以下是完整的示例代码:
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_callkeep/flutter_callkeep.dart';
import 'package:flutter_callkeep_example/app_router.dart';
import 'package:flutter_callkeep_example/navigation_service.dart';
import 'package:uuid/uuid.dart';
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print("Handling a background message: ${message.messageId}");
configureCallkeep();
displayIncomingCall(const Uuid().v4());
}
void configureCallkeep() {
final config = CallKeepConfig(
appName: 'CallKeep',
acceptText: 'Accept',
declineText: 'Decline',
missedCallText: 'Missed call',
callBackText: 'Call back',
android: CallKeepAndroidConfig(
logo: "ic_logo",
showCallBackAction: true,
showMissedCallNotification: true,
ringtoneFileName: 'system_ringtone_default',
accentColor: '#0955fa',
backgroundUrl: 'assets/test.png',
incomingCallNotificationChannelName: 'Incoming Calls',
missedCallNotificationChannelName: 'Missed Calls',
),
ios: CallKeepIosConfig(
iconName: 'CallKitLogo',
handleType: CallKitHandleType.generic,
isVideoSupported: true,
maximumCallGroups: 2,
maximumCallsPerCallGroup: 1,
audioSessionActive: true,
audioSessionPreferredSampleRate: 44100.0,
audioSessionPreferredIOBufferDuration: 0.005,
supportsDTMF: true,
supportsHolding: true,
supportsGrouping: false,
supportsUngrouping: false,
ringtoneFileName: 'system_ringtone_default',
),
headers: {'apiKey': 'Abc@123!', 'platform': 'flutter'},
);
CallKeep.instance.configure(config);
}
Future<void> displayIncomingCall(String uuid) async {
final data = CallEvent(
uuid: uuid,
callerName: 'Hien Nguyen',
handle: '0123456789',
hasVideo: false,
duration: 30000,
extra: {'userId': '1a2b3c4d'},
);
await CallKeep.instance.displayIncomingCall(data);
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
late final Uuid _uuid;
String? _currentUuid;
late final FirebaseMessaging _firebaseMessaging;
@override
void initState() {
super.initState();
_uuid = const Uuid();
initFirebase();
WidgetsBinding.instance.addObserver(this);
checkAndNavigationCallingPage();
getDevicePushTokenVoIP();
}
Future<CallEvent?> getCurrentCall() async {
var calls = await CallKeep.instance.activeCalls();
if (calls.isNotEmpty) {
print('DATA: $calls');
_currentUuid = calls[0].uuid;
return calls[0];
} else {
_currentUuid = "";
return null;
}
}
checkAndNavigationCallingPage() async {
var currentCall = await getCurrentCall();
print('not answered call ${currentCall?.toMap()}');
if (currentCall != null) {
NavigationService.instance.pushNamedIfNotCurrent(
AppRoute.callingPage,
args: currentCall.toMap(),
);
}
}
@override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
print(state);
if (state == AppLifecycleState.resumed) {
checkAndNavigationCallingPage();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
initFirebase() async {
try {
await Firebase.initializeApp();
} catch (e) {
print(e);
}
_firebaseMessaging = FirebaseMessaging.instance;
_firebaseMessaging.requestPermission();
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
print(
'Message title: ${message.notification?.title}, body: ${message.notification?.body}, data: ${message.data}');
_currentUuid = _uuid.v4();
displayIncomingCall(_currentUuid!);
});
_firebaseMessaging.getToken().then((token) {
print('Device Token FCM: $token');
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
onGenerateRoute: AppRoute.generateRoute,
initialRoute: AppRoute.homePage,
navigatorKey: NavigationService.instance.navigationKey,
navigatorObservers: [NavigationService.instance.routeObserver],
);
}
Future<void> getDevicePushTokenVoIP() async {
var devicePushTokenVoIP = await CallKeep.instance.getDevicePushTokenVoIP();
print(devicePushTokenVoIP);
}
}
以上是关于如何在Flutter项目中使用flutter_callkeep
插件进行通话保持的完整指南。请根据自己的需求调整配置和代码。
更多关于Flutter通话保持插件flutter_callkeep的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter通话保持插件flutter_callkeep的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用flutter_callkeep
插件来实现通话保持功能的示例代码。flutter_callkeep
插件允许你在Flutter应用中处理来电界面和通话保持功能。这个插件需要与原生iOS和Android代码进行交互,因此你需要进行一些平台特定的配置。
1. 添加依赖
首先,在你的pubspec.yaml
文件中添加flutter_callkeep
依赖:
dependencies:
flutter:
sdk: flutter
flutter_callkeep: ^x.y.z # 请替换为最新版本号
然后运行flutter pub get
来安装依赖。
2. 配置iOS
在ios/Runner/Info.plist
文件中,添加以下权限请求:
<key>NSMicrophoneUsageDescription</key>
<string>我们需要您的许可才能使用麦克风</string>
<key>NSCameraUsageDescription</key>
<string>我们需要您的许可才能使用相机</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>voip</string>
</array>
<key>UIApplicationSupportsMultipleWindows</key>
<true/>
3. 配置Android
在android/app/src/main/AndroidManifest.xml
文件中,添加以下权限和声明:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application
... >
<service
android:name=".MyCallKeepService"
android:permission="android.permission.BIND_VOIP_SERVICE">
<intent-filter>
<action android:name="android.telecom.VoipService" />
</intent-filter>
</service>
</application>
创建MyCallKeepService.java
或MyCallKeepService.kt
(取决于你使用的是Java还是Kotlin):
Java:
package com.example.myapp;
import android.content.Intent;
import android.telecom.Connection;
import android.telecom.ConnectionService;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.content.Context;
import androidx.annotation.NonNull;
public class MyCallKeepService extends ConnectionService {
@Override
public Connection onCreateConnection(PhoneAccountHandle handle) {
return new MyConnection(this);
}
@Override
public PhoneAccount onCreateOutgoingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
return new PhoneAccount.Builder(handle, "My VoIP Account")
.setCapabilities(PhoneAccount.CAPABILITIES_SELF_MANAGED)
.build();
}
private static class MyConnection extends Connection {
private MyCallKeepService service;
MyConnection(MyCallKeepService service) {
this.service = service;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onDisconnect() {
super.onDisconnect();
}
@NonNull
@Override
public void onAnswer() {
super.onAnswer();
}
@NonNull
@Override
public void onHold() {
super.onHold();
}
@Override
public void addVideoProvider(@NonNull android.telecom.VideoProvider videoProvider) {
// Not implemented
}
@Override
public void removeVideoProvider(@NonNull android.telecom.VideoProvider videoProvider) {
// Not implemented
}
@Override
public boolean can(int capability) {
return capability == CONNECTION_CAPABILITY_MUTE ||
capability == CONNECTION_CAPABILITY_HOLD;
}
}
}
Kotlin:
package com.example.myapp
import android.content.Context
import android.content.Intent
import android.telecom.*
import androidx.annotation.NonNull
class MyCallKeepService : ConnectionService() {
override fun onCreateConnection(handle: PhoneAccountHandle): Connection {
return MyConnection(this)
}
override fun onCreateOutgoingConnection(
handle: PhoneAccountHandle,
request: ConnectionRequest
): PhoneAccount {
return PhoneAccount.Builder(handle, "My VoIP Account")
.setCapabilities(PhoneAccount.CAPABILITIES_SELF_MANAGED)
.build()
}
private inner class MyConnection : Connection() {
override fun onDestroy() {
super.onDestroy()
}
override fun onActive() {
super.onActive()
}
override fun onDisconnect() {
super.onDisconnect()
}
@NonNull
override fun onAnswer() {
super.onAnswer()
}
@NonNull
override fun onHold() {
super.onHold()
}
override fun addVideoProvider(@NonNull videoProvider: VideoProvider) {
// Not implemented
}
override fun removeVideoProvider(@NonNull videoProvider: VideoProvider) {
// Not implemented
}
override fun can(capability: Int): Boolean {
return capability == CONNECTION_CAPABILITY_MUTE ||
capability == CONNECTION_CAPABILITY_HOLD
}
}
}
4. 初始化flutter_callkeep
在你的Flutter应用中,初始化flutter_callkeep
并配置它:
import 'package:flutter/material.dart';
import 'package:flutter_callkeep/flutter_callkeep.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
CallKeep.setup(
iosConfig: IOSConfig(
appName: 'MyApp',
),
androidConfig: AndroidConfig(
alertTitle: 'Incoming Call',
alertMessage: 'from Flutter App',
cancelButton: 'Decline',
acceptButton: 'Accept',
ringtone: 'ringtone_uri', // Provide your ringtone URI here
),
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter CallKeep Example'),
),
body: Center(
child: ElevatedButton(
onPressed: () async {
// Simulate an incoming call
var uuid = UUID().v4(); // You need to add a UUID package to generate unique IDs
await CallKeep.startCall(
uuid,
handle: '+1234567890',
handleType: HandleType.PhoneNumber,
isVideo: false,
);
},
child: Text('Start Call'),
),
),
),
);
}
}
确保在你的pubspec.yaml
文件中添加了uuid
依赖:
dependencies:
uuid: ^x.y.z # 请替换为最新版本号
5. 处理来电和通话保持事件
你可以通过监听CallKeep
的事件来处理通话状态的变化:
@override
void initState() {
super.initState();
CallKeep.addEventListener('answerCall', (event) async {
var uuid = event['uuid'];
await CallKeep.acceptCall(uuid);
});
CallKeep.addEventListener('endCall', (event) async {
var uuid = event['uuid'];
await CallKeep.endCall(uuid);
});
CallKeep.addEventListener('holdCall', (event) async {
var uuid = event['uuid'];
await CallKeep.holdCall(uuid);
});
CallKeep.addEventListener('unholdCall', (event) async {
var uuid = event['uuid'];
await CallKeep.unholdCall(uuid);
});
}
这段代码展示了如何监听和响应answerCall
、endCall
、holdCall
和unholdCall
事件。
总结
通过上述步骤,你可以在Flutter应用中集成