From ea1382e801789f8463bfd909799bb6d00bcf1e02 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 9 Jan 2024 16:42:11 +0100 Subject: [PATCH] Refresh view when call is paused --- .../phone-list.imageset/Contents.json | 21 + .../phone-list.imageset/phone-list.svg | 6 + .../phone-transfer.imageset/Contents.json | 21 + .../phone-transfer.svg | 4 + Linphone/Core/CoreContext.swift | 4 - Linphone/Localizable.xcstrings | 21 +- Linphone/Ressources/linphonerc-factory | 2 + .../TelecomManager/ProviderDelegate.swift | 158 ++-- Linphone/TelecomManager/TelecomManager.swift | 282 +++---- Linphone/UI/Call/CallView.swift | 705 ++++++++++-------- .../UI/Call/ViewModel/CallViewModel.swift | 13 +- 11 files changed, 677 insertions(+), 560 deletions(-) create mode 100644 Linphone/Assets.xcassets/phone-list.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/phone-list.imageset/phone-list.svg create mode 100644 Linphone/Assets.xcassets/phone-transfer.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/phone-transfer.imageset/phone-transfer.svg diff --git a/Linphone/Assets.xcassets/phone-list.imageset/Contents.json b/Linphone/Assets.xcassets/phone-list.imageset/Contents.json new file mode 100644 index 000000000..93d7f6f6b --- /dev/null +++ b/Linphone/Assets.xcassets/phone-list.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "phone-list.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/phone-list.imageset/phone-list.svg b/Linphone/Assets.xcassets/phone-list.imageset/phone-list.svg new file mode 100644 index 000000000..d070e2710 --- /dev/null +++ b/Linphone/Assets.xcassets/phone-list.imageset/phone-list.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Linphone/Assets.xcassets/phone-transfer.imageset/Contents.json b/Linphone/Assets.xcassets/phone-transfer.imageset/Contents.json new file mode 100644 index 000000000..702f535c8 --- /dev/null +++ b/Linphone/Assets.xcassets/phone-transfer.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "phone-transfer.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/phone-transfer.imageset/phone-transfer.svg b/Linphone/Assets.xcassets/phone-transfer.imageset/phone-transfer.svg new file mode 100644 index 000000000..c63342fd6 --- /dev/null +++ b/Linphone/Assets.xcassets/phone-transfer.imageset/phone-transfer.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 736500641..a07c6b926 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -105,10 +105,6 @@ final class CoreContext: ObservableObject { self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true - let videoActivationPolicy = self.mCore.videoActivationPolicy! - videoActivationPolicy.automaticallyAccept = true - self.mCore.videoActivationPolicy! = videoActivationPolicy - try? self.mCore.start() // Create a Core listener to listen for the callback we need diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 467b2feee..8dc0dee51 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -107,6 +107,9 @@ }, "+" : { + }, + "|" : { + }, "0" : { @@ -268,6 +271,9 @@ }, "Deny all" : { + }, + "Dialer" : { + }, "Display Name" : { @@ -415,9 +421,6 @@ }, "Outgoing Call" : { - }, - "Participants" : { - }, "password" : { "extractionState" : "manual", @@ -438,6 +441,12 @@ }, "Pause" : { + }, + "Paused" : { + + }, + "Paused by remote" : { + }, "Personnalize your profil mode" : { @@ -474,9 +483,6 @@ }, "Scan QR code" : { - }, - "Screen share" : { - }, "Search contact or history call" : { @@ -540,6 +546,9 @@ }, "to Linphone" : { + }, + "Transfer" : { + }, "Transport" : { diff --git a/Linphone/Ressources/linphonerc-factory b/Linphone/Ressources/linphonerc-factory index 4074322fe..0abf8269d 100644 --- a/Linphone/Ressources/linphonerc-factory +++ b/Linphone/Ressources/linphonerc-factory @@ -31,6 +31,8 @@ ec_calibrator_cool_tones=1 [video] auto_resize_preview_to_keep_ratio=1 max_conference_size=vga +automatically_accept=1 +automatically_initiate=0 [misc] enable_basic_to_client_group_chat_room_migration=0 diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift index 55ff1cf40..44ef6095e 100644 --- a/Linphone/TelecomManager/ProviderDelegate.swift +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -1,21 +1,21 @@ /* -* Copyright (c) 2010-2020 Belledonne Communications SARL. -* -* This file is part of linphone-iphone -* -* This program is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program. If not, see . -*/ + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ // swiftlint:disable line_length import Foundation @@ -56,19 +56,19 @@ class CallInfo { } /* -* A delegate to support callkit. -*/ + * A delegate to support callkit. + */ class ProviderDelegate: NSObject { let provider: CXProvider var uuids: [String: UUID] = [:] var callInfos: [UUID: CallInfo] = [:] - + override init() { provider = CXProvider(configuration: ProviderDelegate.providerConfiguration) super.init() provider.setDelegate(self, queue: nil) } - + static var providerConfiguration: CXProviderConfiguration { get { let providerConfiguration = CXProviderConfiguration() @@ -97,18 +97,18 @@ class ProviderDelegate: NSObject { let callId = callInfo?.callId ?? "" /* - if (ConfigManager.instance().config?.hasEntry(section: "app", key: "max_calls") == 1) { // moved from misc to app section intentionally upon app start or remote configuration - if let maxCalls = ConfigManager.instance().config?.getInt(section: "app",key: "max_calls",defaultValue: 10), Core.get().callsNb > maxCalls { - Log.directLog(BCTBX_LOG_MESSAGE, text: "CallKit: declining call, as max calls (\(maxCalls)) reached call-id: [\(String(describing: callId))] and UUID: [\(uuid.description)]") - decline(uuid: uuid) - - CoreContext.shared.doOnCoreQueue(synchronous: true) { core in - try? call?.decline(reason: .Busy) - } - return - } - } - */ + if (ConfigManager.instance().config?.hasEntry(section: "app", key: "max_calls") == 1) { // moved from misc to app section intentionally upon app start or remote configuration + if let maxCalls = ConfigManager.instance().config?.getInt(section: "app",key: "max_calls",defaultValue: 10), Core.get().callsNb > maxCalls { + Log.directLog(BCTBX_LOG_MESSAGE, text: "CallKit: declining call, as max calls (\(maxCalls)) reached call-id: [\(String(describing: callId))] and UUID: [\(uuid.description)]") + decline(uuid: uuid) + + CoreContext.shared.doOnCoreQueue(synchronous: true) { core in + try? call?.decline(reason: .Busy) + } + return + } + } + */ Log.info("CallKit: report new incoming call with call-id: [\(callId)] and UUID: [\(uuid.description)]") // TelecomManager.instance().setHeldOtherCalls(exceptCallid: callId ?? "") // ALREADY COMMENTED ON LINPHONE-IPHONE 5.2 @@ -140,7 +140,7 @@ class ProviderDelegate: NSObject { } } } - + func updateCall(uuid: UUID, handle: String, hasVideo: Bool = false, displayName: String) { let update = CXCallUpdate() update.remoteHandle = CXHandle(type: .generic, value: handle) @@ -148,11 +148,11 @@ class ProviderDelegate: NSObject { update.hasVideo = hasVideo provider.reportCall(with: uuid, updated: update) } - + func reportOutgoingCallStartedConnecting(uuid: UUID) { provider.reportOutgoingCall(with: uuid, startedConnectingAt: nil) } - + func reportOutgoingCallConnected(uuid: UUID) { provider.reportOutgoingCall(with: uuid, connectedAt: nil) } @@ -164,7 +164,7 @@ class ProviderDelegate: NSObject { func decline(uuid: UUID) { provider.reportCall(with: uuid, endedAt: .init(), reason: .unanswered) } - + func endCallNotExist(uuid: UUID, timeout: DispatchTime) { DispatchQueue.main.asyncAfter(deadline: timeout) { CoreContext.shared.doOnCoreQueue(synchronous: true) { core in @@ -188,7 +188,7 @@ extension ProviderDelegate: CXProviderDelegate { let uuid = action.callUUID let callId = callInfos[uuid]?.callId - + // remove call infos first, otherwise CXEndCallAction will be called more than onece if callId != nil { uuids.removeValue(forKey: callId!) @@ -203,7 +203,7 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } } - + func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { let uuid = action.callUUID let callInfo = callInfos[uuid] @@ -221,15 +221,17 @@ extension ProviderDelegate: CXProviderDelegate { let call = core.getCallByCallid(callId: callId) - if UIApplication.shared.applicationState != .active { - TelecomManager.shared.backgroundContextCall = call - TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true || call?.callLog?.wasConference() == true - if #available(iOS 16.0, *) { - if call?.cameraEnabled == true { - call?.cameraEnabled = AVCaptureSession().isMultitaskingCameraAccessSupported + DispatchQueue.main.async() { + if UIApplication.shared.applicationState != .active { + TelecomManager.shared.backgroundContextCall = call + TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true || call?.callLog?.wasConference() == true + if #available(iOS 16.0, *) { + if call?.cameraEnabled == true { + call?.cameraEnabled = AVCaptureSession().isMultitaskingCameraAccessSupported + } + } else { + call?.cameraEnabled = false // Disable camera while app is not on foreground } - } else { - call?.cameraEnabled = false // Disable camera while app is not on foreground } } TelecomManager.shared.callkitAudioSessionActivated = false @@ -242,7 +244,7 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } } - + func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { let uuid = action.callUUID let callId = callInfos[uuid]?.callId ?? "" @@ -275,29 +277,29 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } else { if call?.conference != nil && core.callsNb > 1 {/* - try TelecomManager.shared.lc?.enterConference() - action.fulfill() - NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self) - */} else { - try call!.resume() - // We'll notify callkit that the action is fulfilled when receiving the 200Ok, which is the point - // where we actually start the media streams. - TelecomManager.shared.actionToFulFill = action - // HORRIBLE HACK HERE - PLEASE APPLE FIX THIS !! - // When resuming a SIP call after a native call has ended remotely, didActivate: audioSession - // is never called. - // It looks like in this case, it is implicit. - // As a result we have to notify the Core that the AudioSession is active. - // The SpeakerBox demo application written by Apple exhibits this behavior. - // https://developer.apple.com/documentation/callkit/making_and_receiving_voip_calls_with_callkit - // We can clearly see there that startAudio() is called immediately in the CXSetHeldCallAction - // handler, while it is called from didActivate: audioSession otherwise. - // Callkit's design is not consistent, or its documentation imcomplete, wich is somewhat disapointing. - // - Log.info("Assuming AudioSession is active when executing a CXSetHeldCallAction with isOnHold=false.") - core.activateAudioSession(actived: true) - TelecomManager.shared.callkitAudioSessionActivated = true - } + try TelecomManager.shared.lc?.enterConference() + action.fulfill() + NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self) + */} else { + try call!.resume() + // We'll notify callkit that the action is fulfilled when receiving the 200Ok, which is the point + // where we actually start the media streams. + TelecomManager.shared.actionToFulFill = action + // HORRIBLE HACK HERE - PLEASE APPLE FIX THIS !! + // When resuming a SIP call after a native call has ended remotely, didActivate: audioSession + // is never called. + // It looks like in this case, it is implicit. + // As a result we have to notify the Core that the AudioSession is active. + // The SpeakerBox demo application written by Apple exhibits this behavior. + // https://developer.apple.com/documentation/callkit/making_and_receiving_voip_calls_with_callkit + // We can clearly see there that startAudio() is called immediately in the CXSetHeldCallAction + // handler, while it is called from didActivate: audioSession otherwise. + // Callkit's design is not consistent, or its documentation imcomplete, wich is somewhat disapointing. + // + Log.info("Assuming AudioSession is active when executing a CXSetHeldCallAction with isOnHold=false.") + core.activateAudioSession(actived: true) + TelecomManager.shared.callkitAudioSessionActivated = true + } } } } catch { @@ -332,7 +334,7 @@ extension ProviderDelegate: CXProviderDelegate { } } } - + func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) { CoreContext.shared.doOnCoreQueue { core in Log.info("CallKit: Call grouped callUUid : \(action.callUUID) with callUUID: \(String(describing: action.callUUIDToGroupWith)).") @@ -340,7 +342,7 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } } - + func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { let uuid = action.callUUID let callId = callInfos[uuid]?.callId @@ -350,7 +352,7 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } } - + func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) { let uuid = action.callUUID let callId = callInfos[uuid]?.callId ?? "" @@ -368,18 +370,18 @@ extension ProviderDelegate: CXProviderDelegate { action.fulfill() } } - + func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) { let uuid = action.uuid let callId = callInfos[uuid]?.callId Log.error("CallKit: Call time out with call-id: \(String(describing: callId)) an UUID: \(uuid.description).") action.fulfill() } - + func providerDidReset(_ provider: CXProvider) { Log.info("CallKit: did reset.") } - + func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { CoreContext.shared.doOnCoreQueue { core in Log.info("CallKit: audio session activated.") @@ -387,7 +389,7 @@ extension ProviderDelegate: CXProviderDelegate { TelecomManager.shared.callkitAudioSessionActivated = true } } - + func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { CoreContext.shared.doOnCoreQueue { core in Log.info("CallKit: audio session deactivated.") diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 22372bc1a..506ba21e4 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -41,9 +41,11 @@ class TelecomManager: ObservableObject { let callController: CXCallController // to support callkit @Published var callInProgress: Bool = false - @Published var callStarted: Bool = false + @Published var callStarted: Bool = false + @Published var outgoingCallStarted: Bool = false @Published var remoteVideo: Bool = false - @Published var isRemoteRecording: Bool = false + @Published var isRecordingByRemote: Bool = false + @Published var isPausedByRemote: Bool = false var actionToFulFill: CXCallAction? var callkitAudioSessionActivated: Bool? @@ -118,15 +120,15 @@ class TelecomManager: ObservableObject { } } - func doCallWithCore(addr: Address) { - CoreContext.shared.doOnCoreQueue { core in + func doCallWithCore(addr: Address) { + CoreContext.shared.doOnCoreQueue { core in do { try self.startCallCallKit(core: core, addr: addr, isSas: false, isVideo: false, isConference: false) } catch { Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr) \(error) ") } - } - } + } + } private func makeRecordFilePath() -> String{ var filePath = "recording_" @@ -140,25 +142,25 @@ class TelecomManager: ObservableObject { let writablePath = paths[0] return writablePath.appending("/\(filePath)") } - + func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws { // let displayName = FastAddressBook.displayName(for: addr.getCobject) let lcallParams = try core.createCallParams(call: nil) /* - if ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference") && AppManager.network() == .network_2g { - Log.directLog(BCTBX_LOG_MESSAGE, text: "Enabling low bandwidth mode") - lcallParams.lowBandwidthEnabled = true - } - - if (displayName != nil) { - try addr.setDisplayname(newValue: displayName!) - } - - if(ConfigManager.instance().lpConfigBoolForKey(key: "override_domain_with_default_one")) { - try addr.setDomain(newValue: ConfigManager.instance().lpConfigStringForKey(key: "domain", section: "assistant")) - } - */ + if ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference") && AppManager.network() == .network_2g { + Log.directLog(BCTBX_LOG_MESSAGE, text: "Enabling low bandwidth mode") + lcallParams.lowBandwidthEnabled = true + } + + if (displayName != nil) { + try addr.setDisplayname(newValue: displayName!) + } + + if(ConfigManager.instance().lpConfigBoolForKey(key: "override_domain_with_default_one")) { + try addr.setDomain(newValue: ConfigManager.instance().lpConfigStringForKey(key: "domain", section: "assistant")) + } + */ if nextCallIsTransfer { let call = core.currentCall @@ -176,13 +178,13 @@ class TelecomManager: ObservableObject { lcallParams.mediaEncryption = .ZRTP } if isConference { - /* if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) { - lcallParams.videoEnabled = true - lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly - lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! == .Grid ? .Grid : .ActiveSpeaker - } else { - lcallParams.videoEnabled = false - }*/ + /* if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) { + lcallParams.videoEnabled = true + lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly + lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! == .Grid ? .Grid : .ActiveSpeaker + } else { + lcallParams.videoEnabled = false + }*/ } else { lcallParams.videoEnabled = isVideo } @@ -202,7 +204,7 @@ class TelecomManager: ObservableObject { } DispatchQueue.main.async { - + self.outgoingCallStarted = true self.callStarted = true if self.callInProgress == false { withAnimation { @@ -220,12 +222,12 @@ class TelecomManager: ObservableObject { callParams.recordFile = makeRecordFilePath() callParams.videoEnabled = hasVideo /*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) { - let low_bandwidth = (AppManager.network() == .network_2g) - if (low_bandwidth) { - Log.directLog(BCTBX_LOG_MESSAGE, text: "Low bandwidth mode") - } - callParams.lowBandwidthEnabled = low_bandwidth - }*/ + let low_bandwidth = (AppManager.network() == .network_2g) + if (low_bandwidth) { + Log.directLog(BCTBX_LOG_MESSAGE, text: "Low bandwidth mode") + } + callParams.lowBandwidthEnabled = low_bandwidth + }*/ // We set the record file name here because we can't do it after the call is started. // let address = call.callLog?.fromAddress @@ -234,10 +236,10 @@ class TelecomManager: ObservableObject { // callParams.recordFile = writablePath /* - if let chatView : ChatConversationView = PhoneMainView.instance().VIEW(ChatConversationView.compositeViewDescription()), chatView.isVoiceRecording { - Log.directLog(BCTBX_LOG_MESSAGE, text: "Voice recording in progress, stopping it befoce accepting the call.") - chatView.stopVoiceRecording() - }*/ + if let chatView : ChatConversationView = PhoneMainView.instance().VIEW(ChatConversationView.compositeViewDescription()), chatView.isVoiceRecording { + Log.directLog(BCTBX_LOG_MESSAGE, text: "Voice recording in progress, stopping it befoce accepting the call.") + chatView.stopVoiceRecording() + }*/ if call.callLog?.wasConference() == true { // Prevent incoming group call to start in audio only layout @@ -249,9 +251,9 @@ class TelecomManager: ObservableObject { try call.acceptWithParams(params: callParams) - DispatchQueue.main.async { - self.callStarted = true - } + DispatchQueue.main.async { + self.callStarted = true + } } catch { Log.error("accept call failed \(error)") } @@ -334,17 +336,18 @@ class TelecomManager: ObservableObject { if cstate == .PushIncomingReceived { displayIncomingCall(call: call, handle: "Calling", hasVideo: false, callId: callId, displayName: "Calling") } else { - remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) - if remoteVideo { - Log.info("[Call] Remote video is activated") - } - - isRemoteRecording = call.remoteParams?.isRecording ?? false - - if isRemoteRecording && ToastViewModel.shared.toastMessage.isEmpty { + DispatchQueue.main.async { + self.remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) - DispatchQueue.main.async { + if self.remoteVideo { + Log.info("[Call] Remote video is activated") + } + + self.isRecordingByRemote = call.remoteParams?.isRecording ?? false + + if self.isRecordingByRemote && ToastViewModel.shared.toastMessage.isEmpty { + var displayName = "" let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!) if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { @@ -358,22 +361,27 @@ class TelecomManager: ObservableObject { } ToastViewModel.shared.toastMessage = "\(displayName) is recording" - ToastViewModel.shared.displayToast = true + ToastViewModel.shared.displayToast = true + + Log.info("[Call] Call is recording by \(call.remoteAddress!.asStringUriOnly())") } - Log.info("[Call] Call is recording by \(call.remoteAddress!.asStringUriOnly())") - } - - if !isRemoteRecording && ToastViewModel.shared.toastMessage.contains("is recording") { - - DispatchQueue.main.async { + if !self.isRecordingByRemote && ToastViewModel.shared.toastMessage.contains("is recording") { + withAnimation { ToastViewModel.shared.toastMessage = "" ToastViewModel.shared.displayToast = false } + + Log.info("[Call] Recording is stopped by \(call.remoteAddress!.asStringUriOnly())") } - Log.info("[Call] Call is recording Stop recording by \(call.remoteAddress!.asStringUriOnly())") + switch call.state { + case Call.State.PausedByRemote: + self.isPausedByRemote = true + default: + self.isPausedByRemote = false + } } if call.userData == nil { @@ -381,24 +389,28 @@ class TelecomManager: ObservableObject { TelecomManager.setAppData(sCall: call, appData: appData) } /* - if let conference = call.conference, ConferenceViewModel.shared.conference.value == nil { - Log.info("[Call] Found conference attached to call and no conference in dedicated view model, init & configure it") - ConferenceViewModel.shared.initConference(conference) - ConferenceViewModel.shared.configureConference(conference) - } - */ + if let conference = call.conference, ConferenceViewModel.shared.conference.value == nil { + Log.info("[Call] Found conference attached to call and no conference in dedicated view model, init & configure it") + ConferenceViewModel.shared.initConference(conference) + ConferenceViewModel.shared.configureConference(conference) + } + */ switch cstate { case .IncomingReceived: let addr = call.remoteAddress let displayName = incomingDisplayName(call: call) - #if targetEnvironment(simulator) +#if targetEnvironment(simulator) DispatchQueue.main.async { - withAnimation { - TelecomManager.shared.callInProgress = true + self.outgoingCallStarted = false + self.callStarted = true + if self.callInProgress == false { + withAnimation { + self.callInProgress = true + } } } - #endif +#endif if call.replacedCall != nil { endCallKitReplacedCall = false @@ -415,17 +427,17 @@ class TelecomManager: ObservableObject { } else if TelecomManager.callKitEnabled(core: core) { /* let isConference = isConferenceCall(call: call) - let isEarlyConference = isConference && CallsViewModel.shared.currentCallData.value??.isConferenceCall.value != true // Conference info not be received yet. - if (isEarlyConference) { - CallsViewModel.shared.currentCallData.readCurrentAndObserve { _ in - let uuid = providerDelegate.uuids["\(callId)"] - if (uuid != nil) { - displayName = "\(VoipTexts.conference_incoming_title): \(CallsViewModel.shared.currentCallData.value??.remoteConferenceSubject.value ?? "") (\(CallsViewModel.shared.currentCallData.value??.conferenceParticipantsCountLabel.value ?? ""))" - providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName) - } - } - } - */ + let isEarlyConference = isConference && CallsViewModel.shared.currentCallData.value??.isConferenceCall.value != true // Conference info not be received yet. + if (isEarlyConference) { + CallsViewModel.shared.currentCallData.readCurrentAndObserve { _ in + let uuid = providerDelegate.uuids["\(callId)"] + if (uuid != nil) { + displayName = "\(VoipTexts.conference_incoming_title): \(CallsViewModel.shared.currentCallData.value??.remoteConferenceSubject.value ?? "") (\(CallsViewModel.shared.currentCallData.value??.conferenceParticipantsCountLabel.value ?? ""))" + providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName) + } + } + } + */ let uuid = providerDelegate.uuids["\(callId)"] if call.replacedCall == nil { TelecomManager.uuidReplacedCall = callId @@ -438,18 +450,23 @@ class TelecomManager: ObservableObject { displayIncomingCall(call: call, handle: addr!.asStringUriOnly(), hasVideo: remoteVideo, callId: callId, displayName: displayName) } } /* else if UIApplication.shared.applicationState != .active { - // not support callkit , use notif - let content = UNMutableNotificationContent() - content.title = NSLocalizedString("Incoming call", comment: "") - content.body = displayName - content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init("notes_of_the_optimistic.caf")) - content.categoryIdentifier = "call_cat" - content.userInfo = ["CallId": callId] - let req = UNNotificationRequest.init(identifier: "call_request", content: content, trigger: nil) - UNUserNotificationCenter.current().add(req, withCompletionHandler: nil) - } */ + // not support callkit , use notif + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Incoming call", comment: "") + content.body = displayName + content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init("notes_of_the_optimistic.caf")) + content.categoryIdentifier = "call_cat" + content.userInfo = ["CallId": callId] + let req = UNNotificationRequest.init(identifier: "call_request", content: content, trigger: nil) + UNUserNotificationCenter.current().add(req, withCompletionHandler: nil) + } */ case .StreamsRunning: if TelecomManager.callKitEnabled(core: core) { + + DispatchQueue.main.async { + self.outgoingCallStarted = false + } + let uuid = providerDelegate.uuids["\(callId)"] if uuid != nil { let callInfo = providerDelegate.callInfos[uuid!] @@ -463,10 +480,10 @@ class TelecomManager: ObservableObject { } /* - if speakerBeforePause { - speakerBeforePause = false - AudioRouteUtils.routeAudioToSpeaker(core: core) - } + if speakerBeforePause { + speakerBeforePause = false + AudioRouteUtils.routeAudioToSpeaker(core: core) + } */ actionToFulFill?.fulfill() @@ -491,12 +508,12 @@ class TelecomManager: ObservableObject { providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid!) } else { if false { /* isConferenceCall(call: call) { - let uuid = UUID() - let callInfo = CallInfo.newOutgoingCallInfo(addr: call.remoteAddress!, isSas: call.params?.mediaEncryption == .ZRTP, displayName: VoipTexts.conference_default_title, isVideo: call.params?.videoEnabled == true, isConference:true) - providerDelegate.callInfos.updateValue(callInfo, forKey: uuid) - providerDelegate.uuids.updateValue(uuid, forKey: "") - providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid) - Core.get().activateAudioSession(actived: true) */ + let uuid = UUID() + let callInfo = CallInfo.newOutgoingCallInfo(addr: call.remoteAddress!, isSas: call.params?.mediaEncryption == .ZRTP, displayName: VoipTexts.conference_default_title, isVideo: call.params?.videoEnabled == true, isConference:true) + providerDelegate.callInfos.updateValue(callInfo, forKey: uuid) + providerDelegate.uuids.updateValue(uuid, forKey: "") + providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid) + Core.get().activateAudioSession(actived: true) */ } else { referedToCall = callId } @@ -505,19 +522,6 @@ class TelecomManager: ObservableObject { case .End, .Error: - DispatchQueue.main.async { - withAnimation { - self.callInProgress = false - self.callStarted = false - } - } - var displayName = "Unknown" - if call.dir == .Incoming { - displayName = incomingDisplayName(call: call) - } else { // if let addr = call.remoteAddress, let contactName = FastAddressBook.displayName(for: addr.getCobject) { - displayName = "TODOContactName" - } - UIDevice.current.isProximityMonitoringEnabled = false if core.callsNb == 0 { core.outputAudioDevice = core.defaultOutputAudioDevice @@ -527,18 +531,34 @@ class TelecomManager: ObservableObject { // bluetoothEnabled = false } - if UIApplication.shared.applicationState != .active && (callLog == nil || callLog?.status == .Missed || callLog?.status == .Aborted || callLog?.status == .EarlyAborted) { - // Configure the notification's payload. - let content = UNMutableNotificationContent() - content.title = NSString.localizedUserNotificationString(forKey: NSLocalizedString("Missed call", comment: ""), arguments: nil) - content.body = NSString.localizedUserNotificationString(forKey: displayName, arguments: nil) + DispatchQueue.main.async { + withAnimation { + self.outgoingCallStarted = false + self.callInProgress = false + self.callStarted = false + } - // Deliver the notification. - let request = UNNotificationRequest(identifier: "call_request", content: content, trigger: nil) // Schedule the notification. - let center = UNUserNotificationCenter.current() - center.add(request) { (error: Error?) in - if error != nil { - Log.info("Error while adding notification request : \(error!.localizedDescription)") + var displayName = "Unknown" + if call.dir == .Incoming { + displayName = self.incomingDisplayName(call: call) + } else { // if let addr = call.remoteAddress, let contactName = FastAddressBook.displayName(for: addr.getCobject) { + displayName = "TODOContactName" + } + + + if UIApplication.shared.applicationState != .active && (callLog == nil || callLog?.status == .Missed || callLog?.status == .Aborted || callLog?.status == .EarlyAborted) { + // Configure the notification's payload. + let content = UNMutableNotificationContent() + content.title = NSString.localizedUserNotificationString(forKey: NSLocalizedString("Missed call", comment: ""), arguments: nil) + content.body = NSString.localizedUserNotificationString(forKey: displayName, arguments: nil) + + // Deliver the notification. + let request = UNNotificationRequest(identifier: "call_request", content: content, trigger: nil) // Schedule the notification. + let center = UNUserNotificationCenter.current() + center.add(request) { (error: Error?) in + if error != nil { + Log.info("Error while adding notification request : \(error!.localizedDescription)") + } } } } @@ -583,22 +603,6 @@ class TelecomManager: ObservableObject { default: break } - - // AudioRouteUtils.isBluetoothAvailable(core: core) - // AudioRouteUtils.isHeadsetAudioRouteAvailable(core: core) - // AudioRouteUtils.isBluetoothAudioRouteAvailable(core: core) - - /* - let readyForRoutechange = callkitAudioSessionActivated == nil || (callkitAudioSessionActivated == true) - if readyForRoutechange && (cstate == .IncomingReceived || cstate == .OutgoingInit || cstate == .Connected || cstate == .StreamsRunning) { - if (call.currentParams?.videoEnabled ?? false) && AudioRouteUtils.isReceiverEnabled(core: core) && call.conference == nil { - AudioRouteUtils.routeAudioToSpeaker(core: core, call: call) - } else if AudioRouteUtils.isBluetoothAvailable(core: core) { - // Use bluetooth device by default if one is available - AudioRouteUtils.routeAudioToBluetooth(core: core, call: call) - } - } - */ } // post Notification kLinphoneCallUpdate NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self, userInfo: [ diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index dc681036d..9f1d99f5a 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -24,10 +24,10 @@ import AVFAudio import linphonesw struct CallView: View { - - @ObservedObject private var coreContext = CoreContext.shared - @ObservedObject private var telecomManager = TelecomManager.shared - @ObservedObject private var contactsManager = ContactsManager.shared + + @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var telecomManager = TelecomManager.shared + @ObservedObject private var contactsManager = ContactsManager.shared @ObservedObject var callViewModel: CallViewModel @@ -35,8 +35,8 @@ struct CallView: View { @State private var orientation = UIDevice.current.orientation let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification) - - @State var startDate = Date.now + + @State var startDate = Date.now @State var audioRouteSheet: Bool = false @State var hideButtonsSheet: Bool = false @State var options: Int = 1 @@ -45,10 +45,10 @@ struct CallView: View { @State var angleDegree = 0.0 @State var fullscreenVideo = false - - var body: some View { - GeometryReader { geo in - if #available(iOS 16.4, *) { + + var body: some View { + GeometryReader { geo in + if #available(iOS 16.4, *) { innerView(geometry: geo) .sheet(isPresented: .constant( @@ -60,54 +60,55 @@ struct CallView: View { ) ) { GeometryReader { _ in - VStack(spacing: 0) { - HStack(spacing: 12) { - Button { + VStack(spacing: 0) { + HStack(spacing: 12) { + Button { callViewModel.terminateCall() - } label: { - Image("phone-disconnect") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - - } - .frame(width: 90, height: 60) - .background(Color.redDanger500) - .cornerRadius(40) - - Spacer() - - Button { + } label: { + Image("phone-disconnect") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.redDanger500) + .cornerRadius(40) + + Spacer() + + Button { callViewModel.toggleVideo() - } label: { + } label: { Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Button { + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + + Button { callViewModel.toggleMuteMicrophone() - } label: { + } label: { Image(callViewModel.micMutted ? "microphone-slash" : "microphone") - .renderingMode(.template) - .resizable() + .renderingMode(.template) + .resizable() .foregroundStyle(callViewModel.micMutted ? .black : .white) - .frame(width: 32, height: 32) - - } - .frame(width: 60, height: 60) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) .background(callViewModel.micMutted ? .white : Color.gray500) - .cornerRadius(40) - - Button { - if AVAudioSession.sharedInstance().availableInputs != nil + .cornerRadius(40) + + Button { + if AVAudioSession.sharedInstance().availableInputs != nil && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { hideButtonsSheet = true @@ -123,200 +124,206 @@ struct CallView: View { } } - } label: { + } label: { Image(imageAudioRoute) - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) .onAppear(perform: getAudioRouteImage) .onReceive(pub) { (output) in self.getAudioRouteImage() } - - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - } - .frame(height: geo.size.height * 0.15) - .padding(.horizontal, 20) + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + } + .frame(height: geo.size.height * 0.15) + .padding(.horizontal, 20) .padding(.top, -6) - - HStack(spacing: 0) { - VStack { - Button { - } label: { - Image("screencast") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Screen share") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("users") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Participants") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("chat-teardrop-text") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Messages") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("notebook") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Disposition") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - } - .frame(height: geo.size.height * 0.15) - - HStack(spacing: 0) { - VStack { - Button { - } label: { - Image("phone-call") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Call list") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { + + HStack(spacing: 0) { + VStack { + Button { + } label: { + Image("phone-transfer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Transfer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("phone-plus") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("New call") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("phone-list") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Call list") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("dialer") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Dialer") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + } + .frame(height: geo.size.height * 0.15) + + HStack(spacing: 0) { + VStack { + Button { + } label: { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + //.foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .foregroundStyle(Color.gray500) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + //.background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) + .background(Color.gray600) + .cornerRadius(40) + //.disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + .disabled(true) + + Text("Messages") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { callViewModel.togglePause() - } label: { - Image("pause") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Pause") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { + } label: { + Image(callViewModel.isPaused ? "play" : "pause") + .renderingMode(.template) + .resizable() + .foregroundStyle(telecomManager.isPausedByRemote ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(telecomManager.isPausedByRemote ? Color.gray600 : (callViewModel.isPaused ? Color.greenSuccess500 : Color.gray500)) + .cornerRadius(40) + .disabled(telecomManager.isPausedByRemote) + + Text("Pause") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { callViewModel.toggleRecording() - } label: { - Image("record-fill") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(callViewModel.isRecording ? Color.redDanger500 : Color.gray500) - .cornerRadius(40) - - Text("Record") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - - VStack { - Button { - } label: { - Image("video-camera") - .renderingMode(.template) - .resizable() - .foregroundStyle(.white) - .frame(width: 32, height: 32) - } - .frame(width: 60, height: 60) - .background(Color.gray500) - .cornerRadius(40) - - Text("Disposition") - .foregroundStyle(.white) - .default_text_style(styleSize: 15) - } - .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) - .hidden() - } - .frame(height: geo.size.height * 0.15) - - Spacer() - } - .frame(maxHeight: .infinity, alignment: .top) + } label: { + Image("record-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : (callViewModel.isRecording ? Color.redDanger500 : Color.gray500)) + .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) + + Text("Record") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + + VStack { + Button { + } label: { + Image("video-camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Text("Disposition") + .foregroundStyle(.white) + .default_text_style(styleSize: 15) + } + .frame(width: geo.size.width * 0.25, height: geo.size.width * 0.25) + .hidden() + } + .frame(height: geo.size.height * 0.15) + + Spacer() + } + .frame(maxHeight: .infinity, alignment: .top) .presentationBackground(.black) - .presentationDetents([.fraction(0.1), .medium]) - .interactiveDismissDisabled() - .presentationBackgroundInteraction(.enabled) - } - } - .sheet(isPresented: $audioRouteSheet, onDismiss: { + .presentationDetents([.fraction(0.1), .fraction(0.45)]) + .interactiveDismissDisabled() + .presentationBackgroundInteraction(.enabled) + } + } + .sheet(isPresented: $audioRouteSheet, onDismiss: { audioRouteSheet = false hideButtonsSheet = false - }) { + }) { VStack(spacing: 0) { Button(action: { options = 1 @@ -346,9 +353,9 @@ struct CallView: View { Image(!callViewModel.isHeadPhoneAvailable() ? "ear" : "headset") .renderingMode(.template) - .resizable() + .resizable() .foregroundStyle(.white) - .frame(width: 25, height: 25, alignment: .leading) + .frame(width: 25, height: 25, alignment: .leading) } }) .frame(maxHeight: .infinity) @@ -416,16 +423,16 @@ struct CallView: View { } .padding(.horizontal, 20) .presentationBackground(Color.gray600) - .presentationDetents([.fraction(0.3)]) + .presentationDetents([.fraction(0.3)]) .frame(maxHeight: .infinity) } - } - } - } - - @ViewBuilder + } + } + } + + @ViewBuilder func innerView(geometry: GeometryProxy) -> some View { - VStack { + VStack { if !fullscreenVideo { Rectangle() .foregroundColor(Color.orangeMain500) @@ -451,6 +458,35 @@ struct CallView: View { .foregroundStyle(.white) } + if !telecomManager.outgoingCallStarted && telecomManager.callInProgress { + Text("|") + .foregroundStyle(.white) + + ZStack { + Text(callViewModel.timeElapsed.convertDurationToString()) + .onAppear { + callViewModel.timeElapsed = 0 + startDate = Date.now + } + .onReceive(callViewModel.timer) { firedDate in + callViewModel.timeElapsed = Int(firedDate.timeIntervalSince(startDate)) + + } + .foregroundStyle(.white) + .if(callViewModel.isPaused || telecomManager.isPausedByRemote) { view in + view.hidden() + } + + if callViewModel.isPaused { + Text("Paused") + .foregroundStyle(.white) + } else if telecomManager.isPausedByRemote { + Text("Paused by remote") + .foregroundStyle(.white) + } + } + } + Spacer() if callViewModel.cameraDisplayed { @@ -469,65 +505,65 @@ struct CallView: View { .frame(height: 40) .zIndex(1) } - - ZStack { - VStack { - Spacer() - - if callViewModel.remoteAddress != nil { - let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!) - - let contactAvatarModel = addressFriend != nil - ? ContactsManager.shared.avatarListModel.first(where: { - ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) - && $0.friend!.name == addressFriend!.name - && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() - }) - : ContactAvatarModel(friend: nil, withPresence: false) - - if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - if contactAvatarModel != nil { - Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true) - } - } else { - if callViewModel.remoteAddress!.displayName != nil { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.remoteAddress!.displayName!, - lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1 - ? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 100, height: 100) - .clipShape(Circle()) - - } else { - Image(uiImage: contactsManager.textToImage( - firstName: callViewModel.remoteAddress!.username ?? "Username Error", - lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1 - ? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1] - : "")) - .resizable() - .frame(width: 100, height: 100) - .clipShape(Circle()) - } - - } - } else { - Image("profil-picture-default") - .resizable() - .frame(width: 100, height: 100) - .clipShape(Circle()) - } - - Text(callViewModel.displayName) - .padding(.top) - .foregroundStyle(.white) - - Text(callViewModel.remoteAddressString) - .foregroundStyle(.white) - - Spacer() - } + + ZStack { + VStack { + Spacer() + + if callViewModel.remoteAddress != nil { + let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!) + + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, withPresence: false) + + if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { + if contactAvatarModel != nil { + Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true) + } + } else { + if callViewModel.remoteAddress!.displayName != nil { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.remoteAddress!.displayName!, + lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1 + ? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + } else { + Image(uiImage: contactsManager.textToImage( + firstName: callViewModel.remoteAddress!.username ?? "Username Error", + lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1 + ? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + } + + } + } else { + Image("profil-picture-default") + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + } + + Text(callViewModel.displayName) + .padding(.top) + .foregroundStyle(.white) + + Text(callViewModel.remoteAddressString) + .foregroundStyle(.white) + + Spacer() + } LinphoneVideoViewHolder { view in coreContext.doOnCoreQueue { core in @@ -535,7 +571,7 @@ struct CallView: View { } } .frame( - width: + width: angleDegree == 0 ? 120 * ((geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom) / 160) : 120 * ((geometry.size.width + geometry.safeAreaInsets.leading + geometry.safeAreaInsets.trailing) / 120), @@ -577,8 +613,8 @@ struct CallView: View { VStack { Image("record-fill") .renderingMode(.template) - .resizable() - .foregroundStyle(Color.redDanger500) + .resizable() + .foregroundStyle(Color.redDanger500) .frame(width: 32, height: 32) .padding(10) .if(fullscreenVideo) { view in @@ -594,31 +630,39 @@ struct CallView: View { ) } - if !telecomManager.callStarted && !fullscreenVideo { - VStack { - ActivityIndicator() - .frame(width: 20, height: 20) - .padding(.top, 100) - + if telecomManager.outgoingCallStarted { + VStack { + ActivityIndicator() + .frame(width: 20, height: 20) + .padding(.top, 100) + Text(callViewModel.counterToMinutes()) + .onAppear { + callViewModel.timeElapsed = 0 + startDate = Date.now + } .onReceive(callViewModel.timer) { firedDate in callViewModel.timeElapsed = Int(firedDate.timeIntervalSince(startDate)) - - } - .padding(.top) - .foregroundStyle(.white) - - Spacer() - } - .background(.clear) - } - } + + } + .padding(.top) + .foregroundStyle(.white) + + Spacer() + } + .frame( + maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8, + maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140 + ) + .background(.clear) + } + } .frame( maxWidth: fullscreenVideo ? geometry.size.width : geometry.size.width - 8, maxHeight: fullscreenVideo ? geometry.size.height + geometry.safeAreaInsets.top + geometry.safeAreaInsets.bottom : geometry.size.height - 140 ) - .background(Color.gray600) - .cornerRadius(20) + .background(Color.gray600) + .cornerRadius(20) .padding(.horizontal, fullscreenVideo ? 0 : 4) .onRotate { newOrientation in orientation = newOrientation @@ -647,7 +691,7 @@ struct CallView: View { callViewModel.orientationUpdate(orientation: orientation) } - + if !fullscreenVideo { if telecomManager.callStarted { if telecomManager.callStarted && idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight @@ -684,13 +728,14 @@ struct CallView: View { Image("video-camera") .renderingMode(.template) .resizable() - .foregroundStyle(.white) + .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray600 : Color.gray500) .cornerRadius(40) + .disabled(callViewModel.isPaused || telecomManager.isPausedByRemote) Button { callViewModel.toggleMuteMicrophone() @@ -764,13 +809,13 @@ struct CallView: View { .padding(.top, 20) } } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray900) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray900) .if(fullscreenVideo) { view in view.ignoresSafeArea(.all) } - } + } func getAudioRouteImage() { imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 382e41e5e..26f9dd2ed 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -35,7 +35,8 @@ class CallViewModel: ObservableObject { @Published var cameraDisplayed: Bool = false @Published var isRecording: Bool = false @Published var isRemoteRecording: Bool = false - @State var timeElapsed: Int = 0 + @Published var isPaused: Bool = false + @Published var timeElapsed: Int = 0 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -76,6 +77,8 @@ class CallViewModel: ObservableObject { self.micMutted = self.currentCall!.microphoneMuted self.cameraDisplayed = self.currentCall!.cameraEnabled == true self.isRecording = self.currentCall!.params!.isRecording + self.isPaused = self.isCallPaused() + self.timeElapsed = 0 } } } @@ -83,8 +86,9 @@ class CallViewModel: ObservableObject { func terminateCall() { withAnimation { - telecomManager.callInProgress = false + telecomManager.outgoingCallStarted = false telecomManager.callStarted = false + telecomManager.callInProgress = false } coreContext.doOnCoreQueue { _ in @@ -98,6 +102,7 @@ class CallViewModel: ObservableObject { func acceptCall() { withAnimation { + telecomManager.outgoingCallStarted = false telecomManager.callInProgress = true telecomManager.callStarted = true } @@ -184,9 +189,11 @@ class CallViewModel: ObservableObject { if self.isCallPaused() { Log.info("[CallViewModel] Resuming call \(self.currentCall!.remoteAddress!.asStringUriOnly())") try self.currentCall!.resume() + self.isPaused = false } else { Log.info("[CallViewModel] Pausing call \(self.currentCall!.remoteAddress!.asStringUriOnly())") try self.currentCall!.pause() + self.isPaused = true } } catch _ { @@ -195,7 +202,7 @@ class CallViewModel: ObservableObject { } } - private func isCallPaused() -> Bool { + func isCallPaused() -> Bool { var result = false if self.currentCall != nil { switch self.currentCall!.state {