Skip to content
This repository was archived by the owner on Jan 18, 2020. It is now read-only.

Commit ff81c39

Browse files
Make quickstart capable of handling multiple calls or call invites (#190)
* Make quickstart capable of handling multiple calls or call invites
1 parent a212493 commit ff81c39

File tree

2 files changed

+78
-69
lines changed

2 files changed

+78
-69
lines changed

ObjcVoiceQuickstart/Base.lproj/Main.storyboard

+9-13
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
3-
<device id="retina4_7" orientation="portrait">
4-
<adaptation id="fullscreen"/>
5-
</device>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
3+
<device id="retina4_7" orientation="portrait" appearance="light"/>
64
<dependencies>
75
<deployment identifier="iOS"/>
8-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
9-
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15510"/>
107
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
118
</dependencies>
129
<scenes>
@@ -23,30 +20,29 @@
2320
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
2421
<subviews>
2522
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="TwilioLogo.png" translatesAutoresizingMaskIntoConstraints="NO" id="Yv2-Ab-jqI">
26-
<rect key="frame" x="67" y="115" width="240" height="240"/>
23+
<rect key="frame" x="67.5" y="115" width="240" height="240"/>
2724
<constraints>
2825
<constraint firstAttribute="width" constant="240" id="Ro1-3D-zN6"/>
2926
<constraint firstAttribute="width" secondItem="Yv2-Ab-jqI" secondAttribute="height" multiplier="1:1" id="d66-dM-6YS"/>
3027
</constraints>
3128
</imageView>
3229
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="KbF-3Z-6Zb">
33-
<rect key="frame" x="67" y="379" width="240" height="30"/>
30+
<rect key="frame" x="67.5" y="379" width="240" height="34"/>
3431
<constraints>
3532
<constraint firstAttribute="width" constant="240" id="2Q5-Ia-8o4"/>
3633
</constraints>
37-
<nil key="textColor"/>
3834
<fontDescription key="fontDescription" type="system" pointSize="14"/>
3935
<textInputTraits key="textInputTraits"/>
4036
</textField>
4137
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="V5R-Ky-YRW">
42-
<rect key="frame" x="172" y="465" width="30" height="30"/>
38+
<rect key="frame" x="172.5" y="469" width="30" height="30"/>
4339
<state key="normal" title="Call"/>
4440
<connections>
45-
<action selector="placeCall:" destination="BYZ-38-t0r" eventType="touchUpInside" id="Zc5-8L-bqN"/>
41+
<action selector="mainButtonPressed:" destination="BYZ-38-t0r" eventType="touchUpInside" id="2fj-Y6-Nwr"/>
4642
</connections>
4743
</button>
4844
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LCr-Ji-S0e">
49-
<rect key="frame" x="67" y="503" width="240" height="88"/>
45+
<rect key="frame" x="67.5" y="507" width="240" height="88"/>
5046
<subviews>
5147
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="viR-NY-T2b">
5248
<rect key="frame" x="58" y="8" width="49" height="31"/>
@@ -84,7 +80,7 @@
8480
</constraints>
8581
</view>
8682
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Dial a client name or phone number. Leaving the field empty results in an automated response." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="r25-bA-TjD">
87-
<rect key="frame" x="59" y="417" width="256" height="40"/>
83+
<rect key="frame" x="59.5" y="421" width="256" height="40"/>
8884
<constraints>
8985
<constraint firstAttribute="height" constant="40" id="vAY-mk-e02"/>
9086
<constraint firstAttribute="width" constant="256" id="waL-BM-doB"/>

ObjcVoiceQuickstart/ViewController.m

+69-56
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,13 @@ @interface ViewController () <PKPushRegistryDelegate, TVONotificationDelegate, T
2424

2525
@property (nonatomic, strong) PKPushRegistry *voipRegistry;
2626
@property (nonatomic, strong) void(^incomingPushCompletionCallback)(void);
27-
@property (nonatomic, strong) TVOCallInvite *callInvite;
28-
@property (nonatomic, strong) TVOCall *call;
2927
@property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL);
3028
@property (nonatomic, strong) TVODefaultAudioDevice *audioDevice;
29+
@property (nonatomic, strong) NSMutableDictionary *activeCallInvites;
30+
@property (nonatomic, strong) NSMutableDictionary *activeCalls;
31+
32+
// activeCall represents the last connected call
33+
@property (nonatomic, strong) TVOCall *activeCall;
3134

