Flutter通话保持插件flutter_callkeep的使用

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

Flutter通话保持插件flutter_callkeep的使用

flutter_callkeep 插件旨在通过iOS CallKit和Android自定义UI来显示来电通知/屏幕。Web端也支持,但用户需要自行构建Flutter中的来电界面。

Native setup

Android

AndroidManifest.xml 中设置主Activity的 launchModesingleInstance

<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.xmlstrings.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

1 回复

更多关于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.javaMyCallKeepService.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);
  });
}

这段代码展示了如何监听和响应answerCallendCallholdCallunholdCall事件。

总结

通过上述步骤,你可以在Flutter应用中集成

回到顶部