diff --git a/README.md b/README.md index 071fd5bc..396df292 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK - Android 2.1.0 (bundled within this library) -- iOS 2.1.0 (specified by the app's own podfile) +- iOS 5.1.1 (specified by the app's own podfile; min. version 5.x) ## Breaking changes in v4.0.0 @@ -63,11 +63,11 @@ Edit your `Podfile` to include TwilioVoice framework source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work -platform :ios, '8.1' +platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 2.1.0' + pod 'TwilioVoice', '~> 5.1.1' ... end @@ -82,11 +82,11 @@ Edit your `Podfile` to include TwilioVoice and RNTwilioVoice frameworks source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work -platform :ios, '8.1' +platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 2.1.0' + pod 'TwilioVoice', '~> 5.1.1' pod 'RNTwilioVoice', path: '../node_modules/react-native-twilio-programmable-voice' ... end @@ -291,10 +291,6 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // } }) -// iOS Only -TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) - -// Android Only TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // { // call_sid: string, // Twilio call sid @@ -303,6 +299,10 @@ TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // call_to: string, // "client:bob" // } }) + +// iOS Only +TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) + // Android Only TwilioVoice.addEventListener('proximity', function(data) { // { diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 9b80fac6..ba68eea1 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -9,12 +9,12 @@ Pod::Spec.new do |s| s.authors = spec['author']['name'] s.homepage = spec['homepage'] s.license = spec['license'] - s.platform = :ios, "8.1" + s.platform = :ios, "10.0" - s.source_files = [ "ios/RNTwilioVoice/*.h", "ios/RNTwilioVoice/*.m"] + s.source_files = [ "ios/RNTwilioVoice/RNTwilioVoice.h", "ios/RNTwilioVoice/RNTwilioVoice.m"] s.source = {:path => "./RNTwilioVoice"} - s.dependency 'React' - s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice' } - s.frameworks = 'TwilioVoice' -end \ No newline at end of file + s.dependency "React" + s.xcconfig = { "FRAMEWORK_SEARCH_PATHS" => "\"${PODS_ROOT}/TwilioVoice/Build/iOS\"" } + s.frameworks = "TwilioVoice" +end diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 7fc54618..74aa1724 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -14,11 +14,19 @@ @interface RNTwilioVoice () *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady"]; + return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming"]; } @synthesize bridge = _bridge; @@ -70,6 +78,7 @@ - (void)dealloc { RCT_EXPORT_METHOD(configureCallKit: (NSDictionary *)params) { if (self.callKitCallController == nil) { + [self initRNTwilioVoice]; _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; configuration.maximumCallGroups = 1; @@ -93,13 +102,11 @@ - (void)dealloc { RCT_EXPORT_METHOD(connect: (NSDictionary *)params) { NSLog(@"Calling phone number %@", [params valueForKey:@"To"]); -// [TwilioVoice setLogLevel:TVOLogLevelVerbose]; - UIDevice* device = [UIDevice currentDevice]; device.proximityMonitoringEnabled = YES; - if (self.call && self.call.state == TVOCallStateConnected) { - [self performEndCallActionWithUUID:self.call.uuid]; + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { + [self performEndCallActionWithUUID:self.activeCall.uuid]; } else { NSUUID *uuid = [NSUUID UUID]; NSString *handle = [params valueForKey:@"To"]; @@ -110,22 +117,23 @@ - (void)dealloc { RCT_EXPORT_METHOD(disconnect) { NSLog(@"Disconnecting call"); - [self performEndCallActionWithUUID:self.call.uuid]; + self.userInitiatedDisconnect = YES; + [self performEndCallActionWithUUID:self.activeCall.uuid]; } RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) { NSLog(@"Mute/UnMute call"); - self.call.muted = muted; + self.activeCall.muted = muted ? YES : NO; } RCT_EXPORT_METHOD(setSpeakerPhone: (BOOL *)speaker) { - [self toggleAudioRoute:speaker]; + [self toggleAudioRoute: speaker ? YES : NO]; } RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { - if (self.call && self.call.state == TVOCallStateConnected) { + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { NSLog(@"SendDigits %@", digits); - [self.call sendDigits:digits]; + [self.activeCall sendDigits:digits]; } } @@ -150,39 +158,34 @@ - (void)dealloc { resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite) { - if (self.callInvite.callSid) { - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; + if (self.activeCallInvites.count) { + TVOCallInvite *callInvite = [self.activeCallInvites valueForKey:[self.activeCallInvites allKeys][self.activeCallInvites.count-1]]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.from) { - [params setObject:self.callInvite.from forKey:@"from"]; + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; } - if (self.callInvite.to) { - [params setObject:self.callInvite.to forKey:@"to"]; - } - if (self.callInvite.state == TVOCallInviteStatePending) { - [params setObject:StatePending forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; } + [params setObject:StatePending forKey:@"call_state"]; resolve(params); - } else if (self.call) { - if (self.call.sid) { - [params setObject:self.call.sid forKey:@"call_sid"]; + } else if (self.activeCall) { + if (self.activeCall.sid) { + [params setObject:self.activeCall.sid forKey:@"call_sid"]; } - if (self.call.to) { - [params setObject:self.call.to forKey:@"call_to"]; + if (self.activeCall.to) { + [params setObject:self.activeCall.to forKey:@"call_to"]; } - if (self.call.from) { - [params setObject:self.call.from forKey:@"call_from"]; + if (self.activeCall.from) { + [params setObject:self.activeCall.from forKey:@"call_from"]; } - if (self.call.state == TVOCallStateConnected) { + if (self.activeCall.state == TVOCallStateConnected) { [params setObject:StateConnected forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateConnecting) { + } else if (self.activeCall.state == TVOCallStateConnecting) { [params setObject:StateConnecting forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateDisconnected) { + } else if (self.activeCall.state == TVOCallStateDisconnected) { [params setObject:StateDisconnected forKey:@"call_state"]; } resolve(params); @@ -191,6 +194,19 @@ - (void)dealloc { } } +- (void)initRNTwilioVoice { + /* + * The important thing to remember when providing a TVOAudioDevice is that the device must be set + * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call). + * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set. + */ + self.audioDevice = [TVODefaultAudioDevice audioDevice]; + TwilioVoice.audioDevice = self.audioDevice; + + self.activeCallInvites = [NSMutableDictionary dictionary]; + self.activeCalls = [NSMutableDictionary dictionary]; +} + - (void)initPushRegistry { self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; self.voipRegistry.delegate = self; @@ -210,113 +226,165 @@ - (NSString *)fetchAccessToken { #pragma mark - PKPushRegistryDelegate - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type { - NSLog(@"pushRegistry:didUpdatePushCredentials:forType"); - - if ([type isEqualToString:PKPushTypeVoIP]) { - const unsigned *tokenBytes = [credentials.token bytes]; - self.deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>", - ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), - ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), - ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; - - NSString *accessToken = [self fetchAccessToken]; - - [TwilioVoice registerWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError *error) { - if (error) { - NSLog(@"An error occurred while registering: %@", [error localizedDescription]); - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setObject:[error localizedDescription] forKey:@"err"]; - - [self sendEventWithName:@"deviceNotReady" body:params]; - } else { - NSLog(@"Successfully registered for VoIP push notifications."); - [self sendEventWithName:@"deviceReady" body:nil]; - } - }]; - } + NSLog(@"pushRegistry:didUpdatePushCredentials:forType:"); + + if ([type isEqualToString:PKPushTypeVoIP]) { + const unsigned *tokenBytes = [credentials.token bytes]; + self.deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>", + ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), + ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), + ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; + NSString *accessToken = [self fetchAccessToken]; + + [TwilioVoice registerWithAccessToken:accessToken + deviceToken:self.deviceTokenString + completion:^(NSError *error) { + if (error) { + NSLog(@"An error occurred while registering: %@", [error localizedDescription]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + [params setObject:[error localizedDescription] forKey:@"err"]; + + [self sendEventWithName:@"deviceNotReady" body:params]; + } else { + NSLog(@"Successfully registered for VoIP push notifications."); + [self sendEventWithName:@"deviceReady" body:nil]; + } + }]; + } } - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { - NSLog(@"pushRegistry:didInvalidatePushTokenForType"); - - if ([type isEqualToString:PKPushTypeVoIP]) { - NSString *accessToken = [self fetchAccessToken]; - - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError * _Nullable error) { - if (error) { - NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); - } else { - NSLog(@"Successfully unregistered for VoIP push notifications."); - } - }]; - - self.deviceTokenString = nil; - } + NSLog(@"pushRegistry:didInvalidatePushTokenForType:"); + + if ([type isEqualToString:PKPushTypeVoIP]) { + NSString *accessToken = [self fetchAccessToken]; + + [TwilioVoice unregisterWithAccessToken:accessToken + deviceToken:self.deviceTokenString + completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); + } else { + NSLog(@"Successfully unregistered for VoIP push notifications."); + } + }]; + + self.deviceTokenString = nil; + } } -- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { - NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); +- (void)pushRegistry:(PKPushRegistry *)registry +didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type +withCompletionHandler:(void (^)(void))completion { + NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:"); - if ([type isEqualToString:PKPushTypeVoIP]) { - [TwilioVoice handleNotification:payload.dictionaryPayload - delegate:self]; - } + if ([type isEqualToString:PKPushTypeVoIP]) { + // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed + if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue:nil]) { + NSLog(@"This is not a valid Twilio Voice notification."); + } + } + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + // Save for later when the notification is properly handled. + self.incomingPushCompletionCallback = completion; + } else { + /** + * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to + * CallKit and fulfill the completion before exiting this callback method. + */ + completion(); + } +} + +- (void)incomingPushHandled { + if (self.incomingPushCompletionCallback) { + self.incomingPushCompletionCallback(); + self.incomingPushCompletionCallback = nil; + } } #pragma mark - TVONotificationDelegate - (void)callInviteReceived:(TVOCallInvite *)callInvite { - if (callInvite.state == TVOCallInviteStatePending) { - [self handleCallInviteReceived:callInvite]; - } else if (callInvite.state == TVOCallInviteStateCanceled) { - [self handleCallInviteCanceled:callInvite]; - } -} - -- (void)handleCallInviteReceived:(TVOCallInvite *)callInvite { - NSLog(@"callInviteReceived:"); - if (self.callInvite && self.callInvite == TVOCallInviteStatePending) { - NSLog(@"Already a pending incoming call invite."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; - } else if (self.call) { - NSLog(@"Already an active call."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; - } - self.callInvite = callInvite; + /** + * Calling `[TwilioVoice handleNotification:delegate:]` will synchronously process your notification payload and + * provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback. + */ + + NSLog(@"callInviteReceived:"); + + NSString *from = @"Unknown"; + if (callInvite.from) { + from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""]; + } - [self reportIncomingCallFrom:callInvite.from withUUID:callInvite.uuid]; + // Always report to CallKit + [self reportIncomingCallFrom:from withUUID:callInvite.uuid]; + self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite; + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + + [params setObject:StatePending forKey:@"call_state"]; + + [self sendEventWithName:@"deviceDidReceiveIncoming" body:params]; } -- (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { - NSLog(@"callInviteCanceled"); +- (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error { - [self performEndCallActionWithUUID:callInvite.uuid]; + /** + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite.callSid) { - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; - } + NSLog(@"cancelledCallInviteReceived:"); - if (self.callInvite.from) { - [params setObject:self.callInvite.from forKey:@"call_from"]; - } - if (self.callInvite.to) { - [params setObject:self.callInvite.to forKey:@"call_to"]; - } - if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; + TVOCallInvite *callInvite; + for (NSString *activeCallInviteId in self.activeCallInvites) { + TVOCallInvite *activeCallInvite = [self.activeCallInvites objectForKey:activeCallInviteId]; + if ([cancelledCallInvite.callSid isEqualToString:activeCallInvite.callSid]) { + callInvite = activeCallInvite; + break; + } + } + + if (callInvite) { + [self performEndCallActionWithUUID:callInvite.uuid]; + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } - self.callInvite = nil; + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + + if (error.code == TVOErrorCallCancelledError) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } else { + [params setObject:StateRejected forKey:@"call_state"]; + } + + [self sendEventWithName:@"connectionDidDisconnect" body:params]; + } } - (void)notificationError:(NSError *)error { @@ -325,92 +393,112 @@ - (void)notificationError:(NSError *)error { #pragma mark - TVOCallDelegate - (void)callDidConnect:(TVOCall *)call { - self.call = call; - self.callKitCompletionCallback(YES); - self.callKitCompletionCallback = nil; - - NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; - [callParams setObject:call.sid forKey:@"call_sid"]; - if (call.state == TVOCallStateConnecting) { - [callParams setObject:StateConnecting forKey:@"call_state"]; - } else if (call.state == TVOCallStateConnected) { - [callParams setObject:StateConnected forKey:@"call_state"]; - } + NSLog(@"callDidConnect:"); - if (call.from) { - [callParams setObject:call.from forKey:@"call_from"]; - } - if (call.to) { - [callParams setObject:call.to forKey:@"call_to"]; - } - [self sendEventWithName:@"connectionDidConnect" body:callParams]; + self.callKitCompletionCallback(YES); + + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.state == TVOCallStateConnecting) { + [callParams setObject:StateConnecting forKey:@"call_state"]; + } else if (call.state == TVOCallStateConnected) { + [callParams setObject:StateConnected forKey:@"call_state"]; + } + + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionDidConnect" body:callParams]; } - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { - NSLog(@"Call failed to connect: %@", error); + NSLog(@"Call failed to connect: %@", error); - self.callKitCompletionCallback(NO); - [self performEndCallActionWithUUID:call.uuid]; - [self callDisconnected:error]; + self.callKitCompletionCallback(NO); + [self performEndCallActionWithUUID:call.uuid]; + [self call:call disconnectedWithError:error]; } - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { - NSLog(@"Call disconnected with error: %@", error); + if (error) { + NSLog(@"Call failed: %@", error); + } else { + NSLog(@"Call disconnected"); + } - [self callDisconnected:error]; -} + if (!self.userInitiatedDisconnect) { + CXCallEndedReason reason = CXCallEndedReasonRemoteEnded; + if (error) { + reason = CXCallEndedReasonFailed; + } -- (void)callDisconnected:(NSError *)error { - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (error) { - NSString* errMsg = [error localizedDescription]; - if (error.localizedFailureReason) { - errMsg = [error localizedFailureReason]; + [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason]; } - [params setObject:errMsg forKey:@"err"]; - } - if (self.call.sid) { - [params setObject:self.call.sid forKey:@"call_sid"]; - } - if (self.call.to) { - [params setObject:self.call.to forKey:@"call_to"]; - } - if (self.call.from) { - [params setObject:self.call.from forKey:@"call_from"]; - } - if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; - self.call = nil; - self.callKitCompletionCallback = nil; + [self call:call disconnectedWithError:error]; } -#pragma mark - AVAudioSession -- (void)toggleAudioRoute: (BOOL *)toSpeaker { - // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. - // Use port override to switch the route. - NSError *error = nil; - NSLog(@"toggleAudioRoute"); +- (void)call:(TVOCall *)call disconnectedWithError:(NSError *)error { + if ([call isEqual:self.activeCall]) { + self.activeCall = nil; + } + [self.activeCalls removeObjectForKey:call.uuid.UUIDString]; + + self.userInitiatedDisconnect = NO; - if (toSpeaker) { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (error) { + NSString* errMsg = [error localizedDescription]; + if (error.localizedFailureReason) { + errMsg = [error localizedFailureReason]; + } + [params setObject:errMsg forKey:@"err"]; } - } else { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + if (call.sid) { + [params setObject:call.sid forKey:@"call_sid"]; } - } + if (call.to) { + [params setObject:call.to forKey:@"call_to"]; + } + if (call.from) { + [params setObject:call.from forKey:@"call_from"]; + } + if (call.state == TVOCallStateDisconnected) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } + [self sendEventWithName:@"connectionDidDisconnect" body:params]; +} + +#pragma mark - AVAudioSession +- (void)toggleAudioRoute:(BOOL)toSpeaker { + // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. Use port override to switch the route. + self.audioDevice.block = ^ { + // We will execute `kDefaultAVAudioSessionConfigurationBlock` first. + kTVODefaultAVAudioSessionConfigurationBlock(); + + // Overwrite the audio route + AVAudioSession *session = [AVAudioSession sharedInstance]; + NSError *error = nil; + if (toSpeaker) { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } else { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } + }; + self.audioDevice.block(); } #pragma mark - CXProviderDelegate - (void)providerDidReset:(CXProvider *)provider { - NSLog(@"providerDidReset"); - TwilioVoice.audioEnabled = YES; + NSLog(@"providerDidReset:"); + self.audioDevice.enabled = YES; } - (void)providerDidBegin:(CXProvider *)provider { @@ -418,13 +506,12 @@ - (void)providerDidBegin:(CXProvider *)provider { } - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession { - NSLog(@"provider:didActivateAudioSession"); - TwilioVoice.audioEnabled = YES; + NSLog(@"provider:didActivateAudioSession:"); + self.audioDevice.enabled = YES; } - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession { - NSLog(@"provider:didDeactivateAudioSession"); - TwilioVoice.audioEnabled = NO; + NSLog(@"provider:didDeactivateAudioSession:"); } - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action { @@ -432,170 +519,197 @@ - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)act } - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action { - NSLog(@"provider:performStartCallAction"); + NSLog(@"provider:performStartCallAction:"); - [TwilioVoice configureAudioSession]; - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; + self.audioDevice.block(); - [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; + [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; - __weak typeof(self) weakSelf = self; - [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) { - __strong typeof(self) strongSelf = weakSelf; - if (success) { - [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]]; - [action fulfill]; - } else { - [action fail]; - } - }]; + __weak typeof(self) weakSelf = self; + [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) { + __strong typeof(self) strongSelf = weakSelf; + if (success) { + [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]]; + [action fulfill]; + } else { + [action fail]; + } + }]; } - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { - NSLog(@"provider:performAnswerCallAction"); - - // RCP: Workaround from https://forums.developer.apple.com/message/169511 suggests configuring audio in the - // completion block of the `reportNewIncomingCallWithUUID:update:completion:` method instead of in - // `provider:performAnswerCallAction:` per the WWDC examples. - // [TwilioVoice configureAudioSession]; + NSLog(@"provider:performAnswerCallAction:"); - NSAssert([self.callInvite.uuid isEqual:action.callUUID], @"We only support one Invite at a time."); + self.audioDevice.enabled = NO; + self.audioDevice.block(); - TwilioVoice.audioEnabled = NO; - [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { - if (success) { - [action fulfill]; - } else { - [action fail]; - } - }]; + [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { + if (success) { + [action fulfill]; + } else { + [action fail]; + } + }]; - [action fulfill]; + [action fulfill]; } - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { - NSLog(@"provider:performEndCallAction"); + NSLog(@"provider:performEndCallAction:"); - TwilioVoice.audioEnabled = NO; + TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString]; + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; - if (self.callInvite && self.callInvite.state == TVOCallInviteStatePending) { - [self sendEventWithName:@"callRejected" body:@"callRejected"]; - [self.callInvite reject]; - self.callInvite = nil; - } else if (self.call) { - [self.call disconnect]; - } + if (callInvite) { + [self sendEventWithName:@"callRejected" body:@"callRejected"]; + [callInvite reject]; + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + } else if (call) { + [call disconnect]; + } else { + NSLog(@"Unknown UUID to perform end-call action with"); + } - [action fulfill]; + self.audioDevice.enabled = YES; + [action fulfill]; } - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action { - if (self.call && self.call.state == TVOCallStateConnected) { - [self.call setOnHold:action.isOnHold]; - [action fulfill]; - } else { - [action fail]; - } + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setOnHold:action.isOnHold]; + [action fulfill]; + } else { + [action fail]; + } } -#pragma mark - CallKit Actions -- (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle { - if (uuid == nil || handle == nil) { - return; - } - - CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; - CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; - CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; - - [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { - if (error) { - NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]); +- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setMuted:action.isMuted]; + [action fulfill]; } else { - NSLog(@"StartCallAction transaction request successful"); - - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.remoteHandle = callHandle; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; - callUpdate.hasVideo = NO; - - [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate]; + [action fail]; } - }]; } -- (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid { - CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:from]; - - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.remoteHandle = callHandle; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; - callUpdate.hasVideo = NO; - - [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { - if (!error) { - NSLog(@"Incoming call successfully reported"); - - // RCP: Workaround per https://forums.developer.apple.com/message/169511 - [TwilioVoice configureAudioSession]; - } else { - NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); +#pragma mark - CallKit Actions +- (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle { + if (uuid == nil || handle == nil) { + return; } - }]; + + CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; + CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; + + [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { + if (error) { + NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]); + } else { + NSLog(@"StartCallAction transaction request successful"); + + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.remoteHandle = callHandle; + callUpdate.supportsDTMF = YES; + callUpdate.supportsHolding = YES; + callUpdate.supportsGrouping = NO; + callUpdate.supportsUngrouping = NO; + callUpdate.hasVideo = NO; + + [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate]; + } + }]; +} + +- (void)reportIncomingCallFrom:(NSString *) from withUUID:(NSUUID *)uuid { + CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:from]; + + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.remoteHandle = callHandle; + callUpdate.supportsDTMF = YES; + callUpdate.supportsHolding = YES; + callUpdate.supportsGrouping = NO; + callUpdate.supportsUngrouping = NO; + callUpdate.hasVideo = NO; + + [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { + if (!error) { + NSLog(@"Incoming call successfully reported."); + } else { + NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); + } + }]; } - (void)performEndCallActionWithUUID:(NSUUID *)uuid { - if (uuid == nil) { - return; - } + UIDevice* device = [UIDevice currentDevice]; + device.proximityMonitoringEnabled = NO; - UIDevice* device = [UIDevice currentDevice]; - device.proximityMonitoringEnabled = NO; - - CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; - CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; + CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; - [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { - if (error) { - NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]); - } else { - NSLog(@"EndCallAction transaction request successful"); - } - }]; + [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { + if (error) { + NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]); + } + else { + NSLog(@"EndCallAction transaction request successful"); + } + }]; } - (void)performVoiceCallWithUUID:(NSUUID *)uuid client:(NSString *)client completion:(void(^)(BOOL success))completionHandler { - - self.call = [TwilioVoice call:[self fetchAccessToken] - params:_callParams - uuid:uuid - delegate:self]; - + __weak typeof(self) weakSelf = self; + TVOConnectOptions *connectOptions = [TVOConnectOptions optionsWithAccessToken:[self fetchAccessToken] block:^(TVOConnectOptionsBuilder *builder) { + __strong typeof(self) strongSelf = weakSelf; + builder.params = strongSelf->_callParams; + builder.uuid = uuid; + }]; + TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self]; + if (call) { + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } self.callKitCompletionCallback = completionHandler; } - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid completion:(void(^)(BOOL success))completionHandler { + TVOCallInvite *callInvite = self.activeCallInvites[uuid.UUIDString]; + NSAssert(callInvite, @"No CallInvite matches the UUID"); - self.call = [self.callInvite acceptWithDelegate:self]; - self.callInvite = nil; - self.callKitCompletionCallback = completionHandler; + TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:callInvite block:^(TVOAcceptOptionsBuilder *builder) { + builder.uuid = callInvite.uuid; + }]; + + TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self]; + + if (!call) { + completionHandler(NO); + } else { + self.callKitCompletionCallback = completionHandler; + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } + + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } } - (void)handleAppTerminateNotification { NSLog(@"handleAppTerminateNotification called"); - if (self.call) { + if (self.activeCall) { NSLog(@"handleAppTerminateNotification disconnecting an active call"); - [self.call disconnect]; + [self.activeCall disconnect]; } }