From e5309a6482782f7868561331a8b3a629a41f6f8f Mon Sep 17 00:00:00 2001 From: Daniel Ornelas Date: Wed, 18 Apr 2018 10:11:56 -0500 Subject: [PATCH] [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. --- ios/sdk/sdk.xcodeproj/project.pbxproj | 20 ++ ios/sdk/src/CallKit.m | 184 ++++++------------ .../src/callkit/JMCallKitEventListener.swift | 63 ++++++ ios/sdk/src/callkit/JMCallKitNotifier.swift | 104 ++++++++++ ios/sdk/src/callkit/JMCallKitProxy.swift | 177 +++++++++++++++++ react/features/base/config/functions.js | 1 + react/features/mobile/callkit/middleware.js | 4 +- 7 files changed, 427 insertions(+), 126 deletions(-) create mode 100644 ios/sdk/src/callkit/JMCallKitEventListener.swift create mode 100644 ios/sdk/src/callkit/JMCallKitNotifier.swift create mode 100644 ios/sdk/src/callkit/JMCallKitProxy.swift diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index db1d2006f..34a326b02 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -38,6 +38,9 @@ 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 */; }; 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 */; }; C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; + C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitNotifier.swift; sourceTree = ""; }; + C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitProxy.swift; sourceTree = ""; }; + C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitEventListener.swift; sourceTree = ""; }; C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = ""; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; @@ -138,6 +144,7 @@ 0BD906E71EC0C00300C8C18E /* src */ = { isa = PBXGroup; children = ( + C69EFA02209A0EFD0027712B /* callkit */, B386B84F20981A11000DEF7A /* invite */, C6A3426B204F127900E062DD /* picture-in-picture */, 0BCA495C1EC4B6C600B793EE /* AudioMode.m */, @@ -198,6 +205,16 @@ name = Pods; sourceTree = ""; }; + C69EFA02209A0EFD0027712B /* callkit */ = { + isa = PBXGroup; + children = ( + C69EFA0B209A0F660027712B /* JMCallKitEventListener.swift */, + C69EFA09209A0F650027712B /* JMCallKitNotifier.swift */, + C69EFA0A209A0F660027712B /* JMCallKitProxy.swift */, + ); + path = callkit; + sourceTree = ""; + }; C6A3426B204F127900E062DD /* picture-in-picture */ = { isa = PBXGroup; children = ( @@ -390,7 +407,10 @@ 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, + C69EFA0C209A0F660027712B /* JMCallKitNotifier.swift in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, + C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */, + C69EFA0E209A0F660027712B /* JMCallKitEventListener.swift in Sources */, 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/sdk/src/CallKit.m b/ios/sdk/src/CallKit.m index fd3e869f0..05d933184 100644 --- a/ios/sdk/src/CallKit.m +++ b/ios/sdk/src/CallKit.m @@ -27,6 +27,8 @@ #import #import +#import + // The events emitted/supported by RNCallKit: static NSString * const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction"; @@ -37,14 +39,10 @@ static NSString * const RNCallKitPerformSetMutedCallAction static NSString * const RNCallKitProviderDidReset = @"providerDidReset"; -@interface RNCallKit : RCTEventEmitter +@interface RNCallKit : RCTEventEmitter @end @implementation RNCallKit -{ - CXCallController *_callController; - CXProvider *_provider; -} RCT_EXTERN void RCTRegisterModule(Class); @@ -70,35 +68,8 @@ RCT_EXTERN void RCTRegisterModule(Class); ]; } -// Display the incoming call to the user -RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)callUUID - 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); - } - }]; +- (void)dealloc { + [JMCallKitProxy removeListener:self]; } // End call @@ -141,14 +112,12 @@ RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) { dictionary); #endif - CXProviderConfiguration *configuration - = [self providerConfigurationFromDictionary:dictionary]; - if (_provider) { - _provider.configuration = configuration; - } else { - _provider = [[CXProvider alloc] initWithConfiguration:configuration]; - [_provider setDelegate:self queue:nil]; + if (![JMCallKitProxy hasProviderBeenConfigurated]) { + [self configureProviderFromDictionary:dictionary]; } + + // register to receive CallKit proxy events + [JMCallKitProxy addListener: self]; } // Start outgoing call @@ -162,6 +131,15 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID #endif 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 alloc] initWithType:CXHandleTypeGeneric value:handle]; CXStartCallAction *action @@ -177,9 +155,9 @@ RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; - [self.provider reportCallWithUUID:callUUID_ - endedAtDate:[NSDate date] - reason:CXCallEndedReasonFailed]; + [JMCallKitProxy reportCallWith:callUUID_ + endedAt:nil + reason:CXCallEndedReasonFailed]; resolve(nil); } @@ -188,8 +166,8 @@ RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; - [self.provider reportOutgoingCallWithUUID:callUUID_ - connectedAtDate:[NSDate date]]; + [JMCallKitProxy reportOutgoingCallWith:callUUID_ + connectedAt:nil]; resolve(nil); } @@ -206,36 +184,20 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID #endif NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID]; - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - if (options[@"displayName"]) { - callUpdate.localizedCallerName = options[@"displayName"]; - } - if (options[@"hasVideo"]) { - callUpdate.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue]; - } - [self.provider reportCallWithUUID:callUUID_ updated:callUpdate]; + NSString *displayName = options[@"displayName"]; + BOOL hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue]; + + [JMCallKitProxy reportCallUpdateWith:callUUID_ + handle:nil + displayName:displayName + hasVideo:hasVideo]; + resolve(nil); } #pragma mark - Helper methods -- (CXCallController *)callController { - if (!_callController) { - _callController = [[CXCallController alloc] init]; - } - - return _callController; -} - -- (CXProvider *)provider { - if (!_provider) { - [self setProviderConfiguration:nil]; - } - - return _provider; -} - -- (CXProviderConfiguration *)providerConfigurationFromDictionary:(NSDictionary* )dictionary { +- (void)configureProviderFromDictionary:(NSDictionary* )dictionary { #ifdef DEBUG NSLog(@"[RNCallKit][providerConfigurationFromDictionary:]"); #endif @@ -251,30 +213,25 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID = [[NSBundle mainBundle] infoDictionary][@"CFBundleDisplayName"]; } - CXProviderConfiguration *providerConfiguration - = [[CXProviderConfiguration alloc] initWithLocalizedName:localizedName]; - // iconTemplateImageData NSString *iconTemplateImageName = dictionary[@"iconTemplateImageName"]; + NSData *iconTemplateImageData; if (iconTemplateImageName) { UIImage *iconTemplateImage = [UIImage imageNamed:iconTemplateImageName inBundle:[NSBundle bundleForClass:self.class] compatibleWithTraitCollection:nil]; if (iconTemplateImage) { - providerConfiguration.iconTemplateImageData + iconTemplateImageData = UIImagePNGRepresentation(iconTemplateImage); } } - providerConfiguration.maximumCallGroups = 1; - providerConfiguration.maximumCallsPerCallGroup = 1; - providerConfiguration.ringtoneSound = dictionary[@"ringtoneSound"]; - providerConfiguration.supportedHandleTypes - = [NSSet setWithObjects:@(CXHandleTypeGeneric), nil]; - providerConfiguration.supportsVideo = YES; - - return providerConfiguration; + NSString *ringtoneSound = dictionary[@"ringtoneSound"]; + + [JMCallKitProxy configureCallKitProviderWithLocalizedName:localizedName + ringtoneSound:ringtoneSound + iconTemplateImageData:iconTemplateImageData]; } - (void)requestTransaction:(CXTransaction *)transaction @@ -284,8 +241,8 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction); #endif - [self.callController requestTransaction:transaction - completion:^(NSError * _Nullable error) { + [JMCallKitProxy request:transaction + completion:^(NSError * _Nullable error) { if (error) { NSLog( @"[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. -- (void)providerDidReset:(CXProvider *)provider { +- (void)providerDidReset { #ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][providerDidReset:]"); #endif @@ -310,84 +267,61 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID } // Answering incoming call -- (void) provider:(CXProvider *)provider - performAnswerCallAction:(CXAnswerCallAction *)action { +- (void) performAnswerCallWithUUID:(NSUUID *)UUID { #ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction:]"); #endif [self sendEventWithName:RNCallKitPerformAnswerCallAction - body:@{ @"callUUID": action.callUUID.UUIDString }]; - [action fulfill]; + body:@{ @"callUUID": UUID.UUIDString }]; } // Call ended, user request -- (void) provider:(CXProvider *)provider - performEndCallAction:(CXEndCallAction *)action { +- (void) performEndCallWithUUID:(NSUUID *)UUID { #ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction:]"); #endif [self sendEventWithName:RNCallKitPerformEndCallAction - body:@{ @"callUUID": action.callUUID.UUIDString }]; - [action fulfill]; + body:@{ @"callUUID": UUID.UUIDString }]; } // Handle audio mute from CallKit view -- (void) provider:(CXProvider *)provider - performSetMutedCallAction:(CXSetMutedCallAction *)action { +- (void) performSetMutedCallWithUUID:(NSUUID *)UUID + isMuted:(BOOL)isMuted { #ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction:]"); #endif [self sendEventWithName:RNCallKitPerformSetMutedCallAction body:@{ - @"callUUID": action.callUUID.UUIDString, - @"muted": @(action.muted) + @"callUUID": UUID.UUIDString, + @"muted": @(isMuted) }]; - [action fulfill]; } // Starting outgoing call -- (void) provider:(CXProvider *)provider - performStartCallAction:(CXStartCallAction *)action { +- (void) performStartCallWithUUID:(NSUUID *)UUID + isVideo:(BOOL)isVideo { #ifdef DEBUG NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction:]"); #endif - - [action fulfill]; - - // 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]]; + [JMCallKitProxy reportOutgoingCallWith:UUID + startedConnectingAt:nil]; } // The following just help with debugging: #ifdef DEBUG -- (void) provider:(CXProvider *)provider - didActivateAudioSession:(AVAudioSession *)audioSession { +- (void) providerDidActivateAudioSessionWithSession:(AVAudioSession *)session { NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession:]"); } -- (void) provider:(CXProvider *)provider - didDeactivateAudioSession:(AVAudioSession *)audioSession { +- (void) providerDidDeactivateAudioSessionWithSession:(AVAudioSession *)session { NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession:]"); } -- (void) provider:(CXProvider *)provider - timedOutPerformingAction:(CXAction *)action { +- (void) providerTimedOutPerformingActionWithAction:(CXAction *)action { NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction:]"); } diff --git a/ios/sdk/src/callkit/JMCallKitEventListener.swift b/ios/sdk/src/callkit/JMCallKitEventListener.swift new file mode 100644 index 000000000..77ed6ef3e --- /dev/null +++ b/ios/sdk/src/callkit/JMCallKitEventListener.swift @@ -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 + } +} diff --git a/ios/sdk/src/callkit/JMCallKitNotifier.swift b/ios/sdk/src/callkit/JMCallKitNotifier.swift new file mode 100644 index 000000000..ac17b1a2c --- /dev/null +++ b/ios/sdk/src/callkit/JMCallKitNotifier.swift @@ -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() + + 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) + } +} diff --git a/ios/sdk/src/callkit/JMCallKitProxy.swift b/ios/sdk/src/callkit/JMCallKitProxy.swift new file mode 100644 index 000000000..d3e5f8c39 --- /dev/null +++ b/ios/sdk/src/callkit/JMCallKitProxy.swift @@ -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 + } + +} diff --git a/react/features/base/config/functions.js b/react/features/base/config/functions.js index d7f48504a..c64f7f60c 100644 --- a/react/features/base/config/functions.js +++ b/react/features/base/config/functions.js @@ -26,6 +26,7 @@ const WHITELISTED_KEYS = [ 'callStatsConfIDNamespace', 'callStatsID', 'callStatsSecret', + 'callUUID', 'channelLastN', 'constraints', 'debug', diff --git a/react/features/mobile/callkit/middleware.js b/react/features/mobile/callkit/middleware.js index 390018481..c417b9ded 100644 --- a/react/features/mobile/callkit/middleware.js +++ b/react/features/mobile/callkit/middleware.js @@ -235,10 +235,12 @@ function _conferenceWillJoin({ getState }, next, action) { const state = getState(); const url = getInviteURL(state); const hasVideo = !isVideoMutedByAudioOnly(state); + const { callUUID } = state['features/base/config']; // When assigning the call UUID, do so in upper case, since iOS will // return it upper cased. - conference.callUUID = uuid.v4().toUpperCase(); + conference.callUUID = (callUUID || uuid.v4()).toUpperCase(); + CallKit.startCall(conference.callUUID, url.toString(), hasVideo) .then(() => { const { room } = state['features/base/conference'];