AzureCommunicationUI/AzureCommunicationUIDemoApp/Sources/Views/CallingDemoView.swift (1,033 lines of code) (raw):

// // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // import SwiftUI import AzureCommunicationCommon import AVFoundation import CallKit import OSLog #if DEBUG @testable import AzureCommunicationUICalling #else import AzureCommunicationUICalling #endif struct CallingDemoView: View { @State var isAlertDisplayed = false @State var isSettingsDisplayed = false @State var isStartExperienceLoading = false @State var exitCompositeExecuted = false @State var isIncomingCall = false @State var alertTitle: String = "" @State var alertMessage: String = "" @State var callState: String = "" @State var issue: CallCompositeUserReportedIssue? @State var issueUrl: String = "" @State var isCallActive = false @State private var isNewViewPresented = false @ObservedObject var envConfigSubject: EnvConfigSubject @ObservedObject var callingViewModel: CallingDemoViewModel @State var incomingCallId = "" @State var headerViewData: CallScreenHeaderViewData? let verticalPadding: CGFloat = 5 let horizontalPadding: CGFloat = 10 var callComposite = CallComposite() @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> #if DEBUG var callingSDKWrapperMock: UITestCallingSDKWrapper? #endif var body: some View { VStack { #if DEBUG // This HStack is for testing toggles. // Adjusted to make buttons invisible but still accessible for automation. HStack { Button("AudioOnly") { envConfigSubject.audioOnly = !envConfigSubject.audioOnly } .frame(width: 1, height: 1) .accessibilityIdentifier(AccessibilityId.toggleAudioOnlyModeAccessibilityID.rawValue) Button("MockSdk") { envConfigSubject.useMockCallingSDKHandler = !envConfigSubject.useMockCallingSDKHandler } .frame(width: 1, height: 1) .accessibilityIdentifier(AccessibilityId.useMockCallingSDKHandlerToggleAccessibilityID.rawValue) } #endif Text("UI Library - SwiftUI Sample") Spacer() acsTokenSelector displayNameTextField userIdTextField meetingSelector Group { settingButton showCallHistoryButton startExperienceButton showExperienceButton HStack { registerPushNotificationButton unregisterPushNotificationButton } if isIncomingCall { HStack { acceptCallButton declineCallButton } } Text(callState) Text(issue?.userMessage ?? "--") .accessibilityIdentifier(AccessibilityId.userReportedIssueAccessibilityID.rawValue) if !issueUrl.isEmpty { Link("Ticket Link", destination: URL(string: issueUrl)!) } } Spacer() } .padding() .alert(isPresented: $isAlertDisplayed) { Alert( title: Text(alertTitle), message: Text(alertMessage), primaryButton: .default(Text("Copy")) { UIPasteboard.general.string = alertMessage }, secondaryButton: .default(Text("Dismiss"), action: { isAlertDisplayed = false })) } .sheet(isPresented: $isSettingsDisplayed) { SettingsView(envConfigSubject: envConfigSubject) } .fullScreenCover(isPresented: $isNewViewPresented) { CustomDemoView() } #if DEBUG .onAppear(perform: { // Dev helper to jump through to mocked experiences Task { if EnvConfig.skipTo.value() == "MockCallScreen" { envConfigSubject.useMockCallingSDKHandler = true envConfigSubject.skipSetupScreen = true await startCallComposite() } else if EnvConfig.skipTo.value() == "MockSetupScreen" { envConfigSubject.useMockCallingSDKHandler = true envConfigSubject.skipSetupScreen = false await startCallComposite() } else if EnvConfig.skipTo.value() == "TeamsCallScreen" { envConfigSubject.enableCallKit = false envConfigSubject.selectedMeetingType = .teamsMeeting envConfigSubject.skipSetupScreen = true await startCallComposite() } else if EnvConfig.skipTo.value() == "TeamsSetupScreen" { envConfigSubject.enableCallKit = false envConfigSubject.selectedMeetingType = .teamsMeeting envConfigSubject.skipSetupScreen = false await startCallComposite() } else if EnvConfig.skipTo.value() == "GroupCallScreen" { envConfigSubject.enableCallKit = false envConfigSubject.selectedMeetingType = .groupCall envConfigSubject.skipSetupScreen = true await startCallComposite() } else if EnvConfig.skipTo.value() == "GroupSetupScreen" { envConfigSubject.enableCallKit = false envConfigSubject.selectedMeetingType = .groupCall envConfigSubject.skipSetupScreen = false await startCallComposite() } } }) #endif } var acsTokenSelector: some View { Group { Picker("Token Type", selection: $envConfigSubject.selectedAcsTokenType) { Text("Token URL").tag(ACSTokenType.tokenUrl) Text("Token").tag(ACSTokenType.token) }.pickerStyle(.segmented) switch envConfigSubject.selectedAcsTokenType { case .tokenUrl: TextField("ACS Token URL", text: $envConfigSubject.acsTokenUrl) .disableAutocorrection(true) .autocapitalization(.none) .textFieldStyle(.roundedBorder) case .token: TextField("ACS Token", text: !envConfigSubject.useExpiredToken ? $envConfigSubject.acsToken : $envConfigSubject.expiredAcsToken) .modifier(TextFieldClearButton(text: $envConfigSubject.acsToken)) .disableAutocorrection(true) .autocapitalization(.none) .textFieldStyle(.roundedBorder) } } .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) } var displayNameTextField: some View { TextField("Display Name", text: $envConfigSubject.displayName) .disableAutocorrection(true) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) .textFieldStyle(.roundedBorder) } var userIdTextField: some View { TextField("User Identifier", text: $envConfigSubject.userId) .disableAutocorrection(true) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) .textFieldStyle(.roundedBorder) } var meetingSelector: some View { Group { Picker("Call Type", selection: $envConfigSubject.selectedMeetingType) { Text("Group Call").tag(MeetingType.groupCall) Text("Teams Meeting").tag(MeetingType.teamsMeeting) Text("1:N Call").tag(MeetingType.oneToNCall) Text("Room Call").tag(MeetingType.roomCall) }.pickerStyle(.segmented) switch envConfigSubject.selectedMeetingType { case .groupCall: TextField( "Group Call Id", text: $envConfigSubject.groupCallId) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) case .teamsMeeting: TextField( "Team Meeting Link", text: $envConfigSubject.teamsMeetingLink) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) TextField( "Team Meeting Id", text: $envConfigSubject.teamsMeetingId) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) TextField( "Team Meeting Passcode", text: $envConfigSubject.teamsMeetingPasscode) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) case .oneToNCall: TextField( "participant MRIs(, separated)", text: $envConfigSubject.participantMRIs) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) case .roomCall: TextField( "Room Id", text: $envConfigSubject.roomId) .autocapitalization(.none) .disableAutocorrection(true) .textFieldStyle(.roundedBorder) } } .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) } var settingButton: some View { Button("Settings") { isSettingsDisplayed = true } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.settingsButtonAccessibilityID.rawValue) } var startExperienceButton: some View { Button("Start Experience") { isStartExperienceLoading = true isCallActive = true // Disable button until dismissed Task { @MainActor in await startCallComposite() isStartExperienceLoading = false } } .buttonStyle(DemoButtonStyle()) .disabled(isStartExperienceDisabled || isStartExperienceLoading || isCallActive) .accessibility(identifier: AccessibilityId.startExperienceAccessibilityID.rawValue) } var showExperienceButton: some View { Button("Show") { showCallComposite() } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.showExperienceAccessibilityID.rawValue) } var registerPushNotificationButton: some View { Button("Register push") { registerPushNotification() } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.registerPushAccessibilityID.rawValue) } var unregisterPushNotificationButton: some View { Button("Unregister push") { unregisterPushNotification() } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.unregisterPushAccessibilityID.rawValue) } var showCallHistoryButton: some View { Button("Show call history") { alertTitle = callingViewModel.callHistoryTitle alertMessage = callingViewModel.callHistoryMessage isAlertDisplayed = true } .buttonStyle(DemoButtonStyle()) } var acceptCallButton: some View { Button("Accept") { accept() } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.acceptCallAccessibilityID.rawValue) } var declineCallButton: some View { Button("Decline") { decline() } .buttonStyle(DemoButtonStyle()) .accessibility(identifier: AccessibilityId.declineCallAccessibilityID.rawValue) } var isStartExperienceDisabled: Bool { let acsToken = envConfigSubject.useExpiredToken ? envConfigSubject.expiredAcsToken : envConfigSubject.acsToken if (envConfigSubject.selectedAcsTokenType == .token && acsToken.isEmpty) || envConfigSubject.selectedAcsTokenType == .tokenUrl && envConfigSubject.acsTokenUrl.isEmpty { return true } if envConfigSubject.selectedMeetingType == .groupCall && envConfigSubject.groupCallId.isEmpty { return true } else if envConfigSubject.selectedMeetingType == .teamsMeeting { // Check if teamsMeetingLink is not empty or both meetingId and passcode are not empty let isTeamsMeetingLinkValid = !envConfigSubject.teamsMeetingLink.isEmpty let isTeamsMeetingIdAndPasscodeValid = !envConfigSubject.teamsMeetingId.isEmpty && !envConfigSubject.teamsMeetingPasscode.isEmpty return !isTeamsMeetingLinkValid && !isTeamsMeetingIdAndPasscodeValid } return false } } extension CallingDemoView { func showCallComposite() { callingViewModel.callComposite?.isHidden = false } func registerPushNotification() { Task { guard let token = $envConfigSubject.deviceToken.wrappedValue else { showAlert(for: "deviceToken not found") return } await createCallComposite()? .registerPushNotifications( deviceRegistrationToken: token) { result in switch result { case .success: showAlert(for: "Register Voip Success") case .failure(let error): showAlert(for: "Register Voip fail: \(error.localizedDescription)") } } } } func unregisterPushNotification() { Task { await createCallComposite()? .unregisterPushNotifications { result in switch result { case .success: showAlert(for: "Unregister Voip Success") case .failure(let error): showAlert(for: "Unregister Voip fail: \(error.localizedDescription)") } } } } func accept() { Task { var remoteInfoDisplayName = envConfigSubject.callkitRemoteInfo if remoteInfoDisplayName.isEmpty { remoteInfoDisplayName = "ACS \(envConfigSubject.selectedMeetingType)" } let cxHandle = CXHandle(type: .generic, value: getCXHandleName()) let callKitRemoteInfo = $envConfigSubject.enableRemoteInfo.wrappedValue ? CallKitRemoteInfo(displayName: remoteInfoDisplayName, handle: cxHandle) : nil isIncomingCall = false await createCallComposite()?.accept(incomingCallId: incomingCallId, callKitRemoteInfo: callKitRemoteInfo, localOptions: getLocalOptions()) } } func decline() { Task { isIncomingCall = false await createCallComposite()?.reject(incomingCallId: incomingCallId) { result in switch result { case .success: showAlert(for: "Reject Success") case .failure(let error): showAlert(for: "Reject fail: \(error.localizedDescription)") } } } } fileprivate func relaunchComposite() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { Task { @MainActor in await startCallComposite() isStartExperienceLoading = false } } } func createCallComposite() async -> CallComposite? { print("CallingDemoView:::: createCallComposite requesting") if GlobalCompositeManager.callComposite != nil { print("CallingDemoView:::: createCallComposite exist") return GlobalCompositeManager.callComposite! } print("CallingDemoView:::: createCallComposite creating") var localizationConfig: LocalizationOptions? let layoutDirection: LayoutDirection = envConfigSubject.isRightToLeft ? .rightToLeft : .leftToRight let barOptions = CallScreenControlBarOptions(leaveCallConfirmationMode: envConfigSubject.displayLeaveCallConfirmation ? .alwaysEnabled : .alwaysDisabled) let setupScreenOptions = SetupScreenOptions( cameraButtonEnabled: envConfigSubject.setupScreenOptionsCameraButtonEnabled, microphoneButtonEnabled: envConfigSubject.setupScreenOptionsMicButtonEnabled) headerViewData = CallScreenHeaderViewData() if !envConfigSubject.callInformationTitle.isEmpty { headerViewData?.title = envConfigSubject.callInformationTitle } if !envConfigSubject.callInformationSubtitle.isEmpty { headerViewData?.subtitle = envConfigSubject.callInformationSubtitle } var callScreenOptions = CallScreenOptions(controlBarOptions: barOptions, headerViewData: headerViewData) if !envConfigSubject.localeIdentifier.isEmpty { let locale = Locale(identifier: envConfigSubject.localeIdentifier) localizationConfig = LocalizationOptions(locale: locale, layoutDirection: layoutDirection) } else if !envConfigSubject.locale.identifier.isEmpty { localizationConfig = LocalizationOptions( locale: envConfigSubject.locale, layoutDirection: layoutDirection) } let setupViewOrientation = envConfigSubject.setupViewOrientation let callingViewOrientation = envConfigSubject.callingViewOrientation let callKitOptions = $envConfigSubject.enableCallKit.wrappedValue ? getCallKitOptions() : nil let userId = CommunicationUserIdentifier(envConfigSubject.userId) let callCompositeOptions = envConfigSubject.useDeprecatedLaunch ? CallCompositeOptions( theme: envConfigSubject.useCustomColors ? CustomColorTheming(envConfigSubject: envConfigSubject) : Theming(envConfigSubject: envConfigSubject), localization: localizationConfig, setupScreenOrientation: setupViewOrientation, callingScreenOrientation: callingViewOrientation, enableMultitasking: envConfigSubject.enableMultitasking, enableSystemPictureInPictureWhenMultitasking: envConfigSubject.enablePipWhenMultitasking, callScreenOptions: callScreenOptions, callKitOptions: callKitOptions, setupScreenOptions: setupScreenOptions) : CallCompositeOptions( theme: envConfigSubject.useCustomColors ? CustomColorTheming(envConfigSubject: envConfigSubject) : Theming(envConfigSubject: envConfigSubject), localization: localizationConfig, setupScreenOrientation: setupViewOrientation, callingScreenOrientation: callingViewOrientation, enableMultitasking: envConfigSubject.enableMultitasking, enableSystemPictureInPictureWhenMultitasking: envConfigSubject.enablePipWhenMultitasking, callScreenOptions: callScreenOptions, callKitOptions: callKitOptions, displayName: envConfigSubject.displayName, disableInternalPushForIncomingCall: envConfigSubject.disableInternalPushForIncomingCall, setupScreenOptions: setupScreenOptions) let useMockCallingSDKHandler = envConfigSubject.useMockCallingSDKHandler if let credential = try? await getTokenCredential() { #if DEBUG let callComposite = useMockCallingSDKHandler ? CallComposite(withOptions: callCompositeOptions, callingSDKWrapperProtocol: callingSDKWrapperMock) : ( envConfigSubject.useDeprecatedLaunch ? CallComposite(withOptions: callCompositeOptions) : CallComposite(credential: credential, withOptions: callCompositeOptions)) callingSDKWrapperMock?.callComposite = callComposite #else let callComposite = envConfigSubject.useDeprecatedLaunch ? CallComposite(withOptions: callCompositeOptions) : CallComposite(credential: credential, withOptions: callCompositeOptions) #endif subscribeToEvents(callComposite: callComposite) GlobalCompositeManager.callComposite = callComposite self.envConfigSubject.saveFromState() return callComposite } return nil } func onPushNotificationReceived(dictionaryPayload: [AnyHashable: Any]) { let pushNotificationInfo = PushNotification(data: dictionaryPayload) os_log("calling demo app: onPushNotificationReceived CallingDemoView") if envConfigSubject.acsToken.isEmpty { os_log("calling demo app: envConfigSubject acs token is empty") self.envConfigSubject.load() } Task { await createCallComposite()?.handlePushNotification(pushNotification: pushNotificationInfo) } } func subscribeToEvents(callComposite: CallComposite) { let onRemoteParticipantJoinedHandler: ([CommunicationIdentifier]) -> Void = { [weak callComposite] ids in guard let composite = callComposite else { return } self.onRemoteParticipantJoined(to: composite, identifiers: ids) } let onErrorHandler: (CallCompositeError) -> Void = { [weak callComposite] error in guard let composite = callComposite else { return } onError(error, callComposite: composite) } let onPipChangedHandler: (Bool) -> Void = { isPictureInPicture in print("::::CallingDemoView:onPipChangedHandler: ", isPictureInPicture) } let onUserReportedIssueHandler: (CallCompositeUserReportedIssue) -> Void = { issue in DispatchQueue.main.schedule { self.issue = issue } sendSupportEventToServer(event: issue) { success, result in if success { self.issueUrl = result } else { self.issueUrl = "" } } } let onCallStateChangedHandler: (CallState) -> Void = { [weak callComposite] callStateEvent in guard let composite = callComposite else { return } onCallStateChanged(callStateEvent, callComposite: composite) } let onDismissedHandler: (CallCompositeDismissed) -> Void = { [] _ in if envConfigSubject.useRelaunchOnDismissedToggle && exitCompositeExecuted { relaunchComposite() } isCallActive = false // Re-enable button when call ends print("::::CallingDemoView::onDismissedHandler") } exitCompositeExecuted = false if !envConfigSubject.exitCompositeAfterDuration.isEmpty { DispatchQueue.main.asyncAfter(deadline: .now() + Float64(envConfigSubject.exitCompositeAfterDuration)! ) { [weak callComposite] in exitCompositeExecuted = true callComposite?.dismiss() } } let callKitCallAccepted: (String) -> Void = { [weak callComposite] callId in isIncomingCall = false callComposite?.launch(callIdAcceptedFromCallKit: callId, localOptions: getLocalOptions()) } let onIncomingCall: (IncomingCall) -> Void = { [] incomingCall in incomingCallId = incomingCall.callId isIncomingCall = true print("::::CallingDemoView::onIncomingCall \(incomingCall.callId)") } let onIncomingCallCancelled: (IncomingCallCancelled) -> Void = { [] event in isIncomingCall = false print("::::CallingDemoView::onIncomingCallCancelled \(event.callId)") showAlert(for: "\(event.callId) cancelled") } let onRemoteParticipantLeftHandler: ([CommunicationIdentifier]) -> Void = { [weak callComposite] ids in guard let composite = callComposite else { return } self.onRemoteParticipantLeft(to: composite, identifiers: ids) } /* <CALL_START_TIME> let onCallStartTimeUpdated: (Date) -> Void = { [] startTime in let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.timeZone = TimeZone.current let systemTimeZoneDateString = dateFormatter.string(from: startTime) print("::::CallingDemoView startTime event call start time \(systemTimeZoneDateString)") } </CALL_START_TIME> */ callComposite.events.onRemoteParticipantJoined = onRemoteParticipantJoinedHandler callComposite.events.onError = onErrorHandler callComposite.events.onCallStateChanged = onCallStateChangedHandler callComposite.events.onDismissed = onDismissedHandler callComposite.events.onPictureInPictureChanged = onPipChangedHandler callComposite.events.onUserReportedIssue = onUserReportedIssueHandler callComposite.events.onIncomingCallAcceptedFromCallKit = callKitCallAccepted callComposite.events.onIncomingCall = onIncomingCall callComposite.events.onIncomingCallCancelled = onIncomingCallCancelled callComposite.events.onRemoteParticipantLeft = onRemoteParticipantLeftHandler /* <CALL_START_TIME> callComposite.events.onCallStartTimeUpdated = onCallStartTimeUpdated </CALL_START_TIME> */ } func getLocalOptions(callComposite: CallComposite? = nil) -> LocalOptions { let renderDisplayName = envConfigSubject.renderedDisplayName.isEmpty ? nil : envConfigSubject.renderedDisplayName let participantViewData = ParticipantViewData(avatar: UIImage(named: envConfigSubject.avatarImageName), displayName: renderDisplayName) let setupScreenViewData = SetupScreenViewData(title: envConfigSubject.navigationTitle, subtitle: envConfigSubject.navigationSubtitle) var setupScreenOptions: SetupScreenOptions? if envConfigSubject.addCustomButton { let micButton = ButtonViewData() let audioDeviceButton = ButtonViewData() let cameraButton = ButtonViewData(onClick: { _ in micButton.visible = !micButton.visible audioDeviceButton.enabled = !audioDeviceButton.enabled }) setupScreenOptions = SetupScreenOptions( cameraButton: cameraButton, microphoneButton: micButton, audioDeviceButton: audioDeviceButton ) } let captionsOptions = CaptionsOptions(captionsOn: envConfigSubject.captionsOn, spokenLanguage: envConfigSubject.spokenLanguage) let callScreenOptions = createCallScreenOptions(callComposite: callComposite) return LocalOptions(participantViewData: participantViewData, setupScreenViewData: setupScreenViewData, cameraOn: envConfigSubject.cameraOn, microphoneOn: envConfigSubject.microphoneOn, skipSetupScreen: envConfigSubject.skipSetupScreen, audioVideoMode: envConfigSubject.audioOnly ? .audioOnly : .audioAndVideo, captionsOptions: captionsOptions, setupScreenOptions: setupScreenOptions, callScreenOptions: callScreenOptions ) } private func createCallScreenOptions(callComposite: CallComposite?) -> CallScreenOptions { // Safely unwrap the image and apply the tint color using the color set named "ChevronColor" var callScreenControlBarOptions: CallScreenControlBarOptions? if envConfigSubject.addCustomButton { let customButtonImage: UIImage if let image = UIImage(named: "ic_fluent_chevron_right_20_regular") { customButtonImage = image.withRenderingMode(.alwaysOriginal) } else { customButtonImage = UIImage().withTintColor(.black) // Fallback to a plain image with black tint print("Error: Image 'ic_fluent_chevron_right_20_regular' not found") } let cameraButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::cameraButton::onClick") }) let micButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::micButton::onClick") }) let audioDeviceButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::audioDeviceButton::onClick") }) let liveCaptionsButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::liveCaptionsButton::onClick") }) let liveCaptionsToggleButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::liveCaptionsToggleButton::onClick") }) let spokenLanguageButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::spokenLanguageButton::onClick") }) let captionsLanguageButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::captionsLanguageButton::onClick") }) let shareDiagnostisButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::shareDiagnostisButton::onClick") }) let reportIssueButton = ButtonViewData(onClick: { _ in print("::::SwiftUIDemoView::CallScreen::reportIssueButton::onClick") }) // Create the custom button with the tinted image let customButton1 = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Hide composite" ) { _ in print("::::SwiftUIDemoView::CallScreen::customButton1::onClick") callComposite?.isHidden = true } let hideButtonsCustomButton = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Hide/show buttons" ) { _ in print("::::SwiftUIDemoView::CallScreen::hideButtonsCustomButton::onClick") cameraButton.visible = !cameraButton.visible micButton.visible = !micButton.visible audioDeviceButton.visible = !audioDeviceButton.visible liveCaptionsButton.visible = !liveCaptionsButton.visible liveCaptionsToggleButton.visible = !liveCaptionsToggleButton.visible spokenLanguageButton.visible = !spokenLanguageButton.visible captionsLanguageButton.visible = !captionsLanguageButton.visible shareDiagnostisButton.visible = !shareDiagnostisButton.visible reportIssueButton.visible = !reportIssueButton.visible customButton1.visible = !customButton1.visible } let disableButtonsCustomButton = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Disable/enable buttons" ) { _ in print("::::SwiftUIDemoView::CallScreen::hideButtonsCustomButton::onClick") cameraButton.enabled = !cameraButton.enabled micButton.enabled = !micButton.enabled audioDeviceButton.enabled = !audioDeviceButton.enabled liveCaptionsButton.enabled = !liveCaptionsButton.enabled liveCaptionsToggleButton.enabled = !liveCaptionsToggleButton.enabled spokenLanguageButton.enabled = !spokenLanguageButton.enabled captionsLanguageButton.enabled = !captionsLanguageButton.enabled shareDiagnostisButton.enabled = !shareDiagnostisButton.enabled reportIssueButton.enabled = !reportIssueButton.enabled customButton1.enabled = !customButton1.enabled } let customButton2 = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Troubleshooting tips" ) { _ in print("::::SwiftUIDemoView::CallScreen::customButton2::onClick") callComposite?.isHidden = true $isNewViewPresented.wrappedValue = true } callScreenControlBarOptions = CallScreenControlBarOptions( leaveCallConfirmationMode: envConfigSubject.displayLeaveCallConfirmation ? .alwaysEnabled : .alwaysDisabled, cameraButton: cameraButton, microphoneButton: micButton, audioDeviceButton: audioDeviceButton, liveCaptionsButton: liveCaptionsButton, liveCaptionsToggleButton: liveCaptionsToggleButton, spokenLanguageButton: spokenLanguageButton, captionsLanguageButton: captionsLanguageButton, shareDiagnosticsButton: shareDiagnostisButton, reportIssueButton: reportIssueButton, customButtons: [hideButtonsCustomButton, disableButtonsCustomButton, customButton1, customButton2] ) } var headerViewData: CallScreenHeaderViewData? if envConfigSubject.addCustomButton { let customButtonImage: UIImage if let image = UIImage(named: "ic_fluent_chat_20_regular") { customButtonImage = image } else { customButtonImage = UIImage().withTintColor(.white) } let customButton1 = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Header custom button 1" ) { _ in print("::::SwiftUIDemoView::CallScreenHeader::customButton1::onClick") } let customButton2 = CustomButtonViewData( id: UUID().uuidString, image: customButtonImage, title: "Header custom button 2" ) { _ in print("::::SwiftUIDemoView::CallScreenHeader::customButton2::onClick") } headerViewData = CallScreenHeaderViewData(customButtons: [customButton1, customButton2]) } if !envConfigSubject.callInformationTitle.isEmpty { headerViewData = headerViewData ?? CallScreenHeaderViewData() headerViewData?.title = envConfigSubject.callInformationTitle } if !envConfigSubject.callInformationSubtitle.isEmpty { headerViewData = headerViewData ?? CallScreenHeaderViewData() headerViewData?.subtitle = envConfigSubject.callInformationSubtitle } return CallScreenOptions(controlBarOptions: callScreenControlBarOptions, headerViewData: headerViewData) } func startCallWithDeprecatedLaunch() async { if let credential = try? await getTokenCredential(), let callComposite = try? await createCallComposite() { let link = getMeetingLink() var localOptions = getLocalOptions(callComposite: callComposite) switch envConfigSubject.selectedMeetingType { case .groupCall: let uuid = try? parseUUID(from: link) // Checking if UUID parsing was successful if let uuid = uuid { if envConfigSubject.displayName.isEmpty { // Launch call composite without displayName callComposite.launch(remoteOptions: RemoteOptions(for: .groupCall(groupId: uuid), credential: credential), localOptions: localOptions) } else { // Launch call composite with displayName callComposite.launch(remoteOptions: RemoteOptions(for: .groupCall(groupId: uuid), credential: credential, displayName: envConfigSubject.displayName), localOptions: localOptions) } } else { // Handle the case where UUID parsing fails showError(for: DemoError.invalidGroupCallId.getErrorCode()) return } case .teamsMeeting: if !envConfigSubject.teamsMeetingLink.isEmpty { if envConfigSubject.displayName.isEmpty { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeeting(teamsLink: envConfigSubject.teamsMeetingLink), credential: credential), localOptions: localOptions ) } else { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeeting(teamsLink: envConfigSubject.teamsMeetingLink), credential: credential, displayName: envConfigSubject.displayName), localOptions: localOptions ) } } else if !envConfigSubject.teamsMeetingId.isEmpty && !envConfigSubject.teamsMeetingPasscode.isEmpty { if envConfigSubject.displayName.isEmpty { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeetingId(meetingId: envConfigSubject.teamsMeetingId, meetingPasscode: envConfigSubject.teamsMeetingPasscode), credential: credential), localOptions: localOptions ) } else { callComposite.launch( remoteOptions: RemoteOptions(for: .teamsMeetingId(meetingId: envConfigSubject.teamsMeetingId, meetingPasscode: envConfigSubject.teamsMeetingPasscode), credential: credential, displayName: envConfigSubject.displayName), localOptions: localOptions ) } } case .oneToNCall: let ids: [String] = link.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } let communicationIdentifiers: [CommunicationIdentifier] = ids.map { createCommunicationIdentifier(fromRawId: $0) } callComposite.launch(participants: communicationIdentifiers, localOptions: localOptions) case .roomCall: if envConfigSubject.displayName.isEmpty { callComposite.launch(remoteOptions: RemoteOptions(for: .roomCall(roomId: link), credential: credential), localOptions: localOptions) } else { callComposite.launch( remoteOptions: RemoteOptions(for: .roomCall(roomId: link), credential: credential, displayName: envConfigSubject.displayName), localOptions: localOptions) } } } } func startCallComposite() async { let link = getMeetingLink() if let callComposite = try? await createCallComposite() { var localOptions = getLocalOptions(callComposite: callComposite) var remoteInfoDisplayName = envConfigSubject.callkitRemoteInfo if remoteInfoDisplayName.isEmpty { remoteInfoDisplayName = "ACS \(envConfigSubject.selectedMeetingType)" } let cxHandle = CXHandle(type: .generic, value: getCXHandleName()) isIncomingCall = false let callKitRemoteInfo = $envConfigSubject.enableRemoteInfo.wrappedValue ? CallKitRemoteInfo(displayName: remoteInfoDisplayName, handle: cxHandle) : nil if envConfigSubject.useDeprecatedLaunch { try? await startCallWithDeprecatedLaunch() } else { switch envConfigSubject.selectedMeetingType { case .groupCall: let uuid = UUID(uuidString: link) ?? UUID() callComposite.launch(locator: .groupCall(groupId: uuid), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) case .teamsMeeting: if !link.isEmpty { callComposite.launch(locator: .teamsMeeting(teamsLink: link), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } else { callComposite.launch(locator: .teamsMeetingId(meetingId: envConfigSubject.teamsMeetingId, meetingPasscode: envConfigSubject.teamsMeetingPasscode), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } case .oneToNCall: let ids: [String] = link.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } let communicationIdentifiers: [CommunicationIdentifier] = ids.map { createCommunicationIdentifier(fromRawId: $0) } callComposite.launch(participants: communicationIdentifiers, callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) case .roomCall: callComposite.launch( locator: .roomCall(roomId: link), callKitRemoteInfo: callKitRemoteInfo, localOptions: localOptions) } } callingViewModel.callComposite = callComposite } else { showError(for: DemoError.invalidToken.getErrorCode()) return } } private func getCallKitOptions() -> CallKitOptions { let cxHandle = CXHandle(type: .generic, value: getCXHandleName()) let providerConfig = CXProviderConfiguration() providerConfig.supportsVideo = true providerConfig.maximumCallGroups = 1 providerConfig.maximumCallsPerCallGroup = 1 providerConfig.includesCallsInRecents = true providerConfig.supportedHandleTypes = [.phoneNumber, .generic] let isCallHoldSupported = $envConfigSubject.enableRemoteHold.wrappedValue let callKitOptions = CallKitOptions(providerConfig: providerConfig, isCallHoldSupported: isCallHoldSupported, provideRemoteInfo: incomingCallRemoteInfo, configureAudioSession: configureAudioSession) return callKitOptions } public func incomingCallRemoteInfo(info: Caller) -> CallKitRemoteInfo { let cxHandle = CXHandle(type: .generic, value: "Incoming call") var remoteInfoDisplayName = envConfigSubject.callkitRemoteInfo if remoteInfoDisplayName.isEmpty { remoteInfoDisplayName = info.displayName } let callKitRemoteInfo = CallKitRemoteInfo(displayName: remoteInfoDisplayName, handle: cxHandle) return callKitRemoteInfo } public func configureAudioSession() -> Error? { let audioSession = AVAudioSession.sharedInstance() var configError: Error? // Check the current audio output route let currentRoute = audioSession.currentRoute let isUsingSpeaker = currentRoute.outputs.contains { $0.portType == .builtInSpeaker } let isUsingReceiver = currentRoute.outputs.contains { $0.portType == .builtInReceiver } // Only configure the session if necessary (e.g., when not on speaker/receiver) if !isUsingSpeaker && !isUsingReceiver { do { // Keeping default .playAndRecord without forcing speaker try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth]) try audioSession.setActive(true) } catch { configError = error } } return configError } private func getCXHandleName() -> String { switch envConfigSubject.selectedMeetingType { case .groupCall: return "Group call" case .teamsMeeting: return "Teams Metting" case .oneToNCall: return "Outgoing call" case .roomCall: return "Rooms call" } } private func getTokenCredential() async throws -> CommunicationTokenCredential { switch envConfigSubject.selectedAcsTokenType { case .token: let acsToken = envConfigSubject.useExpiredToken ? envConfigSubject.expiredAcsToken : envConfigSubject.acsToken if let communicationTokenCredential = try? CommunicationTokenCredential(token: acsToken) { return communicationTokenCredential } else { throw DemoError.invalidToken } case .tokenUrl: if let url = URL(string: envConfigSubject.acsTokenUrl) { let tokenRefresher = AuthenticationHelper.getCommunicationToken(tokenUrl: url, aadToken: envConfigSubject.aadToken) let initialToken = await AuthenticationHelper.fetchInitialToken(with: tokenRefresher) let communicationTokenRefreshOptions = CommunicationTokenRefreshOptions(initialToken: initialToken, refreshProactively: true, tokenRefresher: tokenRefresher) if let credential = try? CommunicationTokenCredential(withOptions: communicationTokenRefreshOptions) { return credential } } throw DemoError.invalidToken } } private func parseUUID(from link: String) throws -> UUID { guard let uuid = UUID(uuidString: link) else { throw DemoError.invalidGroupCallId } return uuid } private func getMeetingLink() -> String { switch envConfigSubject.selectedMeetingType { case .groupCall: return envConfigSubject.groupCallId case .teamsMeeting: return envConfigSubject.teamsMeetingLink case .oneToNCall: return envConfigSubject.participantMRIs case .roomCall: return envConfigSubject.roomId } } private func showError(for errorCode: String) { switch errorCode { case CallCompositeErrorCode.tokenExpired: alertMessage = "Token is invalid" case CallCompositeErrorCode.microphonePermissionNotGranted: alertMessage = "Microphone Permission is denied" case CallCompositeErrorCode.networkConnectionNotAvailable: alertMessage = "Internet error" default: alertMessage = "Unknown error" } alertTitle = "Error" isAlertDisplayed = true } private func showAlert(for message: String) { alertMessage = message alertTitle = "Alert" isAlertDisplayed = true } private func onError(_ error: CallCompositeError, callComposite: CallComposite) { print("::::CallingDemoView::getEventsHandler::onError \(error)") print("::::CallingDemoView error.code \(error.code)") callingViewModel.callHistory.last?.callIds.forEach { print("::::CallingDemoView call id \($0)") } showError(for: error.code) } private func onCallStateChanged(_ callState: CallState, callComposite: CallComposite) { print("::::CallingDemoView::getEventsHandler::onCallStateChanged \(callState.requestString)") /* <CALL_START_TIME> if let date = callComposite.callStartTime() { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.timeZone = TimeZone.current let systemTimeZoneDateString = dateFormatter.string(from: date) print("::::CallingDemoView call start time \(systemTimeZoneDateString)") } </CALL_START_TIME> */ self.callState = "\(callState.requestString) \(callState.callEndReasonCodeInt) \(callState.callId)" } private func onRemoteParticipantJoined(to callComposite: CallComposite, identifiers: [CommunicationIdentifier]) { print("::::CallingDemoView::getEventsHandler::onRemoteParticipantJoined \(identifiers)") if envConfigSubject.customTitleApplyOnRemoteJoin != 0 && identifiers.count >= envConfigSubject.customTitleApplyOnRemoteJoin { headerViewData?.title = "Custom title: change applied" } if envConfigSubject.customSubtitleApplyOnRemoteJoin != 0 && identifiers.count >= envConfigSubject.customSubtitleApplyOnRemoteJoin { headerViewData?.subtitle = "Custom subtitle: change applied" } guard envConfigSubject.useCustomRemoteParticipantViewData else { return } RemoteParticipantAvatarHelper.onRemoteParticipantJoined(to: callComposite, identifiers: identifiers) // Check identifiers to use the the stop/start timer API based on a specific participant leaves the meeting. } private func onRemoteParticipantLeft(to callComposite: CallComposite, identifiers: [CommunicationIdentifier]) { print("::::CallingDemoView::getEventsHandler::onRemoteParticipantLeft \(identifiers)") // Check identifiers to use the the stop/start timer API based on a specific participant leaves the meeting. } } struct CustomDemoView: View { @Environment(\.presentationMode) var presentationMode var body: some View { VStack { Text("This is a new view presented modally.") .font(.largeTitle) .padding() Button("Dismiss") { presentationMode.wrappedValue.dismiss() } .padding() } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color.blue.opacity(0.1)) } }