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'];