[iOS] Proxy CallKit API to be a feature of the SDK

With this the RN component and the consumer app can share same CallKit
provider, configuration, and enable to be part of multiple listeners of
the CallKit flow events. The main driver of this is to enable the
consumer app to be able to report an incoming call to the OS before
loading the JitsiMeetView. Once the user answers the call, the app can
instantiate a JitsiMeetView, pass the CallKit call UUIID, and the Jitsi
Meet components will handle the connection and report back to CallKit
that the  call has been established.
This commit is contained in:
Daniel Ornelas 2018-04-18 10:11:56 -05:00 committed by Lyubo Marinov
parent 520bb8bd22
commit e5309a6482
7 changed files with 427 additions and 126 deletions

View File

@ -38,6 +38,9 @@
B386B85D20981A75000DEF7A /* Invite.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85620981A75000DEF7A /* Invite.m */; }; B386B85D20981A75000DEF7A /* Invite.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85620981A75000DEF7A /* Invite.m */; };
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; }; C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; }; C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
C69EFA0C209A0F660027712B /* JMCallKitNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */; };
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */; };
C69EFA0E209A0F660027712B /* JMCallKitEventListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; }; C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; }; C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; }; C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
@ -83,6 +86,9 @@
B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = "<group>"; }; B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = "<group>"; };
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; }; C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; }; C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitNotifier.swift; sourceTree = "<group>"; };
C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitProxy.swift; sourceTree = "<group>"; };
C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitEventListener.swift; sourceTree = "<group>"; };
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; }; C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; }; C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
@ -138,6 +144,7 @@
0BD906E71EC0C00300C8C18E /* src */ = { 0BD906E71EC0C00300C8C18E /* src */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C69EFA02209A0EFD0027712B /* callkit */,
B386B84F20981A11000DEF7A /* invite */, B386B84F20981A11000DEF7A /* invite */,
C6A3426B204F127900E062DD /* picture-in-picture */, C6A3426B204F127900E062DD /* picture-in-picture */,
0BCA495C1EC4B6C600B793EE /* AudioMode.m */, 0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
@ -198,6 +205,16 @@
name = Pods; name = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C69EFA02209A0EFD0027712B /* callkit */ = {
isa = PBXGroup;
children = (
C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */,
C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */,
C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */,
);
path = callkit;
sourceTree = "<group>";
};
C6A3426B204F127900E062DD /* picture-in-picture */ = { C6A3426B204F127900E062DD /* picture-in-picture */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -390,7 +407,10 @@
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C69EFA0C209A0F660027712B /* JMCallKitNotifier.swift in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */,
C69EFA0E209A0F660027712B /* JMCallKitEventListener.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */, 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -27,6 +27,8 @@
#import <React/RCTEventEmitter.h> #import <React/RCTEventEmitter.h>
#import <React/RCTUtils.h> #import <React/RCTUtils.h>
#import <JitsiMeet/JitsiMeet-Swift.h>
// The events emitted/supported by RNCallKit: // The events emitted/supported by RNCallKit:
static NSString * const RNCallKitPerformAnswerCallAction static NSString * const RNCallKitPerformAnswerCallAction
= @"performAnswerCallAction"; = @"performAnswerCallAction";
@ -37,14 +39,10 @@ static NSString * const RNCallKitPerformSetMutedCallAction
static NSString * const RNCallKitProviderDidReset static NSString * const RNCallKitProviderDidReset
= @"providerDidReset"; = @"providerDidReset";
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate> @interface RNCallKit : RCTEventEmitter <JMCallKitEventListener>
@end @end
@implementation RNCallKit @implementation RNCallKit
{
CXCallController *_callController;
CXProvider *_provider;
}
RCT_EXTERN void RCTRegisterModule(Class); RCT_EXTERN void RCTRegisterModule(Class);
@ -70,35 +68,8 @@ RCT_EXTERN void RCTRegisterModule(Class);
]; ];
} }
// Display the incoming call to the user - (void)dealloc {
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID [JMCallKitProxy removeListener:self];
handle:(NSString *)handle
hasVideo:(BOOL)hasVideo
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
#ifdef DEBUG
NSLog(@"[RNCallKit][displayIncomingCall] callUUID = %@", callUUID);
#endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = hasVideo;
[self.provider reportNewIncomingCallWithUUID:callUUID_
update:callUpdate
completion:^(NSError * _Nullable error) {
if (error) {
reject(nil, @"Error reporting new incoming call", error);
} else {
resolve(nil);
}
}];
} }
// End call // End call
@ -141,14 +112,12 @@ RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
dictionary); dictionary);
#endif #endif
CXProviderConfiguration *configuration if (![JMCallKitProxy hasProviderBeenConfigurated]) {
= [self providerConfigurationFromDictionary:dictionary]; [self configureProviderFromDictionary:dictionary];
if (_provider) {
_provider.configuration = configuration;
} else {
_provider = [[CXProvider alloc] initWithConfiguration:configuration];
[_provider setDelegate:self queue:nil];
} }
// register to receive CallKit proxy events
[JMCallKitProxy addListener: self];
} }
// Start outgoing call // Start outgoing call
@ -162,6 +131,15 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
#endif #endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
// Don't start a call action if there's
// an active call for this UUID
// (i.e. JitsiMeetView was configured from an incoming call
if ([JMCallKitProxy hasActiveCallForUUID:callUUID]) {
resolve(nil);
return;
}
CXHandle *handle_ CXHandle *handle_
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
CXStartCallAction *action CXStartCallAction *action
@ -177,9 +155,9 @@ RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) { reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
[self.provider reportCallWithUUID:callUUID_ [JMCallKitProxy reportCallWith:callUUID_
endedAtDate:[NSDate date] endedAt:nil
reason:CXCallEndedReasonFailed]; reason:CXCallEndedReasonFailed];
resolve(nil); resolve(nil);
} }
@ -188,8 +166,8 @@ RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) { reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
[self.provider reportOutgoingCallWithUUID:callUUID_ [JMCallKitProxy reportOutgoingCallWith:callUUID_
connectedAtDate:[NSDate date]]; connectedAt:nil];
resolve(nil); resolve(nil);
} }
@ -206,36 +184,20 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
#endif #endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; NSString *displayName = options[@"displayName"];
if (options[@"displayName"]) { BOOL hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
callUpdate.localizedCallerName = options[@"displayName"];
} [JMCallKitProxy reportCallUpdateWith:callUUID_
if (options[@"hasVideo"]) { handle:nil
callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue]; displayName:displayName
} hasVideo:hasVideo];
[self.provider reportCallWithUUID:callUUID_ updated:callUpdate];
resolve(nil); resolve(nil);
} }
#pragma mark - Helper methods #pragma mark - Helper methods
- (CXCallController *)callController { - (void)configureProviderFromDictionary:(NSDictionary* )dictionary {
if (!_callController) {
_callController = [[CXCallController alloc] init];
}
return _callController;
}
- (CXProvider *)provider {
if (!_provider) {
[self setProviderConfiguration:nil];
}
return _provider;
}
- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]"); NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]");
#endif #endif
@ -251,30 +213,25 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
= [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"];
} }
CXProviderConfiguration *providerConfiguration
= [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName];
// iconTemplateImageData // iconTemplateImageData
NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"]; NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"];
NSData *iconTemplateImageData;
if (iconTemplateImageName) { if (iconTemplateImageName) {
UIImage *iconTemplateImage UIImage *iconTemplateImage
= [UIImage imageNamed:iconTemplateImageName = [UIImage imageNamed:iconTemplateImageName
inBundle:[NSBundle bundleForClass:self.class] inBundle:[NSBundle bundleForClass:self.class]
compatibleWithTraitCollection:nil]; compatibleWithTraitCollection:nil];
if (iconTemplateImage) { if (iconTemplateImage) {
providerConfiguration.iconTemplateImageData iconTemplateImageData
= UIImagePNGRepresentation(iconTemplateImage); = UIImagePNGRepresentation(iconTemplateImage);
} }
} }
providerConfiguration.maximumCallGroups = 1; NSString *ringtoneSound = dictionary[@"ringtoneSound"];
providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"]; [JMCallKitProxy configureCallKitProviderWithLocalizedName:localizedName
providerConfiguration.supportedHandleTypes ringtoneSound:ringtoneSound
= [NSSet setWithObjects:@(CXHandleTypeGeneric), nil]; iconTemplateImageData:iconTemplateImageData];
providerConfiguration.supportsVideo = YES;
return providerConfiguration;
} }
- (void)requestTransaction:(CXTransaction *)transaction - (void)requestTransaction:(CXTransaction *)transaction
@ -284,8 +241,8 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction); NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
#endif #endif
[self.callController requestTransaction:transaction [JMCallKitProxy request:transaction
completion:^(NSError * _Nullable error) { completion:^(NSError * _Nullable error) {
if (error) { if (error) {
NSLog( NSLog(
@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", @"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)",
@ -298,10 +255,10 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
}]; }];
} }
#pragma mark - CXProviderDelegate #pragma mark - JitsiMeetCallKitListener
// Called when the provider has been reset. We should terminate all calls. // Called when the provider has been reset. We should terminate all calls.
- (void)providerDidReset:(CXProvider *)provider { - (void)providerDidReset {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]"); NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]");
#endif #endif
@ -310,84 +267,61 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
} }
// Answering incoming call // Answering incoming call
- (void) provider:(CXProvider *)provider - (void) performAnswerCallWithUUID:(NSUUID *)UUID {
performAnswerCallAction:(CXAnswerCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformAnswerCallAction [self sendEventWithName:RNCallKitPerformAnswerCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }]; body:@{ @"callUUID": UUID.UUIDString }];
[action fulfill];
} }
// Call ended, user request // Call ended, user request
- (void) provider:(CXProvider *)provider - (void) performEndCallWithUUID:(NSUUID *)UUID {
performEndCallAction:(CXEndCallAction *)action {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformEndCallAction [self sendEventWithName:RNCallKitPerformEndCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }]; body:@{ @"callUUID": UUID.UUIDString }];
[action fulfill];
} }
// Handle audio mute from CallKit view // Handle audio mute from CallKit view
- (void) provider:(CXProvider *)provider - (void) performSetMutedCallWithUUID:(NSUUID *)UUID
performSetMutedCallAction:(CXSetMutedCallAction *)action { isMuted:(BOOL)isMuted {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]");
#endif #endif
[self sendEventWithName:RNCallKitPerformSetMutedCallAction [self sendEventWithName:RNCallKitPerformSetMutedCallAction
body:@{ body:@{
@"callUUID": action.callUUID.UUIDString, @"callUUID": UUID.UUIDString,
@"muted": @(action.muted) @"muted": @(isMuted)
}]; }];
[action fulfill];
} }
// Starting outgoing call // Starting outgoing call
- (void) provider:(CXProvider *)provider - (void) performStartCallWithUUID:(NSUUID *)UUID
performStartCallAction:(CXStartCallAction *)action { isVideo:(BOOL)isVideo {
#ifdef DEBUG #ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]");
#endif #endif
[JMCallKitProxy reportOutgoingCallWith:UUID
[action fulfill]; startedConnectingAt:nil];
// Update call info.
NSUUID *callUUID = action.callUUID;
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle = action.handle;
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = action.isVideo;
[provider reportCallWithUUID:callUUID updated:callUpdate];
// Notify the system about the outgoing call.
[provider reportOutgoingCallWithUUID:callUUID
startedConnectingAtDate:[NSDate date]];
} }
// The following just help with debugging: // The following just help with debugging:
#ifdef DEBUG #ifdef DEBUG
- (void) provider:(CXProvider *)provider - (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session {
didActivateAudioSession:(AVAudioSession *)audioSession {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]");
} }
- (void) provider:(CXProvider *)provider - (void) providerDidDeactivateAudioSessionWithSession:(AVAudioSession *)session {
didDeactivateAudioSession:(AVAudioSession *)audioSession {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]");
} }
- (void) provider:(CXProvider *)provider - (void) providerTimedOutPerformingActionWithAction:(CXAction *)action {
timedOutPerformingAction:(CXAction *)action {
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]"); NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]");
} }

