@ -1,42 +1,44 @@
import ' dart:async ' ;
import ' package:flutter/foundation.dart ' ;
import ' package:flutter/material.dart ' ;
import ' package:callkeep/callkeep.dart ' ;
import ' package:flutter_foreground_task/flutter_foreground_task.dart ' ;
import ' package:flutter_gen/gen_l10n/l10n.dart ' ;
import ' package:matrix/matrix.dart ' ;
import ' package:uuid/uuid.dart ' ;
import ' package: permission_handler/permission_handler .dart' ;
import ' package:fluffychat/utils/voip_plugin.dart ' ;
class CallKeeper {
CallKeeper ( this . callKeepManager , this . uuid, this . number , this . call) {
call ? . onCallStateChanged . stream . listen ( _handleCallState ) ;
CallKeeper ( this . callKeepManager , this . call) {
call . onCallStateChanged . stream . listen ( _handleCallState ) ;
}
CallKeepManager callKeepManager ;
String number ;
String uuid ;
bool held = false ;
bool muted = false ;
bool ? held = false ;
bool ? muted = false ;
bool connected = false ;
CallSession ? call ;
CallSession call ;
/ / update native caller to show what remote user has done .
void _handleCallState ( CallState state ) {
Logs ( ) . v ( ' CallKeepManager::handleCallState: ${ state . toString ( ) } ' ) ;
Logs ( ) . i ( ' CallKeepManager::handleCallState: ${ state . toString ( ) } ' ) ;
switch ( state ) {
case CallState . kConnecting:
Logs ( ) . v ( ' callkeep connecting ' ) ;
break ;
case CallState . kConnected:
Logs ( ) . v ( ' callkeep connected ' ) ;
if ( ! connected ) {
callKeepManager . answer ( uui d) ;
callKeepManager . answer ( call. callI d) ;
} else {
callKeepManager . setMutedCall ( uui d, false ) ;
callKeepManager . setOnHold ( uui d, false ) ;
callKeepManager . setMutedCall ( call. callI d, false ) ;
callKeepManager . setOnHold ( call. callI d, false ) ;
}
break ;
case CallState . kEnded:
callKeepManager . hangup ( uui d) ;
callKeepManager . hangup ( call. callI d) ;
break ;
/ * TODO:
case CallState . kMuted:
@ -68,6 +70,8 @@ class CallKeeper {
}
}
Map < String ? , CallKeeper > calls = < String ? , CallKeeper > { } ;
class CallKeepManager {
factory CallKeepManager ( ) {
return _instance ;
@ -81,32 +85,29 @@ class CallKeepManager {
late FlutterCallkeep _callKeep ;
VoipPlugin ? _voipPlugin ;
Map < String , CallKeeper > calls = < String , CallKeeper > { } ;
String newUUID ( ) = > const Uuid ( ) . v4 ( ) ;
String get appName = > ' Famedly ' ;
String get appName = > ' FluffyChat ' ;
Future < bool > get hasPhoneAccountEnabled async = >
await _callKeep . hasPhoneAccount ( ) ;
Map < String , dynamic > get alertOptions = > < String , dynamic > {
' alertTitle ' : ' Permissions required ' ,
' alertDescription ' : ' $ appName needs to access your phone accounts! ' ,
' alertDescription ' :
' Allow $ appName to register as a calling account? This will allow calls to be handled by the native android dialer. ' ,
' cancelButton ' : ' Cancel ' ,
' okButton ' : ' ok ' ,
/ / Required to get audio in background when using Android 11
' foregroundService ' : {
' channelId ' : ' com.f amedly.talk ' ,
' channelId ' : ' com.f luffy.fluffychat ' ,
' channelName ' : ' Foreground service for my app ' ,
' notificationTitle ' : ' $ appName is running on background ' ,
' notificationIcon ' : ' mipmap/ic_notification_launcher ' ,
} ,
' additionalPermissions ' : [ ' ' ] ,
} ;
void setVoipPlugin ( VoipPlugin plugin ) {
if ( kIsWeb ) {
throw ' Not support callkeep for flutter web ' ;
}
_voipPlugin = plugin ;
_voipPlugin ! . onIncomingCall = ( CallSession call ) async {
bool setupDone = false ;
Future < void > showCallkitIncoming ( CallSession call ) async {
if ( ! setupDone ) {
await _callKeep . setup (
null ,
< String , dynamic > {
@ -116,47 +117,38 @@ class CallKeepManager {
' android ' : alertOptions ,
} ,
backgroundMode: true ) ;
await displayIncomingCall ( call ) ;
call . onCallStateChanged . stream . listen ( ( state ) {
if ( state = = CallState . kEnded ) {
_callKeep . endAllCalls ( ) ;
}
} ) ;
call . onCallEventChanged . stream . listen ( ( event ) {
}
setupDone = 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 ) {
void removeCall ( String ? callUUID ) {
calls . remove ( callUUID ) ;
}
void addCall ( String callUUID , CallKeeper callKeeper ) {
void addCall ( String ? callUUID , CallKeeper callKeeper ) {
if ( calls . containsKey ( callUUID ) ) return ;
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 ) {
void setCallHeld ( String ? callUUID , bool ? held ) {
calls [ callUUID ] ! . held = held ;
}
void setCallMuted ( String callUUID , bool muted ) {
void setCallMuted ( String ? callUUID , bool ? muted ) {
calls [ callUUID ] ! . muted = muted ;
}
@ -164,7 +156,7 @@ class CallKeepManager {
final callUUID = event . callUUID ;
final number = event . handle ;
Logs ( ) . v ( ' [displayIncomingCall] $ callUUID number: $ number ' ) ;
addCall ( callUUID ! , CallKeeper ( this , callUUID , number ! , null ) ) ;
/ / addCall ( callUUID , CallKeeper ( this null ) ) ;
}
void onPushKitToken ( CallKeepPushKitToken event ) {
@ -182,6 +174,7 @@ class CallKeepManager {
_callKeep . on ( CallKeepPerformEndCallAction ( ) , endCall ) ;
_callKeep . on ( CallKeepPushKitToken ( ) , onPushKitToken ) ;
_callKeep . on ( CallKeepDidDisplayIncomingCall ( ) , didDisplayIncomingCall ) ;
Logs ( ) . i ( ' [VOIP] Initialized ' ) ;
}
Future < void > hangup ( String callUUID ) async {
@ -193,10 +186,10 @@ class CallKeepManager {
await _callKeep . rejectCall ( callUUID ) ;
}
Future < void > answer ( String callUUID ) async {
final keeper = calls [ callUUID ] ;
if ( ! keeper ! . connected ) {
await _callKeep . answerIncomingCall ( callUUID ) ;
Future < void > answer ( String ? callUUID ) async {
final keeper = calls [ callUUID ] ! ;
if ( ! keeper . connected ) {
await _callKeep . answerIncomingCall ( callUUID ! ) ;
keeper . connected = true ;
}
}
@ -212,27 +205,66 @@ class CallKeepManager {
}
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 ) ;
displayName: ' New Name ' , handle: callUUID ) ;
} else {
await _callKeep . updateDisplay ( callUUID ,
displayName: number , handle: ' New Name ' ) ;
displayName: callUUID , 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 ) ;
final callKeeper = CallKeeper ( this , call ) ;
addCall ( call . callId , callKeeper ) ;
await _callKeep . displayIncomingCall (
call . callId ,
' ${ call . displayName ! } (FluffyChat) ' ,
localizedCallerName: ' ${ call . displayName ! } (FluffyChat) ' ,
handleType: ' number ' ,
hasVideo: call . type = = CallType . kVideo ,
) ;
return callKeeper ;
}
Future < void > checkoutPhoneAccountSetting ( BuildContext context ) async {
showDialog (
context: context ,
barrierDismissible: true ,
useRootNavigator: false ,
builder: ( _ ) = > AlertDialog (
title: Text ( L10n . of ( context ) ! . callingPermissions ) ,
content: Column (
mainAxisSize: MainAxisSize . min ,
children: [
ListTile (
onTap: ( ) = > openCallingAccountsPage ( context ) ,
title: Text ( L10n . of ( context ) ! . callingAccount ) ,
subtitle: Text ( L10n . of ( context ) ! . callingAccountDetails ) ,
trailing: const Icon ( Icons . phone ) ,
) ,
const Divider ( ) ,
ListTile (
onTap: ( ) = >
FlutterForegroundTask . openSystemAlertWindowSettings ( true ) ,
title: Text ( L10n . of ( context ) ! . appearOnTop ) ,
subtitle: Text ( L10n . of ( context ) ! . appearOnTopDetails ) ,
trailing: const Icon ( Icons . file_upload_rounded ) ,
) ,
const Divider ( ) ,
ListTile (
onTap: ( ) = > openAppSettings ( ) ,
title: Text ( L10n . of ( context ) ! . otherCallingPermissions ) ,
trailing: const Icon ( Icons . mic ) ,
) ,
] ,
) ,
) ,
) ;
}
void openCallingAccountsPage ( BuildContext context ) async {
await _callKeep . setup ( context , < String , dynamic > {
' ios ' : < String , dynamic > {
' appName ' : appName ,
@ -240,8 +272,11 @@ class CallKeepManager {
' android ' : alertOptions ,
} ) ;
final hasPhoneAccount = await _callKeep . hasPhoneAccount ( ) ;
Logs ( ) . e ( hasPhoneAccount . toString ( ) ) ;
if ( ! hasPhoneAccount ) {
await _callKeep . hasDefaultPhoneAccount ( context , alertOptions ) ;
} else {
await _callKeep . openPhoneAccounts ( ) ;
}
}
@ -250,8 +285,9 @@ class CallKeepManager {
final callUUID = event . callUUID ;
final keeper = calls [ event . callUUID ] ! ;
if ( ! keeper . connected ) {
Logs ( ) . e ( ' answered ' ) ;
/ / Answer Call
keeper . call ! . answer ( ) ;
keeper . call . answer ( ) ;
keeper . connected = true ;
}
Timer ( const Duration ( seconds: 1 ) , ( ) {
@ -261,13 +297,13 @@ class CallKeepManager {
Future < void > endCall ( CallKeepPerformEndCallAction event ) async {
final keeper = calls [ event . callUUID ] ;
keeper ? . call ? . hangup ( ) ;
removeCall ( event . callUUID ! ) ;
keeper ? . call . hangup ( ) ;
removeCall ( event . callUUID ) ;
}
Future < void > didPerformDTMFAction ( CallKeepDidPerformDTMFAction event ) async {
final keeper = calls [ event . callUUID ] ! ;
keeper . call ? . sendDTMF ( event . digits ! ) ;
keeper . call . sendDTMF ( event . digits ! ) ;
}
Future < void > didReceiveStartCallAction (
@ -276,11 +312,11 @@ class CallKeepManager {
/ / @ TODO: sometime we receive ` didReceiveStartCallAction ` with handle ` undefined `
return ;
}
final callUUID = event . callUUID ? ? newUUID ( ) ;
final callUUID = event . callUUID ! ;
if ( event . callUUID = = null ) {
final call =
await _voipPlugin ! . voip . inviteToCall ( event . handle ! , CallType . kVideo ) ;
addCall ( callUUID , CallKeeper ( this , call UUID, call . displayName ! , call ) ) ;
addCall ( callUUID , CallKeeper ( this , call ) ) ;
}
await _callKeep . startCall ( callUUID , event . handle ! , event . handle ! ) ;
Timer ( const Duration ( seconds: 1 ) , ( ) {
@ -290,23 +326,23 @@ class CallKeepManager {
Future < void > didPerformSetMutedCallAction (
CallKeepDidPerformSetMutedCallAction event ) async {
final keeper = calls [ event . callUUID ] ! ;
if ( event . muted ? ? false ) {
keeper . call ? . setMicrophoneMuted ( true ) ;
final keeper = calls [ event . callUUID ] ;
if ( event . muted ! ) {
keeper ! . call . setMicrophoneMuted ( true ) ;
} else {
keeper . call ? . setMicrophoneMuted ( false ) ;
keeper ! . call . setMicrophoneMuted ( false ) ;
}
setCallMuted ( event . callUUID ! , event . muted ! ) ;
setCallMuted ( event . callUUID , event . muted ) ;
}
Future < void > didToggleHoldCallAction (
CallKeepDidToggleHoldAction event ) async {
final keeper = calls [ event . callUUID ] ! ;
if ( event . hold ? ? false ) {
keeper . call ? . setRemoteOnHold ( true ) ;
final keeper = calls [ event . callUUID ] ;
if ( event . hold ! ) {
keeper ! . call . setRemoteOnHold ( true ) ;
} else {
keeper . call ? . setRemoteOnHold ( false ) ;
keeper ! . call . setRemoteOnHold ( false ) ;
}
setCallHeld ( event . callUUID ! , event . hold ! ) ;
setCallHeld ( event . callUUID , event . hold ) ;
}
}