AzureCommunicationUI/sdk/AzureCommunicationUICalling/Sources/CallComposite.swift (647 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import AzureCommunicationCommon /* <SDK_CX_PROVIDER_SUPPORT> import AzureCommunicationCalling </SDK_CX_PROVIDER_SUPPORT> */ import UIKit import SwiftUI import FluentUI import AVKit import Combine /* <SDK_CX_PROVIDER_SUPPORT> import CallKit </SDK_CX_PROVIDER_SUPPORT> */ // swiftlint:disable file_length // swiftlint:disable type_body_length /// The main class representing the entry point for the Call Composite. public class CallComposite { /// The class to configure events closures for Call Composite. public class Events { /// Closure to execute when error event occurs inside Call Composite. public var onError: ((CallCompositeError) -> Void)? /// Closures to execute when participant has joined a call inside Call Composite. public var onRemoteParticipantJoined: (([CommunicationIdentifier]) -> Void)? /// Closure to execure when CallComposite is displayed in Picture-In-Picture. public var onPictureInPictureChanged: ((_ isPictureInPicture: Bool) -> Void)? /// Closure to execute when call state changes. public var onCallStateChanged: ((CallState) -> Void)? /// Closure to Call Composite dismissed. public var onDismissed: ((CallCompositeDismissed) -> Void)? /// Closure to execute when the User reports an issue from within the call composite public var onUserReportedIssue: ((CallCompositeUserReportedIssue) -> Void)? /// Closure to incoming call received. public var onIncomingCall: ((IncomingCall) -> Void)? /// Closure to incoming call cancelled. public var onIncomingCallCancelled: ((IncomingCallCancelled) -> Void)? /// Closure to incoming call id accepted by CallKit. public var onIncomingCallAcceptedFromCallKit: ((_ callId: String) -> Void)? /// Closure to execute when participant has left a call inside Call Composite public var onRemoteParticipantLeft: (([CommunicationIdentifier]) -> Void)? /* <CALL_START_TIME> /// Closure to call start time updated. public var onCallStartTimeUpdated: ((Date) -> Void)? </CALL_START_TIME> */ } /// The events handler for Call Composite public let events: Events private let themeOptions: ThemeOptions? private let localizationOptions: LocalizationOptions? private let enableMultitasking: Bool private let enableSystemPipWhenMultitasking: Bool private let setupViewOrientationOptions: OrientationOptions? private let callingViewOrientationOptions: OrientationOptions? // Internal dependencies private var logger: Logger = DefaultLogger(category: "Calling") private var accessibilityProvider: AccessibilityProviderProtocol = AccessibilityProvider() private var localizationProvider: LocalizationProviderProtocol private var orientationProvider: OrientationProvider private var store: Store<AppState, Action>? private var errorManager: ErrorManagerProtocol? private var exitManager: ExitManagerProtocol? private var callStateManager: CallStateManagerProtocol? private var lifeCycleManager: LifeCycleManagerProtocol? private var permissionManager: PermissionsManagerProtocol? private var audioSessionManager: AudioSessionManagerProtocol? private var remoteParticipantsManager: RemoteParticipantsManagerProtocol? private var avatarViewManager: AvatarViewManagerProtocol? private var customCallingSdkWrapper: CallingSDKWrapperProtocol? private var debugInfoManager: DebugInfoManagerProtocol? private var pipManager: PipManagerProtocol? private var updatableOptionsManager: UpdatableOptionsManagerProtocol? private var callHistoryService: CallHistoryService? private lazy var callHistoryRepository = CallHistoryRepository(logger: logger, userDefaults: UserDefaults.standard) private var leaveCallConfirmationMode: LeaveCallConfirmationMode = .alwaysEnabled private var setupScreenOptions: SetupScreenOptions? private var callScreenOptions: CallScreenOptions? private var viewFactory: CompositeViewFactoryProtocol? private var viewController: UIViewController? private var pipViewController: UIViewController? private var cancellables = Set<AnyCancellable>() private var callKitOptions: CallKitOptions? private var callKitRemoteInfo: CallKitRemoteInfo? private var credential: CommunicationTokenCredential? private var userId: CommunicationIdentifier? private var displayName: String? private var disableInternalPushForIncomingCall = false private var callingSDKInitializer: CallingSDKInitializer? private var callConfiguration: CallConfiguration? private var compositeUILaunched = false private var incomingCallAcceptedByCallKitCallId: String? private var videoViewManager: VideoViewManager? private var callingSDKEventsHandler: CallingSDKEventsHandler? private var callingSDKWrapper: CallingSDKWrapperProtocol? /* <CALL_START_TIME> private var callStartTimeInternal: Date? </CALL_START_TIME> */ /// Get debug information for the Call Composite. public var debugInfo: DebugInfo { let localDebugInfoManager = debugInfoManager ?? createDebugInfoManager() return localDebugInfoManager.getDebugInfo() } /// Get call state for the Call Composite. public var callState: CallState { return store?.state.callingState.status.toCallCompositeCallState() ?? CallState.none } /// Create an instance of CallComposite with options. /// - Parameter options: The CallCompositeOptions used to configure the experience. @available(*, deprecated, message: "Use init with CommunicationTokenCredential instead.") public init(withOptions options: CallCompositeOptions? = nil) { credential = nil events = Events() userId = options?.userId themeOptions = options?.themeOptions localizationOptions = options?.localizationOptions localizationProvider = LocalizationProvider(logger: logger) enableMultitasking = options?.enableMultitasking ?? false enableSystemPipWhenMultitasking = options?.enableSystemPipWhenMultitasking ?? false setupViewOrientationOptions = options?.setupScreenOrientation callingViewOrientationOptions = options?.callingScreenOrientation orientationProvider = OrientationProvider() leaveCallConfirmationMode = options?.callScreenOptions?.controlBarOptions?.leaveCallConfirmationMode ?? .alwaysEnabled setupScreenOptions = options?.setupScreenOptions callScreenOptions = options?.callScreenOptions callKitOptions = options?.callKitOptions displayName = options?.displayName if let disableInternalPushForIncomingCall = options?.disableInternalPushForIncomingCall { self.disableInternalPushForIncomingCall = disableInternalPushForIncomingCall } } /// Create an instance of CallComposite with options. /// - Parameter credential: The CommunicationTokenCredential used for call. /// - Parameter options: The CallCompositeOptions used to configure the experience. public init(credential: CommunicationTokenCredential, withOptions options: CallCompositeOptions? = nil) { self.credential = credential userId = options?.userId events = Events() themeOptions = options?.themeOptions localizationOptions = options?.localizationOptions localizationProvider = LocalizationProvider(logger: logger) enableMultitasking = options?.enableMultitasking ?? false enableSystemPipWhenMultitasking = options?.enableSystemPipWhenMultitasking ?? false setupViewOrientationOptions = options?.setupScreenOrientation callingViewOrientationOptions = options?.callingScreenOrientation orientationProvider = OrientationProvider() leaveCallConfirmationMode = options?.callScreenOptions?.controlBarOptions?.leaveCallConfirmationMode ?? .alwaysEnabled setupScreenOptions = options?.setupScreenOptions callScreenOptions = options?.callScreenOptions callKitOptions = options?.callKitOptions displayName = options?.displayName if let disableInternalPushForIncomingCall = options?.disableInternalPushForIncomingCall { self.disableInternalPushForIncomingCall = disableInternalPushForIncomingCall } } /// Dismiss call composite. If call is in progress, user will leave a call. public func dismiss() { logger.debug( "CallComposite dismiss") exitManager?.dismiss() if !compositeUILaunched { disposeSDKWrappers() logger.debug( "CallComposite callingSDKInitializer dispose") let exitManagerCache = exitManager cleanUpManagers() exitManagerCache?.onDismissed() } } /* <SDK_CX_PROVIDER_SUPPORT> /// Get CXProvider public static func getCXProvider() -> CXProvider? { return CallClient.getCXProviderInstance() } </SDK_CX_PROVIDER_SUPPORT> */ /// Handle push notification to receive incoming call notification. /// - Parameter pushNotification: The push notification received. /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either /// a `Void` or an `Error`. public func handlePushNotification(pushNotification: PushNotification, completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard self.credential != nil else { completionHandler?(.failure(CommunicationTokenCredentialError.communicationTokenCredentialNotSet)) return } getCallingSDKInitializer().handlePushNotification(pushNotification: pushNotification, completionHandler: completionHandler) } /// Report incoming call to notify CallKit when in background mode. /// On success you can wake up application. /// - Parameter pushNotification: The push notification received. /// - Parameter callKitOptions: The CallKitOptions used to configure the incoming call. /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either /// a `Void` or an `Error`. public static func reportIncomingCall(pushNotification: PushNotification, callKitOptions: CallKitOptions, completionHandler: ((Result<Void, Error>) -> Void)? = nil) { CallingSDKInitializer.reportIncomingCall(pushNotification: pushNotification, callKitOptions: callKitOptions, completionHandler: completionHandler) } /// Register device token to receive Azure Notification Hubs push notifications. /// - Parameter deviceRegistrationToken: The device registration token. /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either public func registerPushNotifications(deviceRegistrationToken: Data, completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard self.credential != nil else { completionHandler?(.failure(CommunicationTokenCredentialError.communicationTokenCredentialNotSet)) return } getCallingSDKInitializer().registerPushNotification(deviceRegistrationToken: deviceRegistrationToken, completionHandler: completionHandler) } /// Unregister Azure Notification Hubs push notifications /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either public func unregisterPushNotifications(completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard self.credential != nil else { completionHandler?(.failure(CommunicationTokenCredentialError.communicationTokenCredentialNotSet)) return } getCallingSDKInitializer().unregisterPushNotifications(completionHandler: completionHandler) } /// Accept incoming call /// - Parameter incomingCallId: The incoming call id. /// - Parameter callKitRemoteInfo: The CallKitRemoteInfo used to set the CallKit information for the outgoing call. /// - Parameter localOptions: LocalOptions used to set the user participants information for the call. /// This is data is not sent up to ACS. public func accept(incomingCallId: String, callKitRemoteInfo: CallKitRemoteInfo? = nil, localOptions: LocalOptions? = nil) { self.callKitRemoteInfo = callKitRemoteInfo let callConfiguration = CallConfiguration(locator: nil, participants: nil, callId: incomingCallId) self.callConfiguration = callConfiguration launch(callConfiguration, localOptions: localOptions) } /// Reject incoming call /// - Parameter incomingCallId: The incoming call id. /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either public func reject(incomingCallId: String, completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard self.credential != nil else { completionHandler?(.failure(CommunicationTokenCredentialError.communicationTokenCredentialNotSet)) return } getCallingSDKInitializer().reject(incomingCallId: incomingCallId, completionHandler: completionHandler) } /// Hold call /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either /// a `Void` or an `Error`. public func hold(completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard let callingSDKWrapper = callingSDKWrapper else { completionHandler?(.failure((CallError.callIsNotInProgress))) return } Task { do { try await callingSDKWrapper.holdCall() completionHandler?(.success(())) } catch { completionHandler?(.failure(error)) } } } /// Resume call /// - Parameter completionHandler: The completion handler that receives `Result` enum value with either /// a `Void` or an `Error`. public func resume(completionHandler: ((Result<Void, Error>) -> Void)? = nil) { guard let callingSDKWrapper = callingSDKWrapper else { completionHandler?(.failure((CallError.callIsNotInProgress))) return } Task { do { try await callingSDKWrapper.resumeCall() completionHandler?(.success(())) } catch { completionHandler?(.failure(error)) } } } /* <CALL_START_TIME> /// Get call start time public func callStartTime() -> Date? { guard let callingSDKWrapper = callingSDKWrapper else { return nil } return callingSDKWrapper.callStartTime() } </CALL_START_TIME> */ convenience init(withOptions options: CallCompositeOptions? = nil, callingSDKWrapperProtocol: CallingSDKWrapperProtocol? = nil) { self.init(withOptions: options) self.customCallingSdkWrapper = callingSDKWrapperProtocol } deinit { logger.debug("CallComposite Call Composite deallocated") } private func launch(_ callConfiguration: CallConfiguration, localOptions: LocalOptions?) { logger.debug("CallComposite launch composite experience") setupScreenOptions = localOptions?.setupScreenOptions ?? setupScreenOptions callScreenOptions = localOptions?.callScreenOptions ?? callScreenOptions let viewFactory = constructViewFactoryAndDependencies( for: callConfiguration, localOptions: localOptions, callCompositeEventsHandler: events, withCallingSDKWrapper: self.customCallingSdkWrapper ) self.viewFactory = viewFactory setupColorTheming() setupLocalization(with: localizationProvider) guard let store = self.store else { fatalError("CallComposite Construction of dependencies failed") } store.$state .receive(on: DispatchQueue.main) .sink { [weak self] state in self?.receive(state) }.store(in: &cancellables) let viewController = makeToolkitHostingController(router: NavigationRouter(store: store, logger: logger), viewFactory: viewFactory) self.viewController = viewController present(viewController) UIApplication.shared.isIdleTimerDisabled = true if store.state.permissionState.audioPermission == .notAsked || store.state.permissionState.audioPermission == .unknown { store.dispatch(action: .permissionAction(.audioPermissionRequested)) } else { store.dispatch(action: .callingAction(.setupCall)) } compositeUILaunched = true } /// Start Call Composite experience with joining a Teams meeting. /// - Parameter remoteOptions: RemoteOptions used to send to ACS to locate the call. /// - Parameter localOptions: LocalOptions used to set the user participants information for the call. /// This is data is not sent up to ACS. @available(*, deprecated, message: """ Use CallComposite init with CommunicationTokenCredential and launch(locator: JoinLocator, localOptions: LocalOptions? = nil) instead. """) public func launch(remoteOptions: RemoteOptions, localOptions: LocalOptions? = nil) { let configuration = CallConfiguration(locator: remoteOptions.locator, participants: nil, callId: nil) self.credential = remoteOptions.credential self.displayName = remoteOptions.displayName self.callConfiguration = configuration launch(configuration, localOptions: localOptions) } /// Start Call Composite experience with joining an existing call. /// - Parameter locator: Join existing call. /// - Parameter callKitRemoteInfo: CallKitRemoteInfo used to set the /// CallKit information for the outgoing call. /// - Parameter localOptions: LocalOptions used to set the user participants information for the call. /// This is data is not sent up to ACS. public func launch(locator: JoinLocator, callKitRemoteInfo: CallKitRemoteInfo? = nil, localOptions: LocalOptions? = nil) { self.callKitRemoteInfo = callKitRemoteInfo let configuration = CallConfiguration(locator: locator, participants: nil, callId: nil) self.callConfiguration = configuration launch(configuration, localOptions: localOptions) } /// Start Call Composite experience with dialing participants. /// - Parameter participants: participants to dial. /// - Parameter callKitRemoteInfo: CallKitRemoteInfo used to set the /// CallKit information for the outgoing call. /// - Parameter localOptions: LocalOptions used to set the user participants information for the call. /// This data is not sent up to ACS. public func launch(participants: [CommunicationIdentifier], callKitRemoteInfo: CallKitRemoteInfo? = nil, localOptions: LocalOptions? = nil) { self.callKitRemoteInfo = callKitRemoteInfo let configuration = CallConfiguration(locator: nil, participants: participants, callId: nil) self.callConfiguration = configuration launch(configuration, localOptions: localOptions) } /// Start Call Composite experience with call accepted from CallKit. /// - Parameter callIdAcceptedFromCallKit: call id accepted from CallKit. /// - Parameter callKitRemoteInfo: CallKitRemoteInfo used to set the /// CallKit information for the accepted call. /// - Parameter localOptions: LocalOptions used to set the user participants information for the call. /// This data is not sent up to ACS. /// skipSetupScreen will be forced true as call is already accepted. /// cameraOn will be false, default CallKit option /// microphoneOn will be true, default CallKit option public func launch(callIdAcceptedFromCallKit: String, localOptions: LocalOptions? = nil) { logger.debug( "launch \(callIdAcceptedFromCallKit)") let configuration = CallConfiguration(locator: nil, participants: nil, callId: callIdAcceptedFromCallKit) self.callConfiguration = configuration let acceptedCallLocalOptions = LocalOptions(participantViewData: localOptions?.participantViewData, setupScreenViewData: localOptions?.setupScreenViewData, cameraOn: false, microphoneOn: false, skipSetupScreen: true, audioVideoMode: localOptions?.audioVideoMode ?? .audioAndVideo) launch(configuration, localOptions: acceptedCallLocalOptions) } /// Set ParticipantViewData to be displayed for the remote participant. This is data is not sent up to ACS. /// - Parameters: /// - remoteParticipantViewData: ParticipantViewData used to set the participant's information for the call. /// - identifier: The communication identifier for the remote participant. /// - completionHandler: The completion handler that receives `Result` enum value with either /// a `Void` or an `SetParticipantViewDataError`. public func set(remoteParticipantViewData: ParticipantViewData, for identifier: CommunicationIdentifier, completionHandler: ((Result<Void, SetParticipantViewDataError>) -> Void)? = nil) { guard let avatarManager = avatarViewManager else { completionHandler?(.failure(SetParticipantViewDataError.participantNotInCall)) return } avatarManager.set(remoteParticipantViewData: remoteParticipantViewData, for: identifier, completionHandler: completionHandler) } /// Native SDK do not follow call state change order for End and Accept /// Any state can be received first: disconnected for existing call or connected for newly accepted call /// Notify once UI is closed by disconnected state before notifying for new call accept private func onCallAdded(callId: String) { if let incomingCall = callingSDKInitializer?.getIncomingCall() { if incomingCall.id == callId { incomingCallAcceptedByCallKitCallId = callId notifyOnCallKitCallAccepted() } } } /// On incoming call accepted by callkit /// It is possible that composite is in existing call, then on previous call disconnect this function will be called /// CompositeUILaunched will be set to false once existing call is disconnected private func notifyOnCallKitCallAccepted() { logger.debug("CallComposite notifyOnCallKitCallAccepted start") if !compositeUILaunched, pipViewController == nil, let incomingCall = callingSDKInitializer?.getIncomingCall(), let callId = incomingCallAcceptedByCallKitCallId, incomingCall.id == callId, let onIncomingCallAcceptedByCallKit = events.onIncomingCallAcceptedFromCallKit { onIncomingCallAcceptedByCallKit(callId) incomingCallAcceptedByCallKitCallId = nil } } /// Display Call Composite if it was hidden by user going Back in navigation while on the call. private func displayCallCompositeIfWasHidden() { guard let store = self.store, let viewFactory = self.viewFactory else { logger.error("CallComposite was not launched yet. launch() method has to be called first.") return } self.pipManager?.stopPictureInPicture() if self.pipViewController != nil { self.events.onPictureInPictureChanged?(false) } self.pipViewController?.dismissSelf() self.pipViewController = nil self.viewController?.dismissSelf() let viewController = makeToolkitHostingController( router: NavigationRouter(store: store, logger: logger), viewFactory: viewFactory) self.viewController = viewController present(viewController) self.pipManager?.reset() } /// Controls if CallComposite UI is hidder. If CallComosite is created with enableSystemPipWhenMultitasking /// set to true, then setting isHidden to true will start syspem Picture-in-Picture view. public var isHidden: Bool { get { guard let store = self.store else { return true } return store.state.visibilityState.currentStatus != .visible } set(isHidden) { if isHidden { if self.enableSystemPipWhenMultitasking { store?.dispatch(action: .visibilityAction(.pipModeRequested)) } else if self.enableMultitasking { store?.dispatch(action: .visibilityAction(.hideRequested)) } } else { displayCallCompositeIfWasHidden() } } } private func hide() { self.viewController?.dismissSelf() self.viewController = nil if self.enableSystemPipWhenMultitasking && store?.state.navigationState.status == .inCall { store?.dispatch(action: .visibilityAction(.pipModeRequested)) } } // swiftlint:disable function_body_length private func constructViewFactoryAndDependencies( for callConfiguration: CallConfiguration, localOptions: LocalOptions?, callCompositeEventsHandler: CallComposite.Events, withCallingSDKWrapper wrapper: CallingSDKWrapperProtocol? = nil ) -> CompositeViewFactoryProtocol { let callingSDKEventsHandler = CallingSDKEventsHandler(logger: logger) self.callingSDKEventsHandler = callingSDKEventsHandler let callingSdkWrapper = wrapper ?? CallingSDKWrapper( logger: logger, callingEventsHandler: callingSDKEventsHandler, callConfiguration: callConfiguration, callKitRemoteInfo: callKitRemoteInfo, callingSDKInitializer: getCallingSDKInitializer()) self.callingSDKWrapper = callingSdkWrapper let store = Store.constructStore( logger: logger, callingService: CallingService(logger: logger, callingSDKWrapper: callingSdkWrapper), displayName: localOptions?.participantViewData?.displayName ?? displayName, startWithCameraOn: localOptions?.cameraOn, startWithMicrophoneOn: localOptions?.microphoneOn, skipSetupScreen: localOptions?.skipSetupScreen, callType: callConfiguration.compositeCallType, setupScreenOptions: localOptions?.setupScreenOptions, callScreenOptions: localOptions?.callScreenOptions ) self.store = store // Construct managers let avatarViewManager = AvatarViewManager( store: store, localParticipantId: userId ?? createCommunicationIdentifier(fromRawId: ""), localParticipantViewData: localOptions?.participantViewData ) self.avatarViewManager = avatarViewManager let audioSessionManager = AudioSessionManager(store: store, logger: logger, isCallKitEnabled: callKitOptions != nil) self.errorManager = CompositeErrorManager(store: store, callCompositeEventsHandler: callCompositeEventsHandler) self.callStateManager = CallStateManager(store: store, callCompositeEventsHandler: callCompositeEventsHandler) self.exitManager = CompositeExitManager(store: store, callCompositeEventsHandler: callCompositeEventsHandler) self.lifeCycleManager = UIKitAppLifeCycleManager(store: store, logger: logger) self.permissionManager = PermissionsManager(store: store) self.audioSessionManager = audioSessionManager self.remoteParticipantsManager = RemoteParticipantsManager( store: store, callCompositeEventsHandler: callCompositeEventsHandler, avatarViewManager: avatarViewManager ) let debugInfoManager = createDebugInfoManager(callingSDKWrapper: callingSdkWrapper) self.debugInfoManager = debugInfoManager let videoViewManager = VideoViewManager(callingSDKWrapper: callingSdkWrapper, logger: logger) self.videoViewManager = videoViewManager let updatableOptionsManager = UpdatableOptionsManager( store: store, setupScreenOptions: setupScreenOptions, callScreenOptions: callScreenOptions ) self.updatableOptionsManager = updatableOptionsManager if enableSystemPipWhenMultitasking { self.pipManager = createPipManager(store) } self.callHistoryService = CallHistoryService(store: store, callHistoryRepository: self.callHistoryRepository) let captionsRttDataManager = CaptionsRttDataManager( store: store, callingSDKWrapper: callingSdkWrapper ) return CompositeViewFactory( logger: logger, avatarManager: avatarViewManager, videoViewManager: videoViewManager, compositeViewModelFactory: CompositeViewModelFactory( logger: logger, store: store, networkManager: NetworkManager(), audioSessionManager: audioSessionManager, localizationProvider: localizationProvider, accessibilityProvider: accessibilityProvider, debugInfoManager: debugInfoManager, captionsRttDataManager: captionsRttDataManager, localOptions: localOptions, enableMultitasking: enableMultitasking, enableSystemPipWhenMultitasking: enableSystemPipWhenMultitasking, eventsHandler: events, leaveCallConfirmationMode: leaveCallConfirmationMode, callType: callConfiguration.compositeCallType, setupScreenOptions: setupScreenOptions, callScreenOptions: callScreenOptions, capabilitiesManager: CapabilitiesManager(callType: callConfiguration.compositeCallType), avatarManager: avatarViewManager, themeOptions: themeOptions ?? ThemeColor(), updatableOptionsManager: updatableOptionsManager, retrieveLogFiles: callingSdkWrapper.getLogFiles ) ) } private func createDebugInfoManager(callingSDKWrapper: CallingSDKWrapperProtocol) -> DebugInfoManagerProtocol { return DebugInfoManager(callHistoryRepository: self.callHistoryRepository, getLogFiles: { return callingSDKWrapper.getLogFiles() }) } private func createDebugInfoManager() -> DebugInfoManagerProtocol { return DebugInfoManager(callHistoryRepository: self.callHistoryRepository, getLogFiles: { return [] }) } private func cleanUpManagers() { self.errorManager = nil self.callStateManager = nil self.lifeCycleManager = nil self.permissionManager = nil self.audioSessionManager = nil self.avatarViewManager = nil self.remoteParticipantsManager = nil self.debugInfoManager = nil self.pipManager = nil self.callHistoryService = nil self.exitManager = nil self.callingSDKWrapper?.dispose() self.callingSDKWrapper = nil self.updatableOptionsManager = nil } private func disposeSDKWrappers() { self.callingSDKEventsHandler = nil self.callingSDKWrapper = nil self.callingSDKInitializer?.dispose() self.callingSDKInitializer = nil } private func present(_ viewController: UIViewController) { store?.dispatch(action: .visibilityAction(.showNormalEntered)) Task { @MainActor in guard self.isCompositePresentable(), let topViewController = UIWindow.keyWindow?.topViewController else { // go to throw the error in the delegate handler return } topViewController.present(viewController, animated: true, completion: nil) } } private func setupColorTheming() { let colorProvider = ColorThemeProvider(themeOptions: themeOptions) StyleProvider.color = colorProvider Task { @MainActor in if let window = UIWindow.keyWindow { Colors.setProvider(provider: colorProvider, for: window) } } } private func setupLocalization(with provider: LocalizationProviderProtocol) { if let localizationOptions = localizationOptions { provider.apply(localeConfig: localizationOptions) } } private func isCompositePresentable() -> Bool { guard let keyWindow = UIWindow.keyWindow else { return false } let hasCallComposite = keyWindow.hasViewController(ofKind: ContainerUIHostingController.self) return !hasCallComposite } private func getCallingSDKInitializer() -> CallingSDKInitializer { if let callingSDKInitializer = callingSDKInitializer { return callingSDKInitializer } guard let credential = credential else { if let didFail = events.onError { let compositeError = CallCompositeError( code: CallCompositeErrorCode.communicationTokenCredentialNotSet, error: CommunicationTokenCredentialError.communicationTokenCredentialNotSet) didFail(compositeError) } fatalError("CommunicationTokenCredential cannot be nil, use init with credentials.") } let callingSDKInitializer = CallingSDKInitializer(tags: self.callConfiguration?.diagnosticConfig.tags ?? DiagnosticConfig().tags, credential: credential, callKitOptions: callKitOptions, displayName: displayName, disableInternalPushForIncomingCall: disableInternalPushForIncomingCall, logger: logger, events: events, onCallAdded: onCallAdded) self.callingSDKInitializer = callingSDKInitializer return callingSDKInitializer } } // swiftlint:enable function_body_length extension CallComposite { private func receiveStoreEvents(_ store: Store<AppState, Action>) { store.$state .receive(on: DispatchQueue.main) .sink { [weak self] state in self?.receive(state) }.store(in: &cancellables) } private func receive(_ state: AppState) { if state.visibilityState.currentStatus == .hideRequested { store?.dispatch(action: .visibilityAction(.hideEntered)) hide() } /* <CALL_START_TIME> if let onCallStartTimeUpdated = events.onCallStartTimeUpdated, let newCallStartTime = state.callingState.callStartTime, callStartTimeInternal != newCallStartTime { self.callStartTimeInternal = newCallStartTime onCallStartTimeUpdated(newCallStartTime) } </CALL_START_TIME> */ } private func makeToolkitHostingController(router: NavigationRouter, viewFactory: CompositeViewFactoryProtocol) -> ContainerUIHostingController { let isRightToLeft = localizationProvider.isRightToLeft let setupViewOrientationMask = orientationProvider.orientationMask(for: setupViewOrientationOptions) let callingViewOrientationMask = orientationProvider.orientationMask(for: callingViewOrientationOptions) let rootView = ContainerView(router: router, logger: logger, viewFactory: viewFactory, setupViewOrientationMask: setupViewOrientationMask, callingViewOrientationMask: callingViewOrientationMask, isRightToLeft: isRightToLeft) let containerUIHostingController = ContainerUIHostingController(rootView: rootView, callComposite: self, isRightToLeft: isRightToLeft) containerUIHostingController.modalPresentationStyle = .fullScreen router.setDismissComposite { [weak containerUIHostingController, weak self] in self?.logger.debug( "CallComposite setDismissComposite") self?.disposeSDKWrappers() self?.callStateManager?.onCompositeExit() self?.viewController = nil self?.pipViewController = nil self?.viewFactory = nil UIApplication.shared.isIdleTimerDisabled = false let exitManagerCache = self?.exitManager self?.cleanUpManagers() if let hostingController = containerUIHostingController { hostingController.dismissSelf { self?.videoViewManager?.disposeViews() self?.logger.debug( "CallComposite hostingController dismissed") self?.compositeUILaunched = false exitManagerCache?.onDismissed() self?.notifyOnCallKitCallAccepted() } } else { self?.videoViewManager?.disposeViews() self?.compositeUILaunched = false exitManagerCache?.onDismissed() self?.notifyOnCallKitCallAccepted() } } return containerUIHostingController } private func createPipManager(_ store: Store<AppState, Action>) -> PipManager? { return PipManager(store: store, logger: logger, onRequirePipContentView: { guard let store = self.store, let viewFactory = self.viewFactory else { return nil } let viewController = self.makeToolkitHostingController( router: NavigationRouter(store: store, logger: self.logger), viewFactory: viewFactory) self.pipViewController = viewController return viewController.view }, onRequirePipPlaceholderView: { return self.viewController?.view }, onPipStarted: { self.viewController?.dismissSelf(animated: false) self.viewController = nil self.events.onPictureInPictureChanged?(true) }, onPipStoped: { self.pipViewController?.dismissSelf() self.displayCallCompositeIfWasHidden() }, onPipStartFailed: { self.viewController?.dismissSelf() self.viewController = nil }) } } // swiftlint:enable type_body_length