View File

@ -0,0 +1,63 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import AVKit
import CallKit
import Foundation
@objc public protocol JMCallKitEventListener: NSObjectProtocol {
@available(iOS 10.0, *)
@objc optional func providerDidReset()
@available(iOS 10.0, *)
@objc optional func performAnswerCall(UUID: UUID)
@available(iOS 10.0, *)
@objc optional func performEndCall(UUID: UUID)
@available(iOS 10.0, *)
@objc optional func performSetMutedCall(UUID: UUID, isMuted: Bool)
@available(iOS 10.0, *)
@objc optional func performStartCall(UUID: UUID, isVideo: Bool)
@available(iOS 10.0, *)
@objc optional func providerDidActivateAudioSession(session: AVAudioSession)
@available(iOS 10.0, *)
@objc optional func providerDidDeactivateAudioSession(session: AVAudioSession)
@available(iOS 10.0, *)
@objc optional func providerTimedOutPerformingAction(action: CXAction)
}
internal struct JMCallKitEventListenerWrapper: Hashable {
public var hashValue: Int
internal weak var listener: JMCallKitEventListener?
public init(listener: JMCallKitEventListener) {
self.listener = listener
self.hashValue = listener.hash
}
public static func ==(lhs: JMCallKitEventListenerWrapper,
rhs: JMCallKitEventListenerWrapper) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import AVKit
import CallKit
import Foundation
internal final class JMCallKitNotifier: NSObject, CXProviderDelegate {
private var listeners = Set<JMCallKitEventListenerWrapper>()
internal override init() {}
// MARK: - Add/remove listeners
func addListener(_ listener: JMCallKitEventListener) {
let wrapper = JMCallKitEventListenerWrapper(listener: listener)
objc_sync_enter(listeners)
listeners.insert(wrapper)
objc_sync_exit(listeners)
}
func removeListener(_ listener: JMCallKitEventListener) {
let wrapper = JMCallKitEventListenerWrapper(listener: listener)
objc_sync_enter(listeners)
listeners.remove(wrapper)
objc_sync_exit(listeners)
}
// MARK: - CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
objc_sync_enter(listeners)
listeners.forEach { $0.listener?.providerDidReset?() }
objc_sync_exit(listeners)
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
objc_sync_enter(listeners)
listeners.forEach { $0.listener?.performAnswerCall?(UUID: action.callUUID) }
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
objc_sync_enter(listeners)
listeners.forEach { $0.listener?.performEndCall?(UUID: action.callUUID) }
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performSetMutedCall?(UUID: action.callUUID,
isMuted: action.isMuted)
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.performStartCall?(UUID: action.callUUID,
isVideo: action.isVideo)
}
objc_sync_exit(listeners)
action.fulfill()
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.providerDidActivateAudioSession?(session: audioSession)
}
objc_sync_exit(listeners)
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
objc_sync_enter(listeners)
listeners.forEach {
$0.listener?.providerDidDeactivateAudioSession?(session: audioSession)
}
objc_sync_exit(listeners)
}
}

