another.im-ios/Monal/Classes/MLVoIPProcessor.m
2024-11-18 15:53:52 +01:00

842 lines
37 KiB
Objective-C

//
// MLVoIPProcessor.m
// Monal
//
// Created by admin on 03.07.22.
// Copyright © 2022 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLConstants.h"
#import "Monal-Swift.h"
#import "HelperTools.h"
#import "XMPPIQ.h"
#import "XMPPMessage.h"
#import "xmpp.h"
#import "MLXMPPManager.h"
#import "MLVoIPProcessor.h"
#import "MLCall.h"
#import "MonalAppDelegate.h"
#import "ActiveChatsViewController.h"
#import "MLNotificationQueue.h"
#import "secrets.h"
@import PushKit;
@import CallKit;
@import WebRTC;
static NSMutableDictionary* _pendingCalls;
@interface MLVoIPProcessor() <PKPushRegistryDelegate, CXProviderDelegate>
{
}
@property (nonatomic, strong) CXCallController* _Nullable callController;
@property (nonatomic, strong) CXProvider* _Nullable cxProvider;
@end
@interface MLCall() <WebRTCClientDelegate>
@property (nonatomic, strong) NSString* jmiid;
@property (nonatomic) MLCallDirection direction;
@property (nonatomic, strong) MLXMLNode* _Nullable jmiPropose;
@property (nonatomic, strong) MLXMLNode* _Nullable jmiProceed;
@property (nonatomic, strong) NSString* _Nullable fullRemoteJid;
@property (nonatomic, strong) WebRTCClient* _Nullable webRTCClient;
@property (nonatomic, strong) CXAnswerCallAction* _Nullable providerAnswerAction;
@property (nonatomic, assign) BOOL isConnected;
@property (nonatomic, assign) BOOL tieBreak;
@property (nonatomic, strong) AVAudioSession* _Nullable audioSession;
@property (nonatomic, assign) MLCallFinishReason finishReason;
@property (nonatomic, readonly) xmpp* account;
@property (nonatomic, readonly) MLVoIPProcessor* voipProcessor;
-(instancetype) initWithUUID:(NSUUID*) uuid jmiid:(NSString*) jmiid contact:(MLContact*) contact callType:(MLCallType) callType andDirection:(MLCallDirection) direction;
-(void) migrateTo:(MLCall*) otherCall;
-(NSString*) short;
-(void) reportRinging;
-(void) handleEndCallActionWithReason:(MLCallFinishReason) reason;
-(void) sendJmiReject;
-(void) sendJmiPropose;
-(void) sendJmiRinging;
@end
@implementation MLVoIPProcessor
+(void) initialize
{
_pendingCalls = [NSMutableDictionary new];
}
-(id) init
{
self = [super init];
CXProviderConfiguration* config = [CXProviderConfiguration new];
config.maximumCallGroups = 1;
config.maximumCallsPerCallGroup = 1;
config.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)];
config.supportsVideo = YES;
config.includesCallsInRecents = YES;
//see https://stackoverflow.com/a/45823730/3528174
#ifndef IS_QUICKSY
config.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:@"CallKitLogo"]);
#else
config.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:@"QuicksyCallKitLogo"]);
#endif
self.cxProvider = [[CXProvider alloc] initWithConfiguration:config];
[self.cxProvider setDelegate:self queue:dispatch_get_main_queue()];
self.callController = [[CXCallController alloc] initWithQueue:dispatch_get_main_queue()];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIncomingVoipCall:) name:kMonalIncomingVoipCall object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIncomingJMIStanza:) name:kMonalIncomingJMIStanza object:nil];
return self;
}
-(void) deinit
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(MLCall* _Nullable) getCallForUUID:(NSUUID*) uuid
{
@synchronized(_pendingCalls) {
return _pendingCalls[uuid];
}
}
-(void) addCall:(MLCall*) call
{
DDLogInfo(@"Adding call to list: %@", call);
@synchronized(_pendingCalls) {
_pendingCalls[call.uuid] = call;
}
[[MLNotificationQueue currentQueue] postNotificationName:kMonalCallAdded object:call userInfo:@{@"uuid": call.uuid}];
}
-(void) removeCall:(MLCall*) call
{
DDLogInfo(@"Removing call from list: %@", call);
@synchronized(_pendingCalls) {
[_pendingCalls removeObjectForKey:call.uuid];
}
[[MLNotificationQueue currentQueue] postNotificationName:kMonalCallRemoved object:call userInfo:@{@"uuid": call.uuid}];
}
-(NSUInteger) pendingCallsCount
{
return _pendingCalls.count;
}
-(NSDictionary<NSString*, MLCall*>*) getActiveCalls
{
@synchronized(_pendingCalls) {
return [_pendingCalls copy];
}
}
-(MLCall* _Nullable) getActiveCallWithContact:(MLContact*) contact
{
@synchronized(_pendingCalls) {
for(NSUUID* uuid in _pendingCalls)
{
MLCall* call = [self getCallForUUID:uuid];
if([call.contact isEqualToContact:contact])
return call;
}
}
return nil;
}
-(MLCall* _Nullable) getCallForJmiid:(NSString*) jmiid
{
@synchronized(_pendingCalls) {
for(NSUUID* uuid in _pendingCalls)
{
MLCall* call = [self getCallForUUID:uuid];
if([call.jmiid isEqualToString:jmiid])
return call;
}
}
return nil;
}
-(MLCall*) initiateCallWithType:(MLCallType) callType toContact:(MLContact*) contact
{
xmpp* account = contact.account;
MLAssert(account != nil, @"account is nil in initiateCallWithType:ToContact:!", (@{@"contact": contact}));
NSUUID* uuid = [NSUUID UUID];
MLCall* call = [[MLCall alloc] initWithUUID:uuid jmiid:uuid.UUIDString contact:contact callType:callType andDirection:MLCallDirectionOutgoing];
DDLogInfo(@"Initiating %@ call to %@: %@", (callType==MLCallTypeAudio ? @"audio" : @"video"), contact, call);
[self addCall:call];
CXHandle* handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:contact.contactJid];
CXStartCallAction* startCallAction = [[CXStartCallAction alloc] initWithCallUUID:call.uuid handle:handle];
startCallAction.contactIdentifier = call.contact.contactDisplayName;
startCallAction.video = call.callType == MLCallTypeVideo;
CXTransaction* transaction = [[CXTransaction alloc] initWithAction:startCallAction];
[self.callController requestTransaction:transaction completion:^(NSError* error) {
if(error != nil)
{
DDLogError(@"Error requesting start call transaction: %@", error);
[self removeCall:call];
}
else
DDLogInfo(@"Successfully created outgoing call transaction for CallKit..");
}];
return call;
}
-(void) voipRegistration
{
PKPushRegistry* voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
voipRegistry.delegate = self;
voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
}
// Handle updated APNS tokens
-(void) pushRegistry:(PKPushRegistry*) registry didUpdatePushCredentials:(PKPushCredentials*) credentials forType:(NSString*) type
{
NSString* token = [HelperTools stringFromToken:credentials.token];
DDLogDebug(@"Ignoring APNS voip token string for type %@: %@", type, token);
}
-(void) pushRegistry:(PKPushRegistry*) registry didInvalidatePushTokenForType:(NSString*) type
{
DDLogDebug(@"APNS voip didInvalidatePushTokenForType:%@ called and ignored...", type);
}
//called if jmi propose was received by appex
-(void) pushRegistry:(PKPushRegistry*) registry didReceiveIncomingPushWithPayload:(PKPushPayload*) payload forType:(PKPushType) type withCompletionHandler:(void (^)(void)) completion
{
DDLogInfo(@"Received voip push with payload: %@", payload);
NSDictionary* userInfo = [HelperTools unserializeData:[HelperTools dataWithBase64EncodedString:payload.dictionaryPayload[@"base64Payload"]]];
[self processIncomingCall:userInfo withCompletion:completion];
}
//called if jmi propose was received by mainapp
-(void) handleIncomingVoipCall:(NSNotification*) notification
{
DDLogInfo(@"Received JMI propose directly in mainapp...");
//handle tie breaking: check for already running call
//(this is only needed if we are in the mainapp because of an already "running" call we now have to tie break)
XMPPMessage* messageNode = notification.userInfo[@"messageNode"];
NSNumber* accountID = notification.userInfo[@"accountID"];
MLContact* contact = [MLContact createContactFromJid:messageNode.fromUser andAccountID:accountID];
MLCall* existingCall = [self getActiveCallWithContact:contact];
if(existingCall == nil || existingCall.state == MLCallStateFinished)
return [self processIncomingCall:notification.userInfo withCompletion:nil];
MLCall* newCall = [self createCallWithJmiPropose:messageNode onAccountID:accountID];
if(newCall == nil)
return;
//handle tie breaking: both parties call each other "simultaneously"
DDLogDebug(@"Found existing call, trying to break the tie: %@", existingCall);
if(existingCall.state < MLCallStateConnecting) //e.g. MLCallStateDiscovering or MLCallStateRinging
{
//determine call sort order
NSData* existingID = [[existingCall.jmiPropose findFirst:@"{urn:xmpp:jingle-message:0}propose@id"] dataUsingEncoding:NSUTF8StringEncoding];
NSData* newID = [[newCall.jmiPropose findFirst:@"{urn:xmpp:jingle-message:0}propose@id"] dataUsingEncoding:NSUTF8StringEncoding];
int result = [HelperTools compareIOcted:existingID with:newID];
if(result == 0)
result = [HelperTools compareIOcted:[existingCall.contact.contactJid dataUsingEncoding:NSUTF8StringEncoding] with:[newCall.contact.contactJid dataUsingEncoding:NSUTF8StringEncoding]];
DDLogInfo(@"Tie-breaking new incoming call: existingID=%@, newID=%@, result=%d, existingCall=%@, newCall=%@", existingID, newID, result, existingCall, newCall);
if(result <= 0) //keep existingID, reject new call having a higher ID
{
//reject new call and do nothing with the existingCall
newCall.tieBreak = YES;
[newCall handleEndCallActionWithReason:MLCallFinishReasonDeclined];
}
else //use newID, hang up existing call having a higher ID
{
//hang up existing call (retract it) and process new incoming call
existingCall.tieBreak = YES;
[existingCall end];
[self processIncomingCall:notification.userInfo withCompletion:nil];
}
return;
}
//handle tie breaking: one party migrates the call to other device
else if(existingCall.state < MLCallStateFinished) //call already running
{
if(newCall.callType == existingCall.callType)
{
DDLogInfo(@"Migrating from new call to existing call: %@", existingCall);
[existingCall migrateTo:newCall];
//drop new call after migration to make sure it does not interfere with our existing call
DDLogInfo(@"Dropping newCall '%@' in favor of migrated existingCall '%@' ...", [newCall short], [existingCall short]);
newCall = nil;
}
else
{
existingCall.tieBreak = YES; //will be ignored if call was connected, but it doesn't hurt either
[existingCall end];
[self processIncomingCall:notification.userInfo withCompletion:nil];
}
return;
}
unreachable();
}
-(void) processIncomingCall:(NSDictionary* _Nonnull) userInfo withCompletion:(void (^ _Nullable)(void)) completion
{
//TODO: handle jmi propose coming from other devices on our account (see TODO in MLMessageProcessor.m)
XMPPMessage* messageNode = userInfo[@"messageNode"];
NSNumber* accountID = userInfo[@"accountID"];
MLCall* call = [self createCallWithJmiPropose:messageNode onAccountID:accountID];
if(call == nil)
{
//ios will stop delivering voip notifications if an incoming pushkit notification doesn't trigger a visible ringing
//indication within 5 seconds (one single time is permitted, the second time we get punished indefinitely)
MLAssert(completion == nil, @"Got nil call but wakeup was via pushkit, this is dangerous!!", userInfo);
//call pushkit completion if given
if(completion != nil)
completion();
return;
}
DDLogInfo(@"Now processing new incoming call: %@", call);
//add call to pending calls list
[self addCall:call];
[self.cxProvider reportNewIncomingCallWithUUID:call.uuid update:[self constructUpdateForCall:call] completion:^(NSError *error) {
//add our completion handler to handler queue to initiate xmpp connections
//this must be done in main thread because the app delegate is only allowed in main thread
dispatch_async(dispatch_get_main_queue(), ^{
MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate incomingWakeupWithCompletionHandler:^(UIBackgroundFetchResult result __unused) {
DDLogWarn(@"VoIP push wakeup handler timed out");
}];
});
if(error != nil)
{
DDLogError(@"Call disallowed by system: %@", error);
[call sendJmiReject];
//remove this call from pending calls
[self removeCall:call];
}
else
{
DDLogDebug(@"Call reported successfully using CallKit, initializing xmpp and WebRTC now...");
//initialize webrtc class (ask for external service credentials, gather ice servers etc.) for call as soon as the callkit ui is shown
//this will be done once the app delegate started to connect our xmpp accounts above
//do this in an extra thread to not block this callback thread (could be main thread or otherwise restricted by apple)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
DDLogDebug(@"Sending jmi ringing message...");
[call sendJmiRinging];
//wait for our account to connect before initializing webrtc using XEP-0215 iq stanzas
//if the user proceeds the call before we are bound, the outgoing proceed message stanza will be queued and sent once we are bound
//outgoing iq messages are not queued in all cases (e.g. non-smacks reconnect), hence this waiting loop
while(call.account.accountState < kStateBound)
[NSThread sleepForTimeInterval:0.250];
DDLogDebug(@"Account is connected, now really initialize WebRTC...");
[self initWebRTCForPendingCall:call];
});
}
}];
//call pushkit completion if given
if(completion != nil)
completion();
}
-(void) initWebRTCForPendingCall:(MLCall*) call
{
DDLogInfo(@"Initializing WebRTC for: %@", call);
NSMutableArray* iceServers = [NSMutableArray new];
if([call.account.connectionProperties.discoveredStunTurnServers count] > 0)
{
for(NSDictionary* service in call.account.connectionProperties.discoveredStunTurnServers)
[call.account queryExternalServiceCredentialsFor:service completion:^(id data) {
//this will not include any credentials if we got an error answer for our credentials query (data will be an empty dict)
//--> just use the server without credentials then
NSMutableDictionary* serviceWithCredentials = [NSMutableDictionary dictionaryWithDictionary:service];
[serviceWithCredentials addEntriesFromDictionary:(NSDictionary*)data];
DDLogDebug(@"Got new external service credentials: %@", serviceWithCredentials);
//transport is only defined for turn/turns, but not for stun/stuns from M110 onwards
NSString* transport = @"";
if([@[@"turn", @"turns"] containsObject:serviceWithCredentials[@"type"]])
{
if(serviceWithCredentials[@"username"] == nil || serviceWithCredentials[@"password"] == nil)
{
DDLogWarn(@"Invalid turn/turns credentials: missing username or password, ignoring!");
return;
}
if(serviceWithCredentials[@"transport"] != nil)
transport = [NSString stringWithFormat:@"?transport=%@", serviceWithCredentials[@"transport"]];
}
[iceServers addObject:[[RTCIceServer alloc]
initWithURLStrings:@[[NSString stringWithFormat:@"%@:%@:%@%@",
serviceWithCredentials[@"type"],
[HelperTools isIP:serviceWithCredentials[@"host"]] ? [NSString stringWithFormat:@"[%@]", serviceWithCredentials[@"host"]] : serviceWithCredentials[@"host"],
serviceWithCredentials[@"port"],
transport
]]
username:serviceWithCredentials[@"username"]
credential:serviceWithCredentials[@"password"]
tlsCertPolicy:RTCTlsCertPolicyInsecureNoCheck
]];
//proceed only if all ice servers have been processed
if([iceServers count] == [call.account.connectionProperties.discoveredStunTurnServers count])
{
DDLogInfo(@"Done processing ICE servers, trying to connect WebRTC session...");
[self createWebRTCClientForCall:call usingICEServers:iceServers];
}
}];
}
else if([[HelperTools defaultsDB] boolForKey: @"webrtcUseFallbackTurn"])
{
DDLogInfo(@"No ICE servers detected, trying to connect WebRTC session using our own STUN servers as fallback...");
//use own stun server as fallback
[iceServers addObject:[[RTCIceServer alloc] initWithURLStrings:[HelperTools getFailoverStunServers]]];
// request turn credentials
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/new" relativeToURL:[HelperTools getFailoverTurnApiServer]]];
if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"])
urlRequest.requiresDNSSECValidation = YES;
[urlRequest setTimeoutInterval:3.0];
NSURLSession* challengeSession = [HelperTools createEphemeralURLSession];
[[challengeSession dataTaskWithRequest:urlRequest completionHandler:^(NSData* data, NSURLResponse* response, NSError* error) {
if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200)
{
DDLogWarn(@"Could not retrieve turn challenge, only using stun: %@", error);
[self createWebRTCClientForCall:call usingICEServers:iceServers];
return;
}
// parse challenge
NSError* challengeJsonErr;
NSDictionary* challenge = [NSJSONSerialization JSONObjectWithData:data options:0 error:&challengeJsonErr];
if(challengeJsonErr != nil && [challenge objectForKey:@"challenge"] != nil)
{
DDLogWarn(@"Could not parse turn challenge, only using stun: %@", challengeJsonErr);
[self createWebRTCClientForCall:call usingICEServers:iceServers];
return;
}
unsigned long validUntil = [challenge[@"validUntil"] unsignedLongValue];
NSMutableData* challengeHmacInput = [NSMutableData dataWithBytes:&validUntil length:sizeof(validUntil)];
[challengeHmacInput appendData:[challenge[@"challenge"] dataUsingEncoding:NSUTF8StringEncoding]];
// create challenge response
NSDictionary* challengeResponseDict = @{
@"challenge": challenge,
@"appId": TURN_API_SECRET_ID,
@"challengeResponse": [HelperTools encodeBase64WithData:[HelperTools sha512HmacForKey:[TURN_API_SECRET dataUsingEncoding:NSUTF8StringEncoding] andData:challengeHmacInput]],
};
NSError* challengeRespJsonErr;
NSData* challengeResp = [NSJSONSerialization dataWithJSONObject:challengeResponseDict options:kNilOptions error:&challengeRespJsonErr];
if(challengeRespJsonErr != nil)
{
DDLogWarn(@"Could not create json challenge reponse, only using stun: %@", challengeRespJsonErr);
[self createWebRTCClientForCall:call usingICEServers:iceServers];
return;
}
NSMutableURLRequest* responseRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"/api/v1/challenge/validate" relativeToURL:[HelperTools getFailoverTurnApiServer]]];
if([[HelperTools defaultsDB] boolForKey: @"useDnssecForAllConnections"])
responseRequest.requiresDNSSECValidation = YES;
[responseRequest setHTTPMethod:@"POST"];
[responseRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
[responseRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[responseRequest setValue:[NSString stringWithFormat:@"%lu", (unsigned long)[challengeResp length]] forHTTPHeaderField:@"Content-Length"];
[responseRequest setTimeoutInterval:3.0];
[responseRequest setHTTPBody:challengeResp];
NSURLSession* responseSession = [HelperTools createEphemeralURLSession];
[[responseSession dataTaskWithRequest:responseRequest completionHandler:^(NSData* turnCredentialsData, NSURLResponse* response, NSError* error) {
if(error != nil || [(NSHTTPURLResponse*)response statusCode] != 200)
{
DDLogWarn(@"Could not retrieve turn credentials, only using stun: %@", error);
[self createWebRTCClientForCall:call usingICEServers:iceServers];
return;
}
NSError* turnCredentialsErr;
NSDictionary* turnCredentials = [NSJSONSerialization JSONObjectWithData:turnCredentialsData options:0 error:&turnCredentialsErr];
if(turnCredentials == nil || turnCredentials[@"username"] == nil || turnCredentials[@"password"] == nil || turnCredentials[@"uris"] == nil)
{
DDLogWarn(@"Could not parse turn credentials, only using stun: %@", turnCredentialsErr);
[self createWebRTCClientForCall:call usingICEServers:iceServers];
return;
}
[iceServers addObject:[[RTCIceServer alloc] initWithURLStrings:[turnCredentials objectForKey:@"uris"] username:[turnCredentials objectForKey:@"username"] credential:[turnCredentials objectForKey:@"password"]]];
[self createWebRTCClientForCall:call usingICEServers:iceServers];
}] resume];
}] resume];
}
//continue without any stun/turn servers if only p2p but no stun/turn servers could be found on local xmpp server
//AND no fallback to monal servers was configured
else
{
[self createWebRTCClientForCall:call usingICEServers:@[]];
}
}
-(void) createWebRTCClientForCall:(MLCall*) call usingICEServers:(NSArray<RTCIceServer*>*) iceServers
{
BOOL forceRelay = ![[HelperTools defaultsDB] boolForKey:@"webrtcAllowP2P"];
DDLogInfo(@"Initializing webrtc with forceRelay=%@ using ice servers: %@", bool2str(forceRelay), iceServers);
MLAssert(call.webRTCClient == nil, @"Call does already have a webrtc client object!", (@{@"old_client": call.webRTCClient}));
WebRTCClient* webRTCClient = [[WebRTCClient alloc] initWithIceServers:iceServers audioOnly:call.callType==MLCallTypeAudio forceRelay:forceRelay];
call.webRTCClient = webRTCClient;
webRTCClient.delegate = call;
}
-(void) handleIncomingJMIStanza:(NSNotification*) notification
{
XMPPMessage* messageNode = notification.userInfo[@"messageNode"];
MLAssert(messageNode != nil, @"messageNode is nil in handleIncomingJMIStanza!", notification.userInfo);
xmpp* account = notification.object;
MLAssert(account != nil, @"account is nil in handleIncomingJMIStanza!", notification.userInfo);
return [self handleIncomingJMIStanza:messageNode onAccount:account];
}
-(void) handleIncomingJMIStanza:(XMPPMessage*) messageNode onAccount:(xmpp*) account
{
NSString* jmiid = [messageNode findFirst:@"{urn:xmpp:jingle-message:0}*@id"];
MLAssert(jmiid != nil, @"call jmiid invalid!", (@{@"*@id": nilWrapper([messageNode findFirst:@"{urn:xmpp:jingle-message:0}*@id"])}));
MLCall* call = [self getCallForJmiid:jmiid];
if(call == nil)
{
DDLogWarn(@"Ignoring unexpected JMI stanza for unknown call: %@", messageNode);
//TODO: log action in history db (see TODO in MLMessageProcessor.m)
return;
}
DDLogInfo(@"Got new incoming JMI stanza for call: %@", call);
if(call.direction == MLCallDirectionIncoming && [messageNode check:@"{urn:xmpp:jingle-message:0}proceed"])
{
if(![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi proceed of incoming call NOT coming from other device on our account...");
return;
}
if(call.jmiProceed != nil)
{
DDLogWarn(@"Someone tried to proceed an already proceeded incoming call, ignoring this jmi proceed!");
return;
}
[call handleEndCallActionWithReason:MLCallFinishReasonAnsweredElsewhere];
}
else if(call.direction == MLCallDirectionOutgoing && [messageNode check:@"{urn:xmpp:jingle-message:0}proceed"])
{
if(call.jmiProceed != nil)
{
DDLogWarn(@"Someone tried to proceed an already proceeded outgoing call, ignoring this jmi proceed!");
return;
}
//order matters here!
call.fullRemoteJid = messageNode.from;
call.jmiProceed = messageNode;
}
else if(call.direction == MLCallDirectionIncoming && [messageNode check:@"{urn:xmpp:jingle-message:0}ringing"])
{
if(![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi ringing of incoming call NOT coming from other device on our account...");
return;
}
DDLogWarn(@"Ignoring jmi ringing of incoming call coming from other device on our account...");
}
else if(call.direction == MLCallDirectionOutgoing && [messageNode check:@"{urn:xmpp:jingle-message:0}ringing"])
{
if([messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi ringing of outgoing call coming from other device on our account...");
return;
}
if(call.jmiPropose == nil)
{
DDLogWarn(@"Other device did try to report state ringing for a not yet proposed call, ignoring this jmi ringing!");
return;
}
[call reportRinging];
}
else if(call.direction == MLCallDirectionIncoming && [messageNode check:@"{urn:xmpp:jingle-message:0}reject"])
{
if(![messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi reject of incoming call NOT coming from other device on our account...");
return;
}
if(call.jmiProceed != nil)
{
DDLogWarn(@"Other device did try to reject already proceeded call, ignoring this jmi reject!");
return;
}
DDLogVerbose(@"Marking incoming call as rejected...");
[call handleEndCallActionWithReason:MLCallFinishReasonRejected];
}
else if(call.direction == MLCallDirectionOutgoing && [messageNode check:@"{urn:xmpp:jingle-message:0}reject"])
{
if([messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi reject of outgoing call coming from other device on our account...");
return;
}
if(call.jmiProceed != nil)
{
DDLogWarn(@"Remote did try to reject already proceeded call, ignoring this jmi reject!");
return;
}
DDLogVerbose(@"Marking outgoing call as rejected...");
[call handleEndCallActionWithReason:MLCallFinishReasonRejected];
}
else if(call.direction == MLCallDirectionIncoming && [messageNode check:@"{urn:xmpp:jingle-message:0}retract"])
{
if([messageNode.fromUser isEqualToString:account.connectionProperties.identity.jid])
{
DDLogWarn(@"Ignoring bogus jmi retract of incoming call coming from other device on our account...");
return;
}
if(call.jmiProceed != nil)
{
DDLogWarn(@"Remote did try to retract already proceeded call");
return;
}
[call handleEndCallActionWithReason:MLCallFinishReasonUnanswered];
}
//this should never happen: one cannot retract a call initiated on one device using another device
else if(call.direction == MLCallDirectionOutgoing && [messageNode check:@"{urn:xmpp:jingle-message:0}retract"])
{
DDLogError(@"This jmi retract should never happen: one cannot retract a call initiated on one device using another device!");
return;
}
else if([messageNode check:@"{urn:xmpp:jingle-message:0}finish"])
{
NSString* reason = [messageNode findFirst:@"{urn:xmpp:jingle-message:0}finish/{urn:xmpp:jingle:1}reason/*$"];
if(call.jmiProceed != nil)
{
DDLogInfo(@"Remote finished call with reason: %@", reason);
[call end]; //use "end" because this was a successful call
}
else
{
DDLogWarn(@"Remote did try to finish an not yet established call");
if([@"connectivity-error" isEqualToString:reason])
[call handleEndCallActionWithReason:MLCallFinishReasonConnectivityError];
else
[call handleEndCallActionWithReason:MLCallFinishReasonAnsweredElsewhere];
}
}
else
DDLogWarn(@"NOT handling JMI stanza, wrong jmi-type/direction combination: %@", messageNode);
}
#pragma mark - CXProvider delegate
-(void) providerDidReset:(CXProvider*) provider
{
DDLogDebug(@"CXProvider: providerDidReset with provider=%@", provider);
}
-(void) providerDidBegin:(CXProvider*) provider
{
DDLogDebug(@"CXProvider: providerDidBegin with provider=%@", provider);
}
-(void) provider:(CXProvider*) provider performStartCallAction:(CXStartCallAction*) action
{
MLCall* call = [self getCallForUUID:action.callUUID];
DDLogDebug(@"CXProvider: performStartCallAction with provider=%@, CXStartCallAction=%@, pendingCallsInfo: %@", provider, action, call);
if(call == nil)
{
DDLogWarn(@"Pending call not present anymore: %@", (@{
@"provider": provider,
@"action": action,
@"uuid": action.callUUID,
}));
[action fail];
return;
}
//update call info to include the right info
[self.cxProvider reportCallWithUUID:call.uuid updated:[self constructUpdateForCall:call]];
//propose call to contact (e.g. let it ring)
[call sendJmiPropose];
//initialize webrtc class (ask for external service credentials, gather ice servers etc.) for call as soon as the JMI propose was sent
[self initWebRTCForPendingCall:call];
[action fulfill];
}
-(void) provider:(CXProvider*) provider performAnswerCallAction:(CXAnswerCallAction*) action
{
MLCall* call = [self getCallForUUID:action.callUUID];
DDLogDebug(@"CXProvider: performAnswerCallAction with provider=%@, CXAnswerCallAction=%@, pendingCallsInfo: %@", provider, action, call);
if(call == nil)
{
DDLogWarn(@"Pending call not present anymore: %@", (@{
@"provider": provider,
@"action": action,
@"uuid": action.callUUID,
}));
[action fail];
return;
}
call.providerAnswerAction = action;
MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate.activeChats presentCall:call];
}
-(void) provider:(CXProvider*) provider performEndCallAction:(CXEndCallAction*) action
{
MLCall* call = [self getCallForUUID:action.callUUID];
DDLogDebug(@"CXProvider: performEndCallAction with provider=%@, CXEndCallAction=%@, pendingCallsInfo: %@", provider, action, call);
if(call == nil)
{
DDLogWarn(@"Pending call not present anymore: %@", (@{
@"provider": provider,
@"action": action,
@"uuid": action.callUUID,
}));
[action fail];
return;
}
//handle call termination
if(call.direction == MLCallDirectionIncoming)
{
if(call.isConnected)
[call handleEndCallActionWithReason:MLCallFinishReasonNormal];
else if(call.jmiProceed != nil)
[call handleEndCallActionWithReason:MLCallFinishReasonConnectivityError];
else
[call handleEndCallActionWithReason:MLCallFinishReasonDeclined]; //send reject
}
else
{
if(call.isConnected)
[call handleEndCallActionWithReason:MLCallFinishReasonNormal];
else if(call.jmiProceed != nil)
[call handleEndCallActionWithReason:MLCallFinishReasonConnectivityError];
else
[call handleEndCallActionWithReason:MLCallFinishReasonRetracted]; //send retract
}
[action fulfill];
}
-(void) provider:(CXProvider*) provider performSetMutedCallAction:(CXSetMutedCallAction*) action
{
MLCall* call = [self getCallForUUID:action.callUUID];
DDLogDebug(@"CXProvider: performSetMutedCallAction with provider=%@, CXSetMutedCallAction=%@, pendingCallsInfo: %@", provider, action, call);
if(call == nil)
{
DDLogWarn(@"Pending call not present anymore: %@", (@{
@"provider": provider,
@"action": action,
@"uuid": action.callUUID,
}));
[action fail];
return;
}
call.muted = action.muted;
[action fulfill];
}
-(void) provider:(CXProvider*) provider didActivateAudioSession:(AVAudioSession*) audioSession
{
DDLogDebug(@"CXProvider: didActivateAudioSession with provider=%@, audioSession=%@", provider, audioSession);
@synchronized(_pendingCalls) {
for(NSUUID* uuid in _pendingCalls)
{
MLCall* call = [self getCallForUUID:uuid];
call.audioSession = audioSession;
}
dispatch_async(dispatch_get_main_queue(), ^{
MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
DDLogVerbose(@"Setting audio state to MLAudioStateCall...");
appDelegate.audioState = MLAudioStateCall;
});
}
}
-(void) provider:(CXProvider*) provider didDeactivateAudioSession:(AVAudioSession*) audioSession
{
DDLogDebug(@"CXProvider: didDeactivateAudioSession with provider=%@, audioSession=%@", provider, audioSession);
@synchronized(_pendingCalls) {
for(NSUUID* uuid in _pendingCalls)
{
MLCall* call = [self getCallForUUID:uuid];
call.audioSession = nil;
}
//switch back to default audio settings destroyed by callkit
dispatch_async(dispatch_get_main_queue(), ^{
[HelperTools configureDefaultAudioSession];
MonalAppDelegate* appDelegate = (MonalAppDelegate*)[[UIApplication sharedApplication] delegate];
DDLogVerbose(@"Setting audio state to MLAudioStateNormal...");
appDelegate.audioState = MLAudioStateNormal;
});
}
}
-(CXCallUpdate*) constructUpdateForCall:(MLCall*) call
{
CXCallUpdate* update = [CXCallUpdate new];
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:call.contact.contactJid];
update.localizedCallerName = call.contact.contactDisplayName;
update.supportsDTMF = NO;
update.hasVideo = call.callType == MLCallTypeVideo;
update.supportsHolding = NO;
update.supportsGrouping = NO;
update.supportsUngrouping = NO;
return update;
}
-(MLCall* _Nullable) createCallWithJmiPropose:(XMPPMessage*) messageNode onAccountID:(NSNumber*) accountID
{
//if the jmi id is a uuid, just use it, otherwise infer a uuid from the given jmi id
NSUUID* uuid = [messageNode findFirst:@"{urn:xmpp:jingle-message:0}propose@id|uuidcast"];
NSString* jmiid = [messageNode findFirst:@"{urn:xmpp:jingle-message:0}propose@id"];
MLAssert(uuid != nil, @"call uuid invalid!", (@{@"propose@id": nilWrapper(jmiid)}));
//check if we are in a loop (both accounts participating in this call on the same monal instance)
//--> ignore this incoming call if that is true
if([self getCallForJmiid:jmiid] != nil)
{
DDLogWarn(@"Call loop detected, ignoring incoming call...");
return nil;
}
MLCallType callType = MLCallTypeAudio;
if([messageNode check:@"{urn:xmpp:jingle-message:0}propose/{urn:xmpp:jingle:apps:rtp:1}description<media=video>"])
callType = MLCallTypeVideo;
MLCall* call = [[MLCall alloc] initWithUUID:uuid jmiid:jmiid contact:[MLContact createContactFromJid:messageNode.fromUser andAccountID:accountID] callType:callType andDirection:MLCallDirectionIncoming];
//order matters here!
call.fullRemoteJid = messageNode.from;
call.jmiPropose = messageNode;
return call;
}
@end