3235
@property (nonatomic, strong) CXProvider *callKitProvider;
3336
@property (nonatomic, strong) CXCallController *callKitCallController;
@@ -65,6 +68,9 @@ - (void)viewDidLoad {
6568
*/
6669
self.audioDevice = [TVODefaultAudioDevice audioDevice];
6770
TwilioVoice.audioDevice = self.audioDevice;
71+
72+
self.activeCallInvites = [NSMutableDictionary dictionary];
73+
self.activeCalls = [NSMutableDictionary dictionary];
6874
}
6975

7076
- (void)configureCallKit {
@@ -100,10 +106,10 @@ - (NSString *)fetchAccessToken {
100106
return accessToken;
101107
}
102108

103-
- (IBAction)placeCall:(id)sender {
104-
if (self.call && self.call.state == TVOCallStateConnected) {
109+
- (IBAction)mainButtonPressed:(id)sender {
110+
if (self.activeCall != nil) {
105111
self.userInitiatedDisconnect = YES;
106-
[self performEndCallActionWithUUID:self.call.uuid];
112+
[self performEndCallActionWithUUID:self.activeCall.uuid];
107113
[self toggleUIState:NO showCallControl:NO];
108114
} else {
109115
NSUUID *uuid = [NSUUID UUID];
@@ -189,7 +195,10 @@ - (void)toggleUIState:(BOOL)isEnabled showCallControl:(BOOL)showCallControl {
189195
}
190196

191197
- (IBAction)muteSwitchToggled:(UISwitch *)sender {
192-
self.call.muted = sender.on;
198+
// The sample app supports toggling mute from app UI only on the last connected call.
199+
if (self.activeCall != nil) {
200+
self.activeCall.muted = sender.on;
201+
}
193202
}
194203

195204
- (IBAction)speakerSwitchToggled:(UISwitch *)sender {
@@ -311,28 +320,18 @@ - (void)callInviteReceived:(TVOCallInvite *)callInvite {
311320
*/
312321

313322
NSLog(@"callInviteReceived:");
314-
315-
if (self.callInvite) {
316-
NSLog(@"A CallInvite is already in progress. Ignoring the incoming CallInvite from %@", callInvite.from);
317-
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
318-
[self incomingPushHandled];
319-
}
320-
return;
321-
} else if (self.call) {
322-
NSLog(@"Already an active call. Ignoring the incoming CallInvite from %@", callInvite.from);
323-
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
324-
[self incomingPushHandled];
325-
}
326-
return;
327-
}
328-
329-
self.callInvite = callInvite;
330323

331324
NSString *from = @"Voice Bot";
332325
if (callInvite.from) {
333326
from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""];
334327
}
328+
329+
// Always report to CallKit
335330
[self reportIncomingCallFrom:from withUUID:callInvite.uuid];
331+
self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite;
332+
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
333+
[self incomingPushHandled];
334+
}
336335
}
337336

338337
- (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error {
@@ -345,15 +344,17 @@ - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvit
345344

346345
NSLog(@"cancelledCallInviteReceived:");
347346

348-
if (!self.callInvite ||
349-
![self.callInvite.callSid isEqualToString:cancelledCallInvite.callSid]) {
350-
NSLog(@"No matching pending CallInvite. Ignoring the Cancelled CallInvite");
351-
return;
347+
TVOCallInvite *callInvite;
348+
for (TVOCallInvite *invite in self.activeCallInvites) {
349+
if ([cancelledCallInvite.callSid isEqualToString:invite.callSid]) {
350+
callInvite = invite;
351+
break;
352+
}
353+
}
354+
355+
if (callInvite) {
356+
[self performEndCallActionWithUUID:callInvite.uuid];
352357
}
353-
354-
[self performEndCallActionWithUUID:self.callInvite.uuid];
355-
356-
self.callInvite = nil;
357358
}
358359

359360
#pragma mark - TVOCallDelegate
@@ -366,9 +367,7 @@ - (void)callDidStartRinging:(TVOCall *)call {
366367
- (void)callDidConnect:(TVOCall *)call {
367368
NSLog(@"callDidConnect:");
368369

369-
self.call = call;
370370
self.callKitCompletionCallback(YES);
371-
self.callKitCompletionCallback = nil;
372371

373372
[self.placeCallButton setTitle:@"Hang Up" forState:UIControlStateNormal];
374373

@@ -396,7 +395,7 @@ - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error {
396395

397396
self.callKitCompletionCallback(NO);
398397
[self performEndCallActionWithUUID:call.uuid];
399-
[self callDisconnected];
398+
[self callDisconnected:call];
400399
}
401400

402401
- (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error {
@@ -415,12 +414,15 @@ - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error {
415414
[self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason];
416415
}
417416

418-
[self callDisconnected];
417+
[self callDisconnected:call];
419418
}
420419

421-
- (void)callDisconnected {
422-
self.call = nil;
423-
self.callKitCompletionCallback = nil;
420+
- (void)callDisconnected:(TVOCall *)call {
421+
if ([call isEqual:self.activeCall]) {
422+
self.activeCall = nil;
423+
}
424+
[self.activeCalls removeObjectForKey:call.uuid.UUIDString];
425+
424426
self.userInitiatedDisconnect = NO;
425427

426428
[self stopSpin];
@@ -533,8 +535,6 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio
533535

534536
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
535537
NSLog(@"provider:performAnswerCallAction:");
536-
537-
NSAssert([self.callInvite.uuid isEqual:action.callUUID], @"We only support one Invite at a time.");
538538

539539
self.audioDevice.enabled = NO;
540540
self.audioDevice.block();
@@ -552,21 +552,27 @@ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAct
552552

553553
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
554554
NSLog(@"provider:performEndCallAction:");
555-
556-
if (self.callInvite) {
557-
[self.callInvite reject];
558-
self.callInvite = nil;
559-
} else if (self.call) {
560-
[self.call disconnect];
555+
556+
TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString];
557+
TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
558+
559+
if (callInvite) {
560+
[callInvite reject];
561+
[self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString];
562+
} else if (call) {
563+
[call disconnect];
564+
} else {
565+
NSLog(@"Unknown UUID to perform end-call action with");
561566
}
562567

563568
self.audioDevice.enabled = YES;
564569
[action fulfill];
565570
}
566571

567572
- (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action {
568-
if (self.call && self.call.state == TVOCallStateConnected) {
569-
[self.call setOnHold:action.isOnHold];
573+
TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
574+
if (call && call.state == TVOCallStateConnected) {
575+
[call setOnHold:action.isOnHold];
570576
[action fulfill];
571577
} else {
572578
[action fail];
@@ -646,27 +652,34 @@ - (void)performVoiceCallWithUUID:(NSUUID *)uuid
646652
builder.params = @{kTwimlParamTo: strongSelf.outgoingValue.text};
647653
builder.uuid = uuid;
648654
}];
649-
self.call = [TwilioVoice connectWithOptions:connectOptions delegate:self];
655+
TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self];
656+
if (call) {
657+
self.activeCall = call;
658+
self.activeCalls[call.uuid.UUIDString] = call;
659+
}
650660
self.callKitCompletionCallback = completionHandler;
651661
}
652662

653663
- (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid
654664
completion:(void(^)(BOOL success))completionHandler {
655-
__weak typeof(self) weakSelf = self;
656-
TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:self.callInvite block:^(TVOAcceptOptionsBuilder *builder) {
657-
__strong typeof(self) strongSelf = weakSelf;
658-
builder.uuid = strongSelf.callInvite.uuid;
665+
TVOCallInvite *callInvite = self.activeCallInvites[uuid.UUIDString];
666+
NSAssert(callInvite, @"No CallInvite matches the UUID");
667+
668+
TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:callInvite block:^(TVOAcceptOptionsBuilder *builder) {
669+
builder.uuid = callInvite.uuid;
659670
}];
660671

661-
self.call = [self.callInvite acceptWithOptions:acceptOptions delegate:self];
672+
TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self];
662673

663-
if (!self.call) {
674+
if (!call) {
664675
completionHandler(NO);
665676
} else {
666677
self.callKitCompletionCallback = completionHandler;
678+
self.activeCall = call;
679+
self.activeCalls[call.uuid.UUIDString] = call;
667680
}
668-
669-
self.callInvite = nil;
681+
682+
[self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString];
670683

671684
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
672685
[self incomingPushHandled];

0 commit comments

Comments
 (0)