View File

@ -0,0 +1,177 @@
/*
* Copyright @ 2018-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import CallKit
import Foundation
/// JitsiMeet CallKit proxy
@available(iOS 10.0, *)
@objc public final class JMCallKitProxy: NSObject {
override private init() {}
// MARK: - CallKit proxy
internal static let cxProvider: CXProvider = {
let config = CXProviderConfiguration(localizedName: "")
let provider = CXProvider(configuration: config)
return provider
}()
internal static let cxCallController: CXCallController = {
return CXCallController()
}()
internal static let callKitNotifier: JMCallKitNotifier = {
return JMCallKitNotifier()
}()
internal static var cxProviderConfiguration: CXProviderConfiguration? {
didSet {
guard let providerConfiguration = cxProviderConfiguration else { return }
cxProvider.configuration = providerConfiguration
cxProvider.setDelegate(callKitNotifier, queue: nil)
}
}
/// Enables the proxy in between callkit and the consumers of the SDK
/// Default to enabled, set to false when you don't want to use callkit
@objc public static var enabled: Bool = true {
didSet {
if enabled == false {
cxProvider.setDelegate(nil, queue: nil)
}
}
}
@objc public static func hasProviderBeenConfigurated() -> Bool {
return cxProviderConfiguration != nil
}
@objc public static func configureCallKitProvider(localizedName: String,
ringtoneSound: String?,
iconTemplateImageData: Data?) {
let configuration = CXProviderConfiguration(localizedName: localizedName)
configuration.ringtoneSound = ringtoneSound
configuration.iconTemplateImageData = iconTemplateImageData
configuration.maximumCallGroups = 1
configuration.maximumCallsPerCallGroup = 1
configuration.supportedHandleTypes = [CXHandle.HandleType.generic]
configuration.supportsVideo = true
cxProviderConfiguration = configuration
}
@objc public static func addListener(_ listener: JMCallKitEventListener) {
callKitNotifier.addListener(listener)
}
@objc public static func removeListener(_ listener: JMCallKitEventListener) {
callKitNotifier.removeListener(listener)
}
@objc public static func hasActiveCallForUUID(_ callUUID: String) -> Bool {
let activeCallForUUID = cxCallController.callObserver.calls.first {
$0.uuid == UUID(uuidString: callUUID)
}
guard activeCallForUUID != nil else { return false }
return true
}
@objc public static func reportNewIncomingCall(UUID: UUID,
handle: String?,
displayName: String?,
hasVideo: Bool,
completion: @escaping (Error?) -> Void) {
guard enabled else { return }
let callUpdate = makeCXUpdate(handle: handle,
displayName: displayName,
hasVideo: hasVideo)
cxProvider.reportNewIncomingCall(with: UUID,
update: callUpdate,
completion: completion)
}
@objc public static func reportCallUpdate(with UUID: UUID,
handle: String?,
displayName: String?,
hasVideo: Bool) {
guard enabled else { return }
let callUpdate = makeCXUpdate(handle: handle,
displayName: displayName,
hasVideo: hasVideo)
cxProvider.reportCall(with: UUID, updated: callUpdate)
}
@objc public static func reportCall(with UUID: UUID,
endedAt dateEnded: Date?,
reason endedReason: CXCallEndedReason) {
guard enabled else { return }
cxProvider.reportCall(with: UUID,
endedAt: dateEnded,
reason: endedReason)
}
@objc public static func reportOutgoingCall(with UUID: UUID,
startedConnectingAt dateStartedConnecting: Date?) {
guard enabled else { return }
cxProvider.reportOutgoingCall(with: UUID,
startedConnectingAt: dateStartedConnecting)
}
@objc public static func reportOutgoingCall(with UUID: UUID,
connectedAt dateConnected: Date?) {
guard enabled else { return }
cxProvider.reportOutgoingCall(with: UUID, connectedAt: dateConnected)
}
@objc public static func request(_ transaction: CXTransaction,
completion: @escaping (Error?) -> Swift.Void) {
guard enabled else { return }
cxCallController.request(transaction, completion: completion)
}
// MARK: - Callkit Proxy helpers
private static func makeCXUpdate(handle: String?,
displayName: String?,
hasVideo: Bool) -> CXCallUpdate {
let update = CXCallUpdate()
update.supportsDTMF = false
update.supportsHolding = false
update.supportsGrouping = false
update.supportsUngrouping = false
update.hasVideo = hasVideo
if let handle = handle {
update.remoteHandle = CXHandle(type: .generic,
value: handle)
}
if let displayName = displayName {
update.localizedCallerName = displayName
}
return update
}
}

View File

@ -26,6 +26,7 @@ const WHITELISTED_KEYS = [
'callStatsConfIDNamespace', 'callStatsConfIDNamespace',
'callStatsID', 'callStatsID',
'callStatsSecret', 'callStatsSecret',
'callUUID',
'channelLastN', 'channelLastN',
'constraints', 'constraints',
'debug', 'debug',

View File

@ -235,10 +235,12 @@ function _conferenceWillJoin({ getState }, next, action) {
const state = getState(); const state = getState();
const url = getInviteURL(state); const url = getInviteURL(state);
const hasVideo = !isVideoMutedByAudioOnly(state); const hasVideo = !isVideoMutedByAudioOnly(state);
const { callUUID } = state['features/base/config'];
// When assigning the call UUID, do so in upper case, since iOS will // When assigning the call UUID, do so in upper case, since iOS will
// return it upper cased. // return it upper cased.
conference.callUUID = uuid.v4().toUpperCase(); conference.callUUID = (callUUID || uuid.v4()).toUpperCase();
CallKit.startCall(conference.callUUID, url.toString(), hasVideo) CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
.then(() => { .then(() => {
const { room } = state['features/base/conference']; const { room } = state['features/base/conference'];