diff --git a/README.md b/README.md index 7208a26..e22c5de 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# NOTE +Master branch is not the latest - check "trusted-care-master" for newest code. +Basically master had some PushWoosh stuff for Android from the guy that i forked, and there were naming conflicts for Android on the PushNotification plugin that we are using - https://github.com/phonegap/phonegap-plugin-push, therefore i reverted to the original creator last commit and made changes on top of that in the new branch. +https://github.com/AleksandarTokarev/cordova-plugin-callkit/tree/trusted-care-master + # cordova-plugin-callkit Cordova plugin that enables CallKit + PushKit (iOS) & ConnectionService (Android) functionality to display native UI. diff --git a/src/ios/CordovaCall.h b/src/ios/CordovaCall.h index dd6db70..9dca162 100644 --- a/src/ios/CordovaCall.h +++ b/src/ios/CordovaCall.h @@ -8,6 +8,7 @@ @property (nonatomic, copy) NSString *VoIPPushCallbackId; @property (nonatomic, copy) NSString *VoIPPushClassName; @property (nonatomic, copy) NSString *VoIPPushMethodName; +@property (nonatomic, copy) NSString *VoIPPushToken; - (void)init:(CDVInvokedUrlCommand*)command; diff --git a/src/ios/CordovaCall.m b/src/ios/CordovaCall.m index 9fdc9dc..1f0471b 100644 --- a/src/ios/CordovaCall.m +++ b/src/ios/CordovaCall.m @@ -6,15 +6,26 @@ @implementation CordovaCall @synthesize VoIPPushCallbackId, VoIPPushClassName, VoIPPushMethodName; -BOOL hasVideo = NO; +BOOL hasVideo = YES; NSString* appName; NSString* ringtone; NSString* icon; BOOL includeInRecents = NO; -NSMutableDictionary *callbackIds; +NSMutableDictionary *callbackIds; NSDictionary* pendingCallFromRecents; BOOL monitorAudioRouteChange = NO; BOOL enableDTMF = NO; +PKPushRegistry *_voipRegistry; + +BOOL isCancelPush = NO; +NSString* callBackUrl; +NSString* callId; + +NSMutableArray* pendingCallResponses; +NSString* const PENDING_RESPONSE_ANSWER = @"pendingResponseAnswer"; +NSString* const PENDING_RESPONSE_REJECT = @"pendingResponseReject"; + +NSString* const KEY_VOIP_PUSH_TOKEN = @"PK_deviceToken"; - (void)pluginInitialize { @@ -24,7 +35,7 @@ - (void)pluginInitialize providerConfiguration.maximumCallGroups = 1; providerConfiguration.maximumCallsPerCallGroup = 1; NSMutableSet *handleTypes = [[NSMutableSet alloc] init]; - [handleTypes addObject:@(CXHandleTypePhoneNumber)]; + [handleTypes addObject:@(CXHandleTypeGeneric)]; providerConfiguration.supportedHandleTypes = handleTypes; providerConfiguration.supportsVideo = YES; if (@available(iOS 11.0, *)) { @@ -45,10 +56,27 @@ - (void)pluginInitialize [callbackIds setObject:[NSMutableArray array] forKey:@"speakerOn"]; [callbackIds setObject:[NSMutableArray array] forKey:@"speakerOff"]; [callbackIds setObject:[NSMutableArray array] forKey:@"DTMF"]; + + // Add call response (answer or reject) to pending if event listeners are not added at the time of responding + pendingCallResponses = [NSMutableArray new]; + //allows user to make call from recents [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receiveCallFromRecents:) name:@"RecentsCallNotification" object:nil]; //detect Audio Route Changes to make speakerOn and speakerOff event handlers [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil]; + + // Initialize PKPushRegistry + //http://stackoverflow.com/questions/27245808/implement-pushkit-and-test-in-development-behavior/28562124#28562124 + dispatch_queue_t mainQueue = dispatch_get_main_queue(); + // Create a push registry object + _voipRegistry = [[PKPushRegistry alloc] initWithQueue: mainQueue]; + // Set the registry's delegate to self + [_voipRegistry setDelegate:(id _Nullable)self]; + // Set the push type to VoIP + _voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; + + // Read VoIPPushToken from UserDefaults + self.VoIPPushToken = [[NSUserDefaults standardUserDefaults] stringForKey:KEY_VOIP_PUSH_TOKEN]; } // CallKit - Interface @@ -67,7 +95,7 @@ - (void)updateProviderConfig providerConfiguration.iconTemplateImageData = iconData; } NSMutableSet *handleTypes = [[NSMutableSet alloc] init]; - [handleTypes addObject:@(CXHandleTypePhoneNumber)]; + [handleTypes addObject:@(CXHandleTypeGeneric)]; providerConfiguration.supportedHandleTypes = handleTypes; providerConfiguration.supportsVideo = hasVideo; if (@available(iOS 11.0, *)) { @@ -173,7 +201,6 @@ - (void)setVideo:(CDVInvokedUrlCommand*)command - (void)receiveCall:(CDVInvokedUrlCommand*)command { BOOL hasId = ![[command.arguments objectAtIndex:1] isEqual:[NSNull null]]; - CDVPluginResult* pluginResult = nil; NSString* callName = [command.arguments objectAtIndex:0]; NSString* callId = hasId?[command.arguments objectAtIndex:1]:callName; NSUUID *callUUID = [[NSUUID alloc] init]; @@ -184,7 +211,7 @@ - (void)receiveCall:(CDVInvokedUrlCommand*)command } if (callName != nil && [callName length] > 0) { - CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callId]; + CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:callId]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; callUpdate.remoteHandle = handle; callUpdate.hasVideo = hasVideo; @@ -193,19 +220,26 @@ - (void)receiveCall:(CDVInvokedUrlCommand*)command callUpdate.supportsUngrouping = NO; callUpdate.supportsHolding = NO; callUpdate.supportsDTMF = enableDTMF; - - [self.provider reportNewIncomingCallWithUUID:callUUID update:callUpdate completion:^(NSError * _Nullable error) { - if(error == nil) { - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Incoming call successful"] callbackId:command.callbackId]; - } else { - [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; + if (!isCancelPush) { + [self.provider reportNewIncomingCallWithUUID:callUUID update:callUpdate completion:^(NSError * _Nullable error) { + if(error == nil) { + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Incoming call successful"] callbackId:command.callbackId]; + } else { + [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:[error localizedDescription]] callbackId:command.callbackId]; + } + }]; + for (id callbackId in callbackIds[@"receiveCall"]) { + CDVPluginResult* pluginResult = nil; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"receiveCall event called successfully"]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } - }]; - for (id callbackId in callbackIds[@"receiveCall"]) { - CDVPluginResult* pluginResult = nil; - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"receiveCall event called successfully"]; - [pluginResult setKeepCallbackAsBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; + } else { + NSArray *calls = self.callController.callObserver.calls; + if([calls count] == 1) { + [self.provider reportCallWithUUID:calls[0].UUID endedAtDate:nil reason:CXCallEndedReasonRemoteEnded]; + } + } } else { [self.commandDelegate sendPluginResult:[CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Caller id can't be empty"] callbackId:command.callbackId]; @@ -225,7 +259,7 @@ - (void)sendCall:(CDVInvokedUrlCommand*)command } if (callName != nil && [callName length] > 0) { - CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callId]; + CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:callId]; CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:callUUID handle:handle]; startCallAction.contactIdentifier = callName; startCallAction.video = hasVideo; @@ -263,7 +297,6 @@ - (void)endCall:(CDVInvokedUrlCommand*)command NSArray *calls = self.callController.callObserver.calls; if([calls count] == 1) { - //[self.provider reportCallWithUUID:calls[0].UUID endedAtDate:nil reason:CXCallEndedReasonRemoteEnded]; CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:calls[0].UUID]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; [self.callController requestTransaction:transaction completion:^(NSError * _Nullable error) { @@ -293,6 +326,16 @@ - (void)registerEvent:(CDVInvokedUrlCommand*)command [pluginResult setKeepCallbackAsBool:YES]; [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; } + + // In case of registerEvent answer or reject called after responding to call, trigger cordova event for the appropriate answer + if ([eventName isEqualToString:@"answer"] && [pendingCallResponses containsObject:PENDING_RESPONSE_ANSWER]) { + [self triggerCordovaEventForCallResponse:@"answer"]; + [pendingCallResponses removeObject:PENDING_RESPONSE_ANSWER]; + } + if ([eventName isEqualToString:@"reject"] && [pendingCallResponses containsObject:PENDING_RESPONSE_REJECT]) { + [self triggerCordovaEventForCallResponse:@"reject"]; + [pendingCallResponses removeObject:PENDING_RESPONSE_REJECT]; + } } - (void)mute:(CDVInvokedUrlCommand*)command @@ -389,7 +432,7 @@ - (void)receiveCallFromRecents:(NSNotification *) notification NSString* callID = notification.object[@"callId"]; NSString* callName = notification.object[@"callName"]; NSUUID *callUUID = [[NSUUID alloc] init]; - CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:callID]; + CXHandle *handle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:callID]; CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:callUUID handle:handle]; startCallAction.video = [notification.object[@"isVideo"] boolValue]?YES:NO; startCallAction.contactIdentifier = callName; @@ -477,11 +520,22 @@ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAct { [self setupAudioSession]; [action fulfill]; - for (id callbackId in callbackIds[@"answer"]) { - CDVPluginResult* pluginResult = nil; - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"answer event called successfully"]; - [pluginResult setKeepCallbackAsBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; + + // Notify Webhook that Native Call has been Answered + NSURL *statusUpdateUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@?id=%@&input=%@", callBackUrl, callId, @"pickup"]]; + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithURL:statusUpdateUrl + completionHandler:^(NSData *statusUpdateData, + NSURLResponse *statusUpdateResponse, + NSError *statusUpdateError) { + // handle response + }] resume]; + + if ([callbackIds[@"answer"] count] == 0) { + // callbackId for event not registered, add to pending to trigger on registration + [pendingCallResponses addObject:PENDING_RESPONSE_ANSWER]; + } else { + [self triggerCordovaEventForCallResponse:@"answer"]; } //[action fail]; } @@ -498,11 +552,23 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *) [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } } else { - for (id callbackId in callbackIds[@"reject"]) { - CDVPluginResult* pluginResult = nil; - pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"reject event called successfully"]; - [pluginResult setKeepCallbackAsBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; + // Notify Webhook that Native Call has been Declined + if (!isCancelPush) { + NSURL *statusUpdateUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@?id=%@&input=%@", callBackUrl, callId, @"declined_callee"]]; + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithURL:statusUpdateUrl + completionHandler:^(NSData *statusUpdateData, + NSURLResponse *statusUpdateResponse, + NSError *statusUpdateError) { + // handle response + }] resume]; + } + + if ([callbackIds[@"reject"] count] == 0) { + // callbackId for event not registered, add to pending to trigger on registration + [pendingCallResponses addObject:PENDING_RESPONSE_REJECT]; + } else { + [self triggerCordovaEventForCallResponse:@"reject"]; } } } @@ -511,6 +577,24 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *) //[action fail]; } +- (void)triggerCordovaEventForCallResponse:(NSString*) response { + if ([response isEqualToString:@"answer"]) { + for (id callbackId in callbackIds[@"answer"]) { + CDVPluginResult* pluginResult = nil; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"answer event called successfully"]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; + } + } else if ([response isEqualToString:@"reject"]) { + for (id callbackId in callbackIds[@"reject"]) { + CDVPluginResult* pluginResult = nil; + pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"reject event called successfully"]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; + } + } +} + - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { [action fulfill]; @@ -536,20 +620,31 @@ - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCal [self.commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } } - // PushKit - (void)init:(CDVInvokedUrlCommand*)command { - self.VoIPPushCallbackId = command.callbackId; - NSLog(@"[objC] callbackId: %@", self.VoIPPushCallbackId); + self.VoIPPushCallbackId = command.callbackId; + NSLog(@"[objC] callbackId: %@", self.VoIPPushCallbackId); + + [self sendTokenPluginResult]; +} + +- (void)sendTokenPluginResult { + if (!self.VoIPPushCallbackId || !self.VoIPPushToken) { + return; + } - //http://stackoverflow.com/questions/27245808/implement-pushkit-and-test-in-development-behavior/28562124#28562124 - PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; - pushRegistry.delegate = self; - pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; + NSMutableDictionary* results = [NSMutableDictionary dictionaryWithCapacity:2]; + [results setObject:self.VoIPPushToken forKey:@"deviceToken"]; + [results setObject:@"true" forKey:@"registration"]; + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:results]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; } -- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type{ +#define PushKit Delegate Methods +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(PKPushType)type{ if([credentials.token length] == 0) { NSLog(@"[objC] No device token!"); return; @@ -558,21 +653,17 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush //http://stackoverflow.com/a/9372848/534755 NSLog(@"[objC] Device token: %@", credentials.token); const unsigned *tokenBytes = [credentials.token bytes]; - NSString *sToken = [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", + self.VoIPPushToken = [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])]; - - NSMutableDictionary* results = [NSMutableDictionary dictionaryWithCapacity:2]; - [results setObject:sToken forKey:@"deviceToken"]; - [results setObject:@"true" forKey:@"registration"]; - CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:results]; - [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; //[pluginResult setKeepCallbackAsBool:YES]; - [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; + // Store VoIPPushToken in UserDefaults + [[NSUserDefaults standardUserDefaults] setObject:self.VoIPPushToken forKey:KEY_VOIP_PUSH_TOKEN]; + + [self sendTokenPluginResult]; } - -- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { NSDictionary *payloadDict = payload.dictionaryPayload[@"aps"]; NSLog(@"[objC] didReceiveIncomingPushWithPayload: %@", payloadDict); @@ -580,32 +671,58 @@ - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayloa NSString *message = payloadDict[@"alert"]; NSLog(@"[objC] received VoIP message: %@", message); - NSString *data = payload.dictionaryPayload[@"data"]; + NSDictionary *data = payload.dictionaryPayload[@"data"]; NSLog(@"[objC] received data: %@", data); NSMutableDictionary* results = [NSMutableDictionary dictionaryWithCapacity:2]; [results setObject:message forKey:@"function"]; - [results setObject:data forKey:@"extra"]; + [results setObject:@"" forKey:@"extra"]; + + NSObject* caller = [data objectForKey:@"Caller"]; + NSArray* args = [NSArray arrayWithObjects:[caller valueForKey:@"Username"], [caller valueForKey:@"ConnectionId"], nil]; + CDVInvokedUrlCommand* newCommand = [[CDVInvokedUrlCommand alloc] initWithArguments:args callbackId:@"" className:self.VoIPPushClassName methodName:self.VoIPPushMethodName]; + // Store URL and Call Id so they can be used for call Answer/Reject + callBackUrl = [caller valueForKey:@"CallbackUrl"]; + callId = [caller valueForKey:@"ConnectionId"]; + if ([[caller valueForKey:@"CancelPush"] isEqualToString:@"true"]) { + isCancelPush = YES; + } else { + isCancelPush = NO; + } + if (!isCancelPush) { + // Notify Webhook that VOIP Push Has been received and app is started + NSURL *statusUpdateUrl = [NSURL URLWithString:[NSString stringWithFormat:@"%@?id=%@&input=%@", callBackUrl, callId, @"connected"]]; + NSURLSession *session = [NSURLSession sharedSession]; + [[session dataTaskWithURL:statusUpdateUrl + completionHandler:^(NSData *statusUpdateData, + NSURLResponse *statusUpdateResponse, + NSError *statusUpdateError) { + // handle response + }] resume]; + } + + [self receiveCall:newCommand]; @try { - NSError* error; - NSDictionary* json = [NSJSONSerialization JSONObjectWithData:[data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&error]; - - NSObject* caller = [json objectForKey:@"Caller"]; - NSArray* args = [NSArray arrayWithObjects:[caller valueForKey:@"Username"], [caller valueForKey:@"ConnectionId"], nil]; + NSError * err; + NSData * jsonData = [NSJSONSerialization dataWithJSONObject:data options:0 error:&err]; + NSString * dataString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [results setObject:dataString forKey:@"extra"]; - CDVInvokedUrlCommand* newCommand = [[CDVInvokedUrlCommand alloc] initWithArguments:args callbackId:@"" className:self.VoIPPushClassName methodName:self.VoIPPushMethodName]; - [self receiveCall:newCommand]; } @catch (NSException *exception) { - NSLog(@"[objC] error: %@", exception.reason); + NSLog(@"[objC] error: %@", exception.reason); + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason]; + [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; + return; } @finally { CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:results]; [pluginResult setKeepCallback:[NSNumber numberWithBool:YES]]; [self.commandDelegate sendPluginResult:pluginResult callbackId:self.VoIPPushCallbackId]; + completion(); } } - @end