mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-11-03 22:07:23 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			313 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
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.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<String, CallKeeper> calls = <String, CallKeeper>{};
 | 
						|
 | 
						|
  String newUUID() => const Uuid().v4();
 | 
						|
 | 
						|
  String get appName => 'Famedly';
 | 
						|
 | 
						|
  Map<String, dynamic> get alertOptions => <String, dynamic>{
 | 
						|
        '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,
 | 
						|
          <String, dynamic>{
 | 
						|
            'ios': <String, dynamic>{
 | 
						|
              'appName': appName,
 | 
						|
            },
 | 
						|
            'android': alertOptions,
 | 
						|
          },
 | 
						|
          backgroundMode: true);
 | 
						|
 | 
						|
      await displayIncomingCall(call);
 | 
						|
 | 
						|
      call.onCallStateChanged.listen((state) {
 | 
						|
        if (state == CallState.kEnded) {
 | 
						|
          _callKeep.endAllCalls();
 | 
						|
        }
 | 
						|
      });
 | 
						|
      call.onCallEventChanged.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<void> 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<void> hangup(String callUUID) async {
 | 
						|
    await _callKeep.endCall(callUUID);
 | 
						|
    removeCall(callUUID);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> reject(String callUUID) async {
 | 
						|
    await _callKeep.rejectCall(callUUID);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> answer(String callUUID) async {
 | 
						|
    final keeper = calls[callUUID];
 | 
						|
    if (!keeper!.connected) {
 | 
						|
      await _callKeep.answerIncomingCall(callUUID);
 | 
						|
      keeper.connected = true;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> setOnHold(String callUUID, bool held) async {
 | 
						|
    await _callKeep.setOnHold(callUUID, held);
 | 
						|
    setCallHeld(callUUID, held);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> setMutedCall(String callUUID, bool muted) async {
 | 
						|
    await _callKeep.setMutedCall(callUUID, muted);
 | 
						|
    setCallMuted(callUUID, muted);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> 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<CallKeeper> 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<void> checkoutPhoneAccountSetting(BuildContext context) async {
 | 
						|
    await _callKeep.setup(context, <String, dynamic>{
 | 
						|
      'ios': <String, dynamic>{
 | 
						|
        'appName': appName,
 | 
						|
      },
 | 
						|
      'android': alertOptions,
 | 
						|
    });
 | 
						|
    final hasPhoneAccount = await _callKeep.hasPhoneAccount();
 | 
						|
    if (!hasPhoneAccount) {
 | 
						|
      await _callKeep.hasDefaultPhoneAccount(context, alertOptions);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /// CallActions.
 | 
						|
  Future<void> 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<void> endCall(CallKeepPerformEndCallAction event) async {
 | 
						|
    final keeper = calls[event.callUUID];
 | 
						|
    keeper?.call?.hangup();
 | 
						|
    removeCall(event.callUUID!);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> didPerformDTMFAction(CallKeepDidPerformDTMFAction event) async {
 | 
						|
    final keeper = calls[event.callUUID]!;
 | 
						|
    keeper.call?.sendDTMF(event.digits!);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> 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<void> 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<void> 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!);
 | 
						|
  }
 | 
						|
}
 |