import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:callkeep/callkeep.dart'; import 'package:matrix/matrix.dart'; import 'package:uuid/uuid.dart'; import 'package:fluffychat/utils/voip_plugin.dart'; class CallKeeper { CallKeeper(this.callKeepManager, this.uuid, this.number, this.call) { call?.onCallStateChanged.stream.listen(_handleCallState); } CallKeepManager callKeepManager; String number; String uuid; bool held = false; bool muted = false; bool connected = false; CallSession? call; void _handleCallState(CallState state) { Logs().v('CallKeepManager::handleCallState: ${state.toString()}'); switch (state) { case CallState.kConnecting: break; case CallState.kConnected: if (!connected) { callKeepManager.answer(uuid); } else { callKeepManager.setMutedCall(uuid, false); callKeepManager.setOnHold(uuid, false); } break; case CallState.kEnded: callKeepManager.hangup(uuid); break; /* TODO: case CallState.kMuted: callKeepManager.setMutedCall(uuid, true); break; case CallState.kHeld: callKeepManager.setOnHold(uuid, true); break; */ case CallState.kFledgling: // TODO: Handle this case. break; case CallState.kInviteSent: // TODO: Handle this case. break; case CallState.kWaitLocalMedia: // TODO: Handle this case. break; case CallState.kCreateOffer: // TODO: Handle this case. break; case CallState.kCreateAnswer: // TODO: Handle this case. break; case CallState.kRinging: // TODO: Handle this case. break; } } } class CallKeepManager { factory CallKeepManager() { return _instance; } CallKeepManager._internal() { _callKeep = FlutterCallkeep(); } static final CallKeepManager _instance = CallKeepManager._internal(); late FlutterCallkeep _callKeep; VoipPlugin? _voipPlugin; Map calls = {}; String newUUID() => const Uuid().v4(); String get appName => 'Famedly'; Map get alertOptions => { 'alertTitle': 'Permissions required', 'alertDescription': '$appName needs to access your phone accounts!', 'cancelButton': 'Cancel', 'okButton': 'ok', // Required to get audio in background when using Android 11 'foregroundService': { 'channelId': 'com.famedly.talk', 'channelName': 'Foreground service for my app', 'notificationTitle': '$appName is running on background', 'notificationIcon': 'mipmap/ic_notification_launcher', }, }; void setVoipPlugin(VoipPlugin plugin) { if (kIsWeb) { throw 'Not support callkeep for flutter web'; } _voipPlugin = plugin; _voipPlugin!.onIncomingCall = (CallSession call) async { await _callKeep.setup( null, { 'ios': { 'appName': appName, }, 'android': alertOptions, }, backgroundMode: true); await displayIncomingCall(call); call.onCallStateChanged.stream.listen((state) { if (state == CallState.kEnded) { _callKeep.endAllCalls(); } }); call.onCallEventChanged.stream.listen((event) { if (event == CallEvent.kLocalHoldUnhold) { Logs().i( 'Call hold event: local ${call.localHold}, remote ${call.remoteOnHold}'); } }); }; } void removeCall(String callUUID) { calls.remove(callUUID); } void addCall(String callUUID, CallKeeper callKeeper) { calls[callUUID] = callKeeper; } String findCallUUID(String number) { var uuid = ''; calls.forEach((String key, CallKeeper item) { if (item.number == number) { uuid = key; return; } }); return uuid; } void setCallHeld(String callUUID, bool held) { calls[callUUID]!.held = held; } void setCallMuted(String callUUID, bool muted) { calls[callUUID]!.muted = muted; } void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) { final callUUID = event.callUUID; final number = event.handle; Logs().v('[displayIncomingCall] $callUUID number: $number'); addCall(callUUID!, CallKeeper(this, callUUID, number!, null)); } void onPushKitToken(CallKeepPushKitToken event) { Logs().v('[onPushKitToken] token => ${event.token}'); } Future initialize() async { _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.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall); } Future hangup(String callUUID) async { await _callKeep.endCall(callUUID); removeCall(callUUID); } Future reject(String callUUID) async { await _callKeep.rejectCall(callUUID); } Future answer(String callUUID) async { final keeper = calls[callUUID]; if (!keeper!.connected) { await _callKeep.answerIncomingCall(callUUID); keeper.connected = true; } } Future setOnHold(String callUUID, bool held) async { await _callKeep.setOnHold(callUUID, held); setCallHeld(callUUID, held); } Future setMutedCall(String callUUID, bool muted) async { await _callKeep.setMutedCall(callUUID, muted); setCallMuted(callUUID, muted); } Future updateDisplay(String callUUID) async { final number = calls[callUUID]!.number; // Workaround because Android doesn't display well displayName, se we have to switch ... if (isIOS) { await _callKeep.updateDisplay(callUUID, displayName: 'New Name', handle: number); } else { await _callKeep.updateDisplay(callUUID, displayName: number, handle: 'New Name'); } } Future displayIncomingCall(CallSession call) async { final callUUID = newUUID(); final callKeeper = CallKeeper(this, callUUID, call.displayName!, call); addCall(callUUID, callKeeper); await _callKeep.displayIncomingCall(callUUID, call.displayName!, handleType: 'number', hasVideo: call.type == CallType.kVideo); return callKeeper; } Future checkoutPhoneAccountSetting(BuildContext context) async { await _callKeep.setup(context, { 'ios': { 'appName': appName, }, 'android': alertOptions, }); final hasPhoneAccount = await _callKeep.hasPhoneAccount(); if (!hasPhoneAccount) { await _callKeep.hasDefaultPhoneAccount(context, alertOptions); } } /// CallActions. Future answerCall(CallKeepPerformAnswerCallAction event) async { final callUUID = event.callUUID; final keeper = calls[event.callUUID]!; if (!keeper.connected) { // Answer Call keeper.call!.answer(); keeper.connected = true; } Timer(const Duration(seconds: 1), () { _callKeep.setCurrentCallActive(callUUID!); }); } Future endCall(CallKeepPerformEndCallAction event) async { final keeper = calls[event.callUUID]; keeper?.call?.hangup(); removeCall(event.callUUID!); } Future didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async { final keeper = calls[event.callUUID]!; keeper.call?.sendDTMF(event.digits!); } Future didReceiveStartCallAction( CallKeepDidReceiveStartCallAction event) async { if (event.handle == null) { // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` return; } final callUUID = event.callUUID ?? newUUID(); if (event.callUUID == null) { final call = await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo); addCall(callUUID, CallKeeper(this, callUUID, call.displayName!, call)); } await _callKeep.startCall(callUUID, event.handle!, event.handle!); Timer(const Duration(seconds: 1), () { _callKeep.setCurrentCallActive(callUUID); }); } Future didPerformSetMutedCallAction( CallKeepDidPerformSetMutedCallAction event) async { final keeper = calls[event.callUUID]!; if (event.muted ?? false) { keeper.call?.setMicrophoneMuted(true); } else { keeper.call?.setMicrophoneMuted(false); } setCallMuted(event.callUUID!, event.muted!); } Future didToggleHoldCallAction( CallKeepDidToggleHoldAction event) async { final keeper = calls[event.callUUID]!; if (event.hold ?? false) { keeper.call?.setRemoteOnHold(true); } else { keeper.call?.setRemoteOnHold(false); } setCallHeld(event.callUUID!, event.hold!); } }