From 96bdf5150cc0001e0a5ac2977d9f273b7e686469 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 8 Jan 2024 16:57:26 +0100 Subject: [PATCH 01/16] Record call --- Linphone/Core/CoreContext.swift | 1 - Linphone/LinphoneApp.swift | 8 +++- .../TelecomManager/ProviderDelegate.swift | 8 ++-- Linphone/TelecomManager/TelecomManager.swift | 47 ++++++++++++++++--- Linphone/UI/Call/CallView.swift | 24 +++++++++- .../UI/Call/ViewModel/CallViewModel.swift | 13 +++-- Linphone/UI/Main/ContentView.swift | 9 +++- Linphone/UI/Main/Fragments/ToastView.swift | 7 +++ 8 files changed, 97 insertions(+), 20 deletions(-) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 98bdb7127..736500641 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -104,7 +104,6 @@ final class CoreContext: ObservableObject { self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true - self.mCore.recordAwareEnabled = true let videoActivationPolicy = self.mCore.videoActivationPolicy! videoActivationPolicy.automaticallyAccept = true diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 61e22bdf3..950f6722f 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -30,6 +30,7 @@ struct LinphoneApp: App { @State private var historyViewModel: HistoryViewModel? @State private var historyListViewModel: HistoryListViewModel? @State private var startCallViewModel: StartCallViewModel? + @State private var callViewModel: CallViewModel? var body: some Scene { WindowGroup { @@ -43,13 +44,15 @@ struct LinphoneApp: App { && editContactViewModel != nil && historyViewModel != nil && historyListViewModel != nil - && startCallViewModel != nil { + && startCallViewModel != nil + && callViewModel != nil { ContentView( contactViewModel: contactViewModel!, editContactViewModel: editContactViewModel!, historyViewModel: historyViewModel!, historyListViewModel: historyListViewModel!, - startCallViewModel: startCallViewModel! + startCallViewModel: startCallViewModel!, + callViewModel: callViewModel! ) } else { SplashScreen() @@ -62,6 +65,7 @@ struct LinphoneApp: App { historyViewModel = HistoryViewModel() historyListViewModel = HistoryListViewModel() startCallViewModel = StartCallViewModel() + callViewModel = CallViewModel() } } } diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift index c12cbed92..55ff1cf40 100644 --- a/Linphone/TelecomManager/ProviderDelegate.swift +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -209,9 +209,11 @@ extension ProviderDelegate: CXProviderDelegate { let callInfo = callInfos[uuid] let callId = callInfo?.callId ?? "" - DispatchQueue.main.async { - withAnimation { - TelecomManager.shared.callInProgress = true + if TelecomManager.shared.callInProgress == false { + DispatchQueue.main.async { + withAnimation { + TelecomManager.shared.callInProgress = true + } } } CoreContext.shared.doOnCoreQueue { core in diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 855e70305..a5270c0e3 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -42,6 +42,8 @@ class TelecomManager: ObservableObject { @Published var callInProgress: Bool = false @Published var callStarted: Bool = false + @Published var remoteVideo: Bool = false + @Published var isRemoteRecording: Bool = false var actionToFulFill: CXCallAction? var callkitAudioSessionActivated: Bool? @@ -125,6 +127,19 @@ class TelecomManager: ObservableObject { } } } + + private func makeRecordFilePath() -> String{ + var filePath = "recording_" + let now = Date() + let dateFormat = DateFormatter() + dateFormat.dateFormat = "E-d-MMM-yyyy-HH-mm-ss" + let date = dateFormat.string(from: now) + filePath = filePath.appending("\(date).mkv") + + let paths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) + 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) @@ -154,6 +169,9 @@ class TelecomManager: ObservableObject { // let writablePath = AppManager.recordingFilePathFromCall(address: addr.username! ) // Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)") // lcallParams.recordFile = writablePath + + lcallParams.recordFile = makeRecordFilePath() + if isSas { lcallParams.mediaEncryption = .ZRTP } @@ -184,9 +202,12 @@ class TelecomManager: ObservableObject { } DispatchQueue.main.async { + self.callStarted = true - withAnimation { - self.callInProgress = true + if self.callInProgress == false { + withAnimation { + self.callInProgress = true + } } } } @@ -195,6 +216,8 @@ class TelecomManager: ObservableObject { func acceptCall(core: Core, call: Call, hasVideo: Bool) { do { let callParams = try core.createCallParams(call: call) + + callParams.recordFile = makeRecordFilePath() callParams.videoEnabled = hasVideo /*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) { let low_bandwidth = (AppManager.network() == .network_2g) @@ -311,12 +334,22 @@ class TelecomManager: ObservableObject { if cstate == .PushIncomingReceived { displayIncomingCall(call: call, handle: "Calling", hasVideo: false, callId: callId, displayName: "Calling") } else { - let video = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) + remoteVideo = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false) - if video { + if remoteVideo { Log.info("[Call] Remote video is activated") } + isRemoteRecording = call.remoteParams?.isRecording ?? false + + if isRemoteRecording && ToastViewModel.shared.toastMessage == "" { + + ToastViewModel.shared.toastMessage = "\(call.remoteAddress) is recording" + ToastViewModel.shared.displayToast.toggle() + + Log.info("[Call] Call is recording by \(call.remoteAddress)") + } + if call.userData == nil { let appData = CallAppData() TelecomManager.setAppData(sCall: call, appData: appData) @@ -351,7 +384,7 @@ class TelecomManager: ObservableObject { providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!) providerDelegate.uuids.removeValue(forKey: callId) providerDelegate.uuids.updateValue(uuid!, forKey: callInfo!.callId) - providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName) + providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: remoteVideo, displayName: displayName) } } else if TelecomManager.callKitEnabled(core: core) { /* @@ -374,9 +407,9 @@ class TelecomManager: ObservableObject { if uuid != nil { // Tha app is now registered, updated the call already existed. - providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName) + providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: remoteVideo, displayName: displayName) } else { - displayIncomingCall(call: call, handle: addr!.asStringUriOnly(), hasVideo: video, callId: callId, displayName: displayName) + displayIncomingCall(call: call, handle: addr!.asStringUriOnly(), hasVideo: remoteVideo, callId: callId, displayName: displayName) } } /* else if UIApplication.shared.applicationState != .active { // not support callkit , use notif diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index cd23af51e..dc681036d 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -273,7 +273,7 @@ struct CallView: View { .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background(callViewModel.isRecording ? Color.redDanger500 : Color.gray500) .cornerRadius(40) Text("Record") @@ -572,6 +572,28 @@ struct CallView: View { ) } + if callViewModel.isRecording { + HStack { + VStack { + Image("record-fill") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 32, height: 32) + .padding(10) + .if(fullscreenVideo) { view in + view.padding(.top, 30) + } + Spacer() + } + 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 + ) + } + if !telecomManager.callStarted && !fullscreenVideo { VStack { ActivityIndicator() diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 834b8874b..382e41e5e 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -33,6 +33,8 @@ class CallViewModel: ObservableObject { @Published var avatarModel: ContactAvatarModel? @Published var micMutted: Bool = false @Published var cameraDisplayed: Bool = false + @Published var isRecording: Bool = false + @Published var isRemoteRecording: Bool = false @State var timeElapsed: Int = 0 let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @@ -40,7 +42,6 @@ class CallViewModel: ObservableObject { var currentCall: Call? init() { - do { try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth) try AVAudioSession.sharedInstance().setActive(true) @@ -48,6 +49,10 @@ class CallViewModel: ObservableObject { } + resetCallView() + } + + func resetCallView() { coreContext.doOnCoreQueue { core in if core.currentCall != nil && core.currentCall!.remoteAddress != nil { self.currentCall = core.currentCall @@ -70,6 +75,7 @@ class CallViewModel: ObservableObject { //self.avatarModel = ??? self.micMutted = self.currentCall!.microphoneMuted self.cameraDisplayed = self.currentCall!.cameraEnabled == true + self.isRecording = self.currentCall!.params!.isRecording } } } @@ -164,10 +170,9 @@ class CallViewModel: ObservableObject { } else { Log.info("[CallViewModel] Starting call recording \(self.currentCall!.params!.isRecording)") self.currentCall!.startRecording() - Log.info("[CallViewModel] Starting call recording \(self.currentCall!.params!.isRecording)") } - //var recording = self.currentCall!.params!.isRecording - //isRecording.postValue(recording) + + self.isRecording = self.currentCall!.params!.isRecording } } } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 64f63dc5b..ee403524a 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -38,6 +38,7 @@ struct ContentView: View { @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var startCallViewModel: StartCallViewModel + @ObservedObject var callViewModel: CallViewModel @State var index = 0 @State private var orientation = UIDevice.current.orientation @@ -661,9 +662,12 @@ struct ContentView: View { } if telecomManager.callInProgress { - CallView(callViewModel: CallViewModel()) + CallView(callViewModel: callViewModel) .zIndex(3) .transition(.scale.combined(with: .move(edge: .top))) + .onAppear { + callViewModel.resetCallView() + } } // if sharedMainViewModel.displayToast { @@ -722,7 +726,8 @@ struct ContentView: View { editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel(), historyListViewModel: HistoryListViewModel(), - startCallViewModel: StartCallViewModel() + startCallViewModel: StartCallViewModel(), + callViewModel: CallViewModel() ) } // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 40d5e7e16..025479ae0 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -54,6 +54,13 @@ struct ToastView: View { .foregroundStyle(Color.greenSuccess500) .default_text_style(styleSize: 15) .padding(8) + + case let str where str.contains("is recording"): + Text(toastViewModel.toastMessage) + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) case "Failed": Text("Invalid QR code!") From 8469e8583edab4aa7bf2fccefa1f5430a891782b Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 9 Jan 2024 12:21:07 +0100 Subject: [PATCH 02/16] Toast display when user records call --- Linphone/TelecomManager/TelecomManager.swift | 34 +++++++++++++++++--- Linphone/UI/Main/Fragments/ToastView.swift | 17 ++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index a5270c0e3..22372bc1a 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -342,12 +342,38 @@ class TelecomManager: ObservableObject { isRemoteRecording = call.remoteParams?.isRecording ?? false - if isRemoteRecording && ToastViewModel.shared.toastMessage == "" { + if isRemoteRecording && ToastViewModel.shared.toastMessage.isEmpty { - ToastViewModel.shared.toastMessage = "\(call.remoteAddress) is recording" - ToastViewModel.shared.displayToast.toggle() + DispatchQueue.main.async { + var displayName = "" + let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!) + if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { + displayName = friend!.address!.displayName! + } else { + if call.remoteAddress!.displayName != nil { + displayName = call.remoteAddress!.displayName! + } else if call.remoteAddress!.username != nil { + displayName = call.remoteAddress!.username! + } + } + + ToastViewModel.shared.toastMessage = "\(displayName) is recording" + ToastViewModel.shared.displayToast = true + } - Log.info("[Call] Call is recording by \(call.remoteAddress)") + Log.info("[Call] Call is recording by \(call.remoteAddress!.asStringUriOnly())") + } + + if !isRemoteRecording && ToastViewModel.shared.toastMessage.contains("is recording") { + + DispatchQueue.main.async { + withAnimation { + ToastViewModel.shared.toastMessage = "" + ToastViewModel.shared.displayToast = false + } + } + + Log.info("[Call] Call is recording Stop recording by \(call.remoteAddress!.asStringUriOnly())") } if call.userData == nil { diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 025479ae0..9f8dc277e 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -100,19 +100,22 @@ struct ToastView: View { .stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) ) .onTapGesture { - withAnimation { - toastViewModel.toastMessage = "" - toastViewModel.displayToast = false - } - } - .onAppear { - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + if !toastViewModel.toastMessage.contains("is recording") { withAnimation { toastViewModel.toastMessage = "" toastViewModel.displayToast = false } } } + .onAppear { + if !toastViewModel.toastMessage.contains("is recording") { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + withAnimation { + toastViewModel.toastMessage = "" + toastViewModel.displayToast = false + } + } + } } Spacer() } From d97f07942f4b756401db0d6b8dd2f3adb886b335 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 9 Jan 2024 16:42:11 +0100 Subject: [PATCH 03/16] 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 { From 1ddf2602b91b8c6694260eb542ff2794c3f8eed9 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Tue, 9 Jan 2024 16:50:40 +0100 Subject: [PATCH 04/16] Fix or disable several swiftlint warnings --- Linphone/UI/Call/CallView.swift | 4 +++- .../UI/Main/Contacts/Fragments/ContactsListFragment.swift | 5 ++++- Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift | 6 ++++-- Linphone/UI/Main/ContentView.swift | 2 ++ .../UI/Main/History/Fragments/HistoryContactFragment.swift | 4 ++++ .../UI/Main/History/Fragments/HistoryListFragment.swift | 4 ++++ 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 9f1d99f5a..4c0bec411 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -18,6 +18,7 @@ */ // swiftlint:disable type_body_length +// swiftlint:disable line_length import SwiftUI import CallKit import AVFAudio @@ -131,7 +132,7 @@ struct CallView: View { .foregroundStyle(.white) .frame(width: 32, height: 32) .onAppear(perform: getAudioRouteImage) - .onReceive(pub) { (output) in + .onReceive(pub) { _ in self.getAudioRouteImage() } @@ -836,3 +837,4 @@ struct CallView: View { CallView(callViewModel: CallViewModel()) } // swiftlint:enable type_body_length +// swiftlint:enable line_length diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 41bfc686b..3f594f977 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -98,5 +98,8 @@ struct ContactsListFragment: View { } #Preview { - ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: {_ in }) + ContactsListFragment(contactViewModel: ContactViewModel() + , contactsListViewModel: ContactsListViewModel() + , showingSheet: .constant(false) + , startCallFunc: {_ in }) } diff --git a/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift b/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift index fb469c160..efd2e1426 100644 --- a/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift +++ b/Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift @@ -44,7 +44,8 @@ class ContactAvatarModel: ObservableObject { if friend!.consolidatedPresence == .Online || friend!.consolidatedPresence == .Busy { if friend!.consolidatedPresence == .Online || friend!.presenceModel!.latestActivityTimestamp != -1 { - self.lastPresenceInfo = friend!.consolidatedPresence == .Online ? "Online" : getCallTime(startDate: friend!.presenceModel!.latestActivityTimestamp) + self.lastPresenceInfo = (friend!.consolidatedPresence == .Online) ? + "Online" : getCallTime(startDate: friend!.presenceModel!.latestActivityTimestamp) } else { self.lastPresenceInfo = "Away" } @@ -68,7 +69,8 @@ class ContactAvatarModel: ObservableObject { self.presenceStatus = cbValue.consolidatedPresence if cbValue.consolidatedPresence == .Online || cbValue.consolidatedPresence == .Busy { if cbValue.consolidatedPresence == .Online || cbValue.presenceModel!.latestActivityTimestamp != -1 { - self.lastPresenceInfo = cbValue.consolidatedPresence == .Online ? "Online" : self.getCallTime(startDate: cbValue.presenceModel!.latestActivityTimestamp) + self.lastPresenceInfo = cbValue.consolidatedPresence == .Online ? + "Online" : self.getCallTime(startDate: cbValue.presenceModel!.latestActivityTimestamp) } else { self.lastPresenceInfo = "Away" } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index ee403524a..69886fde9 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -18,6 +18,7 @@ */ // swiftlint:disable type_body_length +// swiftlint:disable line_length import SwiftUI import linphonesw @@ -731,3 +732,4 @@ struct ContentView: View { ) } // swiftlint:enable type_body_length +// swiftlint:enable line_length diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 7ebc04890..370009462 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +// swiftlint:disable line_length + import SwiftUI import UniformTypeIdentifiers @@ -549,3 +551,5 @@ struct HistoryContactFragment: View { indexPage: .constant(1) ) } + +// swiftlint:enable line_length diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index ceafcb884..71af38981 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +// swiftlint:disable line_length + import SwiftUI import linphonesw @@ -217,3 +219,5 @@ struct HistoryListFragment: View { #Preview { HistoryListFragment(historyListViewModel: HistoryListViewModel(), historyViewModel: HistoryViewModel(), showingSheet: .constant(false)) } + +// swiftlint:enable line_length From 04dbce540cea98bcdf22d0b546a7e0ddb1ade7af Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 9 Jan 2024 17:29:51 +0100 Subject: [PATCH 05/16] Fixes --- .../AppIcon.appiconset/1024.png | Bin 0 -> 41298 bytes .../AppIcon.appiconset/Contents.json | 1 + Linphone/Core/CoreContext.swift | 45 ++- Linphone/Info.plist | 2 + Linphone/LinphoneApp.swift | 8 +- Linphone/TelecomManager/TelecomManager.swift | 4 +- .../Assistant/Fragments/LoginFragment.swift | 6 + .../Fragments/RegisterFragment.swift | 3 + .../ThirdPartySipAccountLoginFragment.swift | 10 +- .../ThirdPartySipAccountWarningFragment.swift | 3 + .../Viewmodel/AccountLoginViewModel.swift | 5 +- Linphone/UI/Call/CallView.swift | 153 +++++++- Linphone/UI/Main/Contacts/ContactsView.swift | 4 +- .../ContactInnerActionsFragment.swift | 4 +- .../Fragments/ContactInnerFragment.swift | 44 +-- .../Fragments/ContactsListBottomSheet.swift | 2 + Linphone/UI/Main/ContentView.swift | 9 +- .../History/Fragments/DialerBottomSheet.swift | 2 +- .../Fragments/HistoryContactFragment.swift | 364 +++++++++--------- .../Fragments/HistoryListFragment.swift | 6 +- .../History/Fragments/StartCallFragment.swift | 65 ++-- Linphone/UI/Main/History/HistoryView.swift | 4 +- Linphone/UI/Welcome/WelcomeView.swift | 2 + Linphone/Utils/PermissionManager.swift | 10 + 24 files changed, 474 insertions(+), 282 deletions(-) create mode 100644 Linphone/Assets.xcassets/AppIcon.appiconset/1024.png diff --git a/Linphone/Assets.xcassets/AppIcon.appiconset/1024.png b/Linphone/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000000000000000000000000000000000000..7b3df457925f29eb1b3ce0ef1dc0b7380a49b381 GIT binary patch literal 41298 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 literal 0 HcmV?d00001 diff --git a/Linphone/Assets.xcassets/AppIcon.appiconset/Contents.json b/Linphone/Assets.xcassets/AppIcon.appiconset/Contents.json index 532cd729c..eab818e5a 100644 --- a/Linphone/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Linphone/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "1024.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index a07c6b926..a7a0abb4a 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -38,7 +38,7 @@ final class CoreContext: ObservableObject { private var mCore: Core! private var mIterateSuscription: AnyCancellable? private var mCoreSuscriptions = Set() - + private init() { do { try initialiseCore() @@ -68,17 +68,17 @@ final class CoreContext: ObservableObject { Factory.Instance.logCollectionPath = configDir Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled) - let url = NSURL(fileURLWithPath: configDir) - if let pathComponent = url.appendingPathComponent("linphonerc") { - let filePath = pathComponent.path - let fileManager = FileManager.default - if !fileManager.fileExists(atPath: filePath) { - let path = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) - if path != nil { - try? FileManager.default.copyItem(at: NSURL(fileURLWithPath: path!) as URL, to: pathComponent) - } - } - } + let url = NSURL(fileURLWithPath: configDir) + if let pathComponent = url.appendingPathComponent("linphonerc") { + let filePath = pathComponent.path + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: filePath) { + let path = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) + if path != nil { + try? FileManager.default.copyItem(at: NSURL(fileURLWithPath: path!) as URL, to: pathComponent) + } + } + } let config = try? Factory.Instance.createConfigWithFactory( path: "\(configDir)/linphonerc", @@ -87,7 +87,7 @@ final class CoreContext: ObservableObject { if config != nil { self.mCore = try? Factory.Instance.createCoreWithConfig(config: config!, systemContext: nil) } - + self.mCore.autoIterateEnabled = false self.mCore.callkitEnabled = true self.mCore.pushNotificationEnabled = true @@ -113,11 +113,14 @@ final class CoreContext: ObservableObject { NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n") if cbVal.status == Config.ConfiguringState.Successful { ToastViewModel.shared.toastMessage = "Successful" - ToastViewModel.shared.displayToast.toggle() - } else { - ToastViewModel.shared.toastMessage = "Failed" - ToastViewModel.shared.displayToast.toggle() - } + ToastViewModel.shared.displayToast = true + } + /* + else { + ToastViewModel.shared.toastMessage = "Failed" + ToastViewModel.shared.displayToast = true + } + */ }) self.mCoreSuscriptions.insert(self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in @@ -135,7 +138,7 @@ final class CoreContext: ObservableObject { self.loggingInProgress = true } else { ToastViewModel.shared.toastMessage = "Registration failed" - ToastViewModel.shared.displayToast.toggle() + ToastViewModel.shared.displayToast = true self.loggingInProgress = false self.loggedIn = false } @@ -166,9 +169,9 @@ final class CoreContext: ObservableObject { cbValue.info, forPasteboardType: UTType.plainText.identifier ) - + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" - ToastViewModel.shared.displayToast.toggle() + ToastViewModel.shared.displayToast = true } }) diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 340c36eb2..33e2c5a4d 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -2,6 +2,8 @@ + UIUserInterfaceStyle + Light NSCameraUsageDescription Camera usage is required for video VOIP calls NSMicrophoneUsageDescription diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 950f6722f..9596a76ce 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -38,8 +38,14 @@ struct LinphoneApp: App { if !sharedMainViewModel.welcomeViewDisplayed { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { - AssistantView() + ZStack { + AssistantView() + + ToastView() + .zIndex(3) + } } else if coreContext.defaultAccount != nil + && coreContext.loggedIn && contactViewModel != nil && editContactViewModel != nil && historyViewModel != nil diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index 506ba21e4..aa93fd28a 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -120,10 +120,10 @@ class TelecomManager: ObservableObject { } } - func doCallWithCore(addr: Address) { + func doCallWithCore(addr: Address, isVideo: Bool) { CoreContext.shared.doOnCoreQueue { core in do { - try self.startCallCallKit(core: core, addr: addr, isSas: false, isVideo: false, isConference: false) + try self.startCallCallKit(core: core, addr: addr, isSas: false, isVideo: isVideo, isConference: false) } catch { Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr) \(error) ") } diff --git a/Linphone/UI/Assistant/Fragments/LoginFragment.swift b/Linphone/UI/Assistant/Fragments/LoginFragment.swift index 8ae3e46e1..51fbd099c 100644 --- a/Linphone/UI/Assistant/Fragments/LoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/LoginFragment.swift @@ -63,6 +63,8 @@ struct LoginFragment: View { TextField("username", text: $accountLoginViewModel.username) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .disabled(coreContext.loggedIn) .frame(height: 25) .padding(.horizontal, 20) @@ -90,6 +92,8 @@ struct LoginFragment: View { } else { TextField("password", text: $accountLoginViewModel.passwd) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .frame(height: 25) .focused($isPasswordFocused) } @@ -287,6 +291,8 @@ struct LoginFragment: View { .background(.black.opacity(0.65)) } } + .navigationTitle("") + .navigationBarHidden(true) } .navigationViewStyle(StackNavigationViewStyle()) } diff --git a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift index 194bd5b5a..dbc339513 100644 --- a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift +++ b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift @@ -65,8 +65,11 @@ struct RegisterFragment: View { } } } + .navigationTitle("") + .navigationBarHidden(true) } .navigationViewStyle(StackNavigationViewStyle()) + .navigationTitle("") .navigationBarHidden(true) } } diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift index 6bb7e3bd0..8a8d5aa0b 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift @@ -81,6 +81,8 @@ struct ThirdPartySipAccountLoginFragment: View { TextField("username", text: $accountLoginViewModel.username) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .disabled(coreContext.loggedIn) .frame(height: 25) .padding(.horizontal, 20) @@ -108,6 +110,8 @@ struct ThirdPartySipAccountLoginFragment: View { } else { TextField("password", text: $accountLoginViewModel.passwd) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .frame(height: 25) .focused($isPasswordFocused) } @@ -139,6 +143,8 @@ struct ThirdPartySipAccountLoginFragment: View { TextField("sip.linphone.org", text: $accountLoginViewModel.domain) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .disabled(coreContext.loggedIn) .frame(height: 25) .padding(.horizontal, 20) @@ -158,6 +164,8 @@ struct ThirdPartySipAccountLoginFragment: View { TextField("Display Name", text: $accountLoginViewModel.displayName) .default_text_style(styleSize: 15) + .disableAutocorrection(true) + .autocapitalization(.none) .disabled(coreContext.loggedIn) .frame(height: 25) .padding(.horizontal, 20) @@ -204,8 +212,6 @@ struct ThirdPartySipAccountLoginFragment: View { Button(action: { self.accountLoginViewModel.login() - accountLoginViewModel.domain = "sip.linphone.org" - accountLoginViewModel.transportType = "TLS" }, label: { Text(coreContext.loggedIn ? "Log out" : "assistant_account_login") .default_text_style_white_600(styleSize: 20) diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift index a3aa14ad5..f9f9fb1f8 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift @@ -171,8 +171,11 @@ struct ThirdPartySipAccountWarningFragment: View { .frame(minHeight: geometry.size.height) } } + .navigationTitle("") + .navigationBarHidden(true) } .navigationViewStyle(StackNavigationViewStyle()) + .navigationTitle("") .navigationBarHidden(true) } } diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index dd9149693..8cf56625f 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -45,7 +45,7 @@ class AccountLoginViewModel: ObservableObject { core.loadConfigFromXml(xmlUri: assistantLinphone) } } - + // Get the transport protocol to use. // TLS is strongly recommended // Only use UDP if you don't have the choice @@ -106,6 +106,9 @@ class AccountLoginViewModel: ObservableObject { self.coreContext.defaultAccount = account } + self.domain = "sip.linphone.org" + self.transportType = "TLS" + } catch { NSLog(error.localizedDescription) } } } diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 4c0bec411..8d06e91b6 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -427,18 +427,29 @@ struct CallView: View { .presentationDetents([.fraction(0.3)]) .frame(maxHeight: .infinity) } + } else { + innerView(geometry: geo) } } } @ViewBuilder + // swiftlint:disable:next cyclomatic_complexity func innerView(geometry: GeometryProxy) -> some View { VStack { if !fullscreenVideo { - Rectangle() - .foregroundColor(Color.orangeMain500) - .edgesIgnoringSafeArea(.top) - .frame(height: 0) + if #available(iOS 16.0, *) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + } else if idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 1) + } HStack { if callViewModel.direction == .Outgoing { @@ -465,13 +476,8 @@ struct CallView: View { 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 @@ -646,6 +652,10 @@ struct CallView: View { callViewModel.timeElapsed = Int(firedDate.timeIntervalSince(startDate)) } + .onDisappear { + callViewModel.timeElapsed = 0 + startDate = Date.now + } .padding(.top) .foregroundStyle(.white) @@ -695,16 +705,101 @@ struct CallView: View { if !fullscreenVideo { if telecomManager.callStarted { - if telecomManager.callStarted && idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight - || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { - HStack(spacing: 12) { - HStack { - + if #available(iOS 16.0, *) { + if telecomManager.callStarted && idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + HStack(spacing: 12) { + HStack { + + } + .frame(height: 60) } - .frame(height: 60) + .padding(.horizontal, 25) + .padding(.top, 20) + } else { + 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 { + callViewModel.toggleVideo() + } label: { + Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") + .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: { + Image(callViewModel.micMutted ? "microphone-slash" : "microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(callViewModel.micMutted ? .black : .white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background(callViewModel.micMutted ? .white : Color.gray500) + .cornerRadius(40) + + Button { + if AVAudioSession.sharedInstance().availableInputs != nil + && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { + + hideButtonsSheet = true + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + audioRouteSheet = true + } + } else { + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) + } catch _ { + + } + } + + } label: { + Image(imageAudioRoute) + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + .onAppear(perform: getAudioRouteImage) + .onReceive(pub) { _ in + self.getAudioRouteImage() + } + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + } + .frame(height: geometry.size.height * 0.15) + .padding(.horizontal, 20) + .padding(.top, -6) } - .padding(.horizontal, 25) - .padding(.top, 20) } else { HStack(spacing: 12) { Button { @@ -726,7 +821,7 @@ struct CallView: View { Button { callViewModel.toggleVideo() } label: { - Image("video-camera") + Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") .renderingMode(.template) .resizable() .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) @@ -753,12 +848,32 @@ struct CallView: View { .cornerRadius(40) Button { + if AVAudioSession.sharedInstance().availableInputs != nil + && !AVAudioSession.sharedInstance().availableInputs!.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty { + + hideButtonsSheet = true + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { + audioRouteSheet = true + } + } else { + do { + try AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty ? .speaker : .none) + } catch _ { + + } + } + } label: { - Image("speaker-high") + Image(imageAudioRoute) .renderingMode(.template) .resizable() .foregroundStyle(.white) .frame(width: 32, height: 32) + .onAppear(perform: getAudioRouteImage) + .onReceive(pub) { _ in + self.getAudioRouteImage() + } } .frame(width: 60, height: 60) diff --git a/Linphone/UI/Main/Contacts/ContactsView.swift b/Linphone/UI/Main/Contacts/ContactsView.swift index a8191f047..fea21d9c7 100644 --- a/Linphone/UI/Main/Contacts/ContactsView.swift +++ b/Linphone/UI/Main/Contacts/ContactsView.swift @@ -41,8 +41,10 @@ struct ContactsView: View { } } label: { Image("user-plus") + .renderingMode(.template) + .foregroundStyle(.white) .padding() - .background(.white) + .background(Color.orangeMain500) .clipShape(Circle()) .shadow(color: .black.opacity(0.2), radius: 4) diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index 0e256c39e..f15e7bd3a 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -90,7 +90,7 @@ struct ContactInnerActionsFragment: View { .onTapGesture { withAnimation { telecomManager.doCallWithCore( - addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index] + addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index], isVideo: false ) } } @@ -272,7 +272,9 @@ struct ContactInnerActionsFragment: View { Button { if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { contactViewModel.objectWillChange.send() + contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.edit() contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.starred.toggle() + contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.done() } } label: { HStack { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index a2627196a..72d4baece 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -86,19 +86,19 @@ struct ContactInnerFragment: View { contactViewModel: contactViewModel, isShowEditContactFragment: .constant(false), isShowDismissPopup: $isShowDismissPopup)) { - Image("pencil-simple") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.orangeMain500) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.top, 2) - } - .simultaneousGesture( - TapGesture().onEnded { - editContactViewModel.selectedEditFriend = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend - editContactViewModel.resetValues() + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) } - ) + .simultaneousGesture( + TapGesture().onEnded { + editContactViewModel.selectedEditFriend = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend + editContactViewModel.resetValues() + } + ) } } .frame(maxWidth: .infinity) @@ -132,10 +132,10 @@ struct ContactInnerFragment: View { .frame(maxWidth: .infinity) .padding(.top, 10) - Text(contactAvatarModel.lastPresenceInfo) + Text(contactAvatarModel.lastPresenceInfo) .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" - ? Color.greenSuccess500 - : Color.orangeWarning600) + ? Color.greenSuccess500 + : Color.orangeWarning600) .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) .frame(maxWidth: .infinity) @@ -151,7 +151,7 @@ struct ContactInnerFragment: View { Spacer() Button(action: { - telecomManager.doCallWithCore(addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.address!) + telecomManager.doCallWithCore(addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.address!, isVideo: false) }, label: { VStack { HStack(alignment: .center) { @@ -180,7 +180,8 @@ struct ContactInnerFragment: View { Image("chat-teardrop-text") .renderingMode(.template) .resizable() - .foregroundStyle(Color.grayMain2c600) + //.foregroundStyle(Color.grayMain2c600) + .foregroundStyle(Color.grayMain2c300) .frame(width: 25, height: 25) .onTapGesture { withAnimation { @@ -200,7 +201,7 @@ struct ContactInnerFragment: View { Spacer() Button(action: { - + telecomManager.doCallWithCore(addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.address!, isVideo: true) }, label: { VStack { HStack(alignment: .center) { @@ -209,11 +210,6 @@ struct ContactInnerFragment: View { .resizable() .foregroundStyle(Color.grayMain2c600) .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } } .padding(16) .background(Color.grayMain2c200) @@ -229,7 +225,7 @@ struct ContactInnerFragment: View { .padding(.top, 20) .frame(maxWidth: .infinity) .background(Color.gray100) - + ContactInnerActionsFragment( contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift index bf6a9645d..644e6f16c 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListBottomSheet.swift @@ -58,7 +58,9 @@ struct ContactsListBottomSheet: View { Spacer() Button { if contactViewModel.selectedFriend != nil { + contactViewModel.selectedFriend!.edit() contactViewModel.selectedFriend!.starred.toggle() + contactViewModel.selectedFriend!.done() } MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 69886fde9..267968710 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -151,6 +151,7 @@ struct ContentView: View { Menu { if index == 0 { Button { + contactViewModel.indexDisplayedFriend = nil isMenuOpen = false magicSearch.allContact = true MagicSearchSingleton.shared.searchForContacts( @@ -168,6 +169,7 @@ struct ContentView: View { } Button { + contactViewModel.indexDisplayedFriend = nil isMenuOpen = false magicSearch.allContact = false MagicSearchSingleton.shared.searchForContacts( @@ -282,9 +284,8 @@ struct ContentView: View { text = newValue } )) - .default_text_style_white_700(styleSize: 15) + .default_text_style_700(styleSize: 15) .padding(.all, 6) - .accentColor(.white) .focused($focusedField) .onAppear { self.focusedField = true @@ -671,10 +672,8 @@ struct ContentView: View { } } - // if sharedMainViewModel.displayToast { ToastView() .zIndex(3) - // } } } .overlay { @@ -698,12 +697,14 @@ struct ContentView: View { .onChange(of: scenePhase) { newPhase in if newPhase == .active { coreContext.onForeground() + /* if !isShowStartCallFragment { contactsManager.fetchContacts() DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { historyListViewModel.computeCallLogsList() } } + */ print("Active") } else if newPhase == .inactive { print("Inactive") diff --git a/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift b/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift index dc3915be6..f8d2b0ddd 100644 --- a/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift +++ b/Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift @@ -275,7 +275,7 @@ struct DialerBottomSheet: View { if !startCallViewModel.searchField.isEmpty { do { let address = try Factory.Instance.createAddress(addr: String("sip:" + startCallViewModel.searchField + "@" + startCallViewModel.domain)) - telecomManager.doCallWithCore(addr: address) + telecomManager.doCallWithCore(addr: address, isVideo: false) } catch { } diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 370009462..a2fd12066 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -32,15 +32,15 @@ struct HistoryContactFragment: View { @ObservedObject var contactAvatarModel: ContactAvatarModel @ObservedObject var historyViewModel: HistoryViewModel - @ObservedObject var historyListViewModel: HistoryListViewModel - @ObservedObject var contactViewModel: ContactViewModel - @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel @State var isMenuOpen = false @Binding var isShowDeleteAllHistoryPopup: Bool - @Binding var isShowEditContactFragment: Bool - @Binding var indexPage: Int + @Binding var isShowEditContactFragment: Bool + @Binding var indexPage: Int var body: some View { NavigationView { @@ -72,25 +72,25 @@ struct HistoryContactFragment: View { Spacer() Menu { - let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil - let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil - let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil - + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + Button { isMenuOpen = false - - if contactsManager.getFriendWithAddress( - address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing - ? historyViewModel.displayedCall!.toAddress! - : historyViewModel.displayedCall!.fromAddress! - ) != nil { - let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing - ? historyViewModel.displayedCall!.toAddress! - : historyViewModel.displayedCall!.fromAddress! - - let friendIndex = contactsManager.lastSearch.firstIndex( - where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) - if friendIndex != nil { + + if contactsManager.getFriendWithAddress( + address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + ) != nil { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + let friendIndex = contactsManager.lastSearch.firstIndex( + where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + if friendIndex != nil { withAnimation { historyViewModel.displayedCall = nil @@ -98,28 +98,28 @@ struct HistoryContactFragment: View { contactViewModel.indexDisplayedFriend = friendIndex } - } - } else { - let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing - ? historyViewModel.displayedCall!.toAddress! - : historyViewModel.displayedCall!.fromAddress! - - withAnimation { + } + } else { + let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing + ? historyViewModel.displayedCall!.toAddress! + : historyViewModel.displayedCall!.fromAddress! + + withAnimation { historyViewModel.displayedCall = nil indexPage = 0 isShowEditContactFragment.toggle() - editContactViewModel.sipAddresses.removeAll() - editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) editContactViewModel.sipAddresses.append("") - } - } - + } + } + } label: { HStack { - Text(addressFriend != nil ? "See contact" : "Add to contacts") + Text(addressFriend != nil ? "See contact" : "Add to contacts") Spacer() - Image(addressFriend != nil ? "user-circle" : "plus-circle") + Image(addressFriend != nil ? "user-circle" : "plus-circle") .resizable() .frame(width: 25, height: 25, alignment: .leading) } @@ -127,18 +127,18 @@ struct HistoryContactFragment: View { Button { isMenuOpen = false - - if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { - UIPasteboard.general.setValue( - historyViewModel.displayedCall!.toAddress!.asStringUriOnly().dropFirst(4), - forPasteboardType: UTType.plainText.identifier - ) - } else { - UIPasteboard.general.setValue( - historyViewModel.displayedCall!.fromAddress!.asStringUriOnly().dropFirst(4), - forPasteboardType: UTType.plainText.identifier - ) - } + + if historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.toAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } else { + UIPasteboard.general.setValue( + historyViewModel.displayedCall!.fromAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" ToastViewModel.shared.displayToast.toggle() @@ -194,8 +194,12 @@ struct HistoryContactFragment: View { ScrollView { VStack(spacing: 0) { VStack(spacing: 0) { + if #unavailable(iOS 16.0) { + Rectangle() + .foregroundColor(Color.gray100) + .frame(height: 7) + } VStack(spacing: 0) { - let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil @@ -223,13 +227,13 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - - Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) Text("") .multilineTextAlignment(.center) @@ -252,13 +256,13 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - - Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) + + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) Text("") .multilineTextAlignment(.center) @@ -284,14 +288,14 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - - Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) - + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + Text("") .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) @@ -313,14 +317,14 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - - Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) - + + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + Text("") .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) @@ -338,22 +342,22 @@ struct HistoryContactFragment: View { .default_text_style(styleSize: 14) .frame(maxWidth: .infinity) .padding(.top, 10) - - if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { - Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) - } else if historyViewModel.displayedCall!.fromAddress != nil { - Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) - .foregroundStyle(Color.grayMain2c700) - .multilineTextAlignment(.center) - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity) - .padding(.top, 5) - } + + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + Text(historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } else if historyViewModel.displayedCall!.fromAddress != nil { + Text(historyViewModel.displayedCall!.fromAddress!.asStringUriOnly()) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 5) + } Text(contactAvatarModel.lastPresenceInfo) .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" @@ -369,21 +373,22 @@ struct HistoryContactFragment: View { .frame(minHeight: 150) .frame(maxWidth: .infinity) .padding(.top, 10) + .padding(.bottom, 2) .background(Color.gray100) HStack { Spacer() Button(action: { - if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { - telecomManager.doCallWithCore( - addr: historyViewModel.displayedCall!.toAddress! - ) - } else if historyViewModel.displayedCall!.dir == .Incoming && historyViewModel.displayedCall!.fromAddress != nil { - telecomManager.doCallWithCore( - addr: historyViewModel.displayedCall!.fromAddress! - ) - } + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + telecomManager.doCallWithCore( + addr: historyViewModel.displayedCall!.toAddress!, isVideo: false + ) + } else if historyViewModel.displayedCall!.dir == .Incoming && historyViewModel.displayedCall!.fromAddress != nil { + telecomManager.doCallWithCore( + addr: historyViewModel.displayedCall!.fromAddress!, isVideo: false + ) + } }, label: { VStack { HStack(alignment: .center) { @@ -399,6 +404,7 @@ struct HistoryContactFragment: View { Text("Appel") .default_text_style(styleSize: 14) + .frame(minWidth: 80) } }) @@ -412,7 +418,8 @@ struct HistoryContactFragment: View { Image("chat-teardrop-text") .renderingMode(.template) .resizable() - .foregroundStyle(Color.grayMain2c600) + //.foregroundStyle(Color.grayMain2c600) + .foregroundStyle(Color.grayMain2c300) .frame(width: 25, height: 25) .onTapGesture { withAnimation { @@ -426,13 +433,22 @@ struct HistoryContactFragment: View { Text("Message") .default_text_style(styleSize: 14) + .frame(minWidth: 80) } }) Spacer() Button(action: { - + if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { + telecomManager.doCallWithCore( + addr: historyViewModel.displayedCall!.toAddress!, isVideo: true + ) + } else if historyViewModel.displayedCall!.dir == .Incoming && historyViewModel.displayedCall!.fromAddress != nil { + telecomManager.doCallWithCore( + addr: historyViewModel.displayedCall!.fromAddress!, isVideo: true + ) + } }, label: { VStack { HStack(alignment: .center) { @@ -441,11 +457,6 @@ struct HistoryContactFragment: View { .resizable() .foregroundStyle(Color.grayMain2c600) .frame(width: 25, height: 25) - .onTapGesture { - withAnimation { - - } - } } .padding(16) .background(Color.grayMain2c200) @@ -453,71 +464,75 @@ struct HistoryContactFragment: View { Text("Video Call") .default_text_style(styleSize: 14) + .frame(minWidth: 80) } }) Spacer() } .padding(.top, 20) + .padding(.bottom, 10) .frame(maxWidth: .infinity) .background(Color.gray100) VStack(spacing: 0) { - - let addressFriend = historyViewModel.displayedCall != nil - ? (historyViewModel.displayedCall!.dir == .Incoming ? historyViewModel.displayedCall!.fromAddress!.asStringUriOnly() - : historyViewModel.displayedCall!.toAddress!.asStringUriOnly()) : nil - - let callLogsFilter = historyListViewModel.callLogs.filter({ $0.dir == .Incoming - ? $0.fromAddress!.asStringUriOnly() == addressFriend - : $0.toAddress!.asStringUriOnly() == addressFriend }) - - ForEach(0.. Date: Fri, 12 Jan 2024 14:58:39 +0100 Subject: [PATCH 06/16] Fix side menu buttons --- Linphone/Core/CoreContext.swift | 6 ++- Linphone/Localizable.xcstrings | 8 +-- Linphone/UI/Main/Fragments/SideMenu.swift | 61 +++++++++++++++++----- Linphone/UI/Main/Fragments/ToastView.swift | 14 +++++ 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index a7a0abb4a..e71947f38 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -170,8 +170,10 @@ final class CoreContext: ObservableObject { forPasteboardType: UTType.plainText.identifier ) - ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" - ToastViewModel.shared.displayToast = true + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = "Success_send_logs" + ToastViewModel.shared.displayToast = true + } } }) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 8dc0dee51..ec823b015 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -371,7 +371,10 @@ "Log out" : { }, - "Logout" : { + "Logs cleared" : { + + }, + "Logs URL copied into clipboard" : { }, "Message" : { @@ -382,9 +385,6 @@ }, "Missed call" : { - }, - "My Profile" : { - }, "New call" : { diff --git a/Linphone/UI/Main/Fragments/SideMenu.swift b/Linphone/UI/Main/Fragments/SideMenu.swift index 029f90ff1..e9fdc8d70 100644 --- a/Linphone/UI/Main/Fragments/SideMenu.swift +++ b/Linphone/UI/Main/Fragments/SideMenu.swift @@ -43,19 +43,44 @@ struct SideMenu: View { HStack { List { - Text("My Profile").onTapGesture { - print("My Profile") - } - Text("Send logs").onTapGesture { - sendLogs() - } - Text("Clear logs").onTapGesture { - print("Clear logs") - Core.resetLogCollection() - } - Text("Logout").onTapGesture { - print("Logout") - } + /* + Text("My Profile") + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .onTapGesture { + print("My Profile") + self.menuClose() + } + */ + Text("Send logs") + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .onTapGesture { + print("Send logs") + sendLogs() + self.menuClose() + } + Text("Clear logs") + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .onTapGesture { + print("Clear logs") + clearLogs() + self.menuClose() + } + /* + Text("Logout") + .frame(height: 40) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.white) + .onTapGesture { + print("Logout") + self.menuClose() + } + */ } .frame(width: self.width - safeAreaInsets.leading) .background(Color.white) @@ -75,4 +100,14 @@ struct SideMenu: View { core.uploadLogCollection() } } + + func clearLogs() { + coreContext.doOnCoreQueue { core in + Core.resetLogCollection() + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = "Success_clear_logs" + ToastViewModel.shared.displayToast = true + } + } + } } diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 9f8dc277e..565128b7e 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -48,6 +48,20 @@ struct ToastView: View { .default_text_style(styleSize: 15) .padding(8) + case "Success_clear_logs": + Text("Logs cleared") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Success_send_logs": + Text("Logs URL copied into clipboard") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + case "Success_copied_into_clipboard": Text("SIP address copied into clipboard") .multilineTextAlignment(.center) From 12f98293311cf94dceb58b2b7a5ef80c353c336e Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 12 Jan 2024 15:19:43 +0100 Subject: [PATCH 07/16] Add user agent --- Linphone/Core/CoreContext.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index e71947f38..d21138495 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -92,6 +92,8 @@ final class CoreContext: ObservableObject { self.mCore.callkitEnabled = true self.mCore.pushNotificationEnabled = true + self.mCore.setUserAgent(name: "Linphone iOS 6.0 Beta (\(UIDevice.current.localizedModel)) - Linphone SDK : \(self.coreVersion)", version: "6.0") + self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { self.defaultAccount = self.mCore.defaultAccount From aa18757a48c1a53f06305b61dcf76d6ac00715df Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 12 Jan 2024 15:41:12 +0100 Subject: [PATCH 08/16] Disable useless buttons in call view --- Linphone/UI/Call/CallView.swift | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index 8d06e91b6..c7d34b298 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -152,12 +152,13 @@ struct CallView: View { Image("phone-transfer") .renderingMode(.template) .resizable() - .foregroundStyle(.white) + .foregroundStyle(Color.gray500) .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background(Color.gray600) .cornerRadius(40) + .disabled(true) Text("Transfer") .foregroundStyle(.white) @@ -171,12 +172,13 @@ struct CallView: View { Image("phone-plus") .renderingMode(.template) .resizable() - .foregroundStyle(.white) + .foregroundStyle(Color.gray500) .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background(Color.gray600) .cornerRadius(40) + .disabled(true) Text("New call") .foregroundStyle(.white) @@ -190,12 +192,13 @@ struct CallView: View { Image("phone-list") .renderingMode(.template) .resizable() - .foregroundStyle(.white) + .foregroundStyle(Color.gray500) .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background(Color.gray600) .cornerRadius(40) + .disabled(true) Text("Call list") .foregroundStyle(.white) @@ -209,12 +212,13 @@ struct CallView: View { Image("dialer") .renderingMode(.template) .resizable() - .foregroundStyle(.white) + .foregroundStyle(Color.gray500) .frame(width: 32, height: 32) } .frame(width: 60, height: 60) - .background(Color.gray500) + .background(Color.gray600) .cornerRadius(40) + .disabled(true) Text("Dialer") .foregroundStyle(.white) From aa1c585024284b663afceb709e41288865f7e4d5 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:50:21 +0100 Subject: [PATCH 09/16] Remove mentions of macOS in the xcodeproj --- Linphone.xcodeproj/project.pbxproj | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index c49a03650..e18215bf2 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -835,7 +835,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; DEVELOPMENT_TEAM = Z2V957B3D6; @@ -843,8 +843,9 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; - INFOPLIST_KEY_NSCameraUsageDescription = "Share photos with your friends and customize avatars"; + INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone usage is required for VOIP calls"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -856,15 +857,16 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -879,7 +881,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CODE_SIGN_ENTITLEMENTS = Linphone/Linphone.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; DEVELOPMENT_TEAM = Z2V957B3D6; @@ -887,8 +889,9 @@ ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; - INFOPLIST_KEY_NSCameraUsageDescription = "Share photos with your friends and customize avatars"; + INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; INFOPLIST_KEY_NSContactsUsageDescription = "Make calls with your friends"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Microphone usage is required for VOIP calls"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Share photos with your friends and customize avatars"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -900,15 +903,16 @@ "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; From e66a0802f5a45b21cd4c1ad2e7384f2091bba56f Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:50:34 +0100 Subject: [PATCH 10/16] Update podfile to use 5.4.0-alpha sdk --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index 18951b82a..2e9ccd0fd 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def basic_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '~> 5.3.0-alpha' + pod 'linphone-sdk', '~> 5.4.0-alpha' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end From 4efc28da9ec344de20c591902a7d22e7d4e622b4 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:54:03 +0100 Subject: [PATCH 11/16] Update push provider - use "apns.dev" or "apns" depending on wether we're using a DEBUG or RELEASE build --- Linphone/Core/CoreContext.swift | 18 +++++++++++++++++- .../Viewmodel/AccountLoginViewModel.swift | 7 ++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index d21138495..f69d10386 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -104,10 +104,25 @@ final class CoreContext: ObservableObject { } }) + self.mCoreSuscriptions.insert(self.mCore.publisher?.onGlobalStateChanged?.postOnCoreQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in + if cbVal.state == GlobalState.On { +#if DEBUG + let pushEnvironment = ".dev" +#else + let pushEnvironment = "" +#endif + for account in cbVal.core.accountList where account.params?.pushNotificationConfig?.provider != ("apns" + pushEnvironment) { + let newParams = account.params?.clone() + Log.info("Account \(String(describing: newParams?.identityAddress?.asStringUriOnly())) - updating apple push provider from \(String(describing: newParams?.pushNotificationConfig?.provider)) to apns\(pushEnvironment)") + newParams?.pushNotificationConfig?.provider = "apns" + pushEnvironment + account.params = newParams + } + } + }) + self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true - try? self.mCore.start() // Create a Core listener to listen for the callback we need // In this case, we want to know about the account registration status @@ -186,6 +201,7 @@ final class CoreContext: ObservableObject { self.mCore.iterate() } + try? self.mCore.start() } } diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index 8cf56625f..819931bd3 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -91,7 +91,12 @@ class AccountLoginViewModel: ObservableObject { accountParams.registerEnabled = true accountParams.pushNotificationAllowed = true accountParams.remotePushNotificationAllowed = false - accountParams.pushNotificationConfig?.provider = "apns.dev" +#if DEBUG + let pushEnvironment = ".dev" +#else + let pushEnvironment = "" +#endif + accountParams.pushNotificationConfig?.provider = "apns" + pushEnvironment // Now that our AccountParams is configured, we can create the Account object let account = try core.createAccount(params: accountParams) From 3f4e8d79cfa05c91771b65a19a62b13b9e389b42 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:56:07 +0100 Subject: [PATCH 12/16] Add crashlytics (WIP) --- Linphone/LinphoneApp.swift | 9 +++++++++ Linphone/Utils/Log.swift | 6 ++++++ Podfile | 10 ++++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 9596a76ce..0930db967 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -18,6 +18,9 @@ */ import SwiftUI +#if USE_CRASHLYTICS +import Firebase +#endif @main struct LinphoneApp: App { @@ -32,6 +35,12 @@ struct LinphoneApp: App { @State private var startCallViewModel: StartCallViewModel? @State private var callViewModel: CallViewModel? + init() { +#if USE_CRASHLYTICS + FirebaseApp.configure() +#endif + } + var body: some Scene { WindowGroup { if coreContext.coreIsStarted { diff --git a/Linphone/Utils/Log.swift b/Linphone/Utils/Log.swift index bb4e61cb1..23901a029 100644 --- a/Linphone/Utils/Log.swift +++ b/Linphone/Utils/Log.swift @@ -24,6 +24,9 @@ import UIKit import os import linphonesw import linphone +#if USE_CRASHLYTICS +import Firebase +#endif class Log: LoggingServiceDelegate { @@ -88,6 +91,9 @@ class Log: LoggingServiceDelegate { } else { NSLog(log) } +#if USE_CRASHLYTICS + Crashlytics.crashlytics().log("\(levelStr) [\(domain)] \(message)\n") +#endif } func onLogMessageWritten(logService: linphonesw.LoggingService, domain: String, level: linphonesw.LogLevel, message: String) { diff --git a/Podfile b/Podfile index 2e9ccd0fd..0c727cb84 100644 --- a/Podfile +++ b/Podfile @@ -9,10 +9,16 @@ def basic_pods else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end - + + crashlytics end - +def crashlytics + if not ENV['USE_CRASHLYTICS'].nil? + pod 'Firebase/Analytics' + pod 'Firebase/Crashlytics' + end +end target 'Linphone' do # Comment the next line if you don't want to use dynamic frameworks From 1b498258a31ee7c6cb4b5bcdaa6f391ec746fde1 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:56:48 +0100 Subject: [PATCH 13/16] Crahslytics - add GoogleService-Info.plist --- GoogleService-Info.plist | 36 ++++++++++++++++++++++++++++++ Linphone.xcodeproj/project.pbxproj | 12 ++++++++++ 2 files changed, 48 insertions(+) create mode 100644 GoogleService-Info.plist diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist new file mode 100644 index 000000000..f996be8f2 --- /dev/null +++ b/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 221368768663-0ufgu96cel0auk4v0me863lgm252b9n2.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.221368768663-0ufgu96cel0auk4v0me863lgm252b9n2 + API_KEY + AIzaSyDJTtlRCM7IqdVUU2dSIYq2YIsTz6bqnkI + GCM_SENDER_ID + 221368768663 + PLIST_VERSION + 1 + BUNDLE_ID + org.linphone.phone + PROJECT_ID + linphone-iphone + STORAGE_BUCKET + linphone-iphone.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:221368768663:ios:a2c822bc087b5a219431d2 + DATABASE_URL + https://linphone-iphone.firebaseio.com + + diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index e18215bf2..b4c229e18 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 660D8A712B517D260092694D /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 660D8A702B517D260092694D /* GoogleService-Info.plist */; }; 662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69D82B25DE18007118BF /* TelecomManager.swift */; }; 662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */; }; 66C491F92B24D25B00CEA16D /* ConfigExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */; }; @@ -97,6 +98,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 660D8A702B517D260092694D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 662B69D82B25DE18007118BF /* TelecomManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelecomManager.swift; sourceTree = ""; }; 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigExtension.swift; sourceTree = ""; }; @@ -249,6 +251,7 @@ D719ABAA2ABC67BF00B41C10 = { isa = PBXGroup; children = ( + 660D8A702B517D260092694D /* GoogleService-Info.plist */, D719ABB52ABC67BF00B41C10 /* Linphone */, D719ABB42ABC67BF00B41C10 /* Products */, A31AF2AB8C6A3D7B7EA3B424 /* Pods */, @@ -600,6 +603,7 @@ D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */, D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */, D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */, + 660D8A712B517D260092694D /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -837,10 +841,15 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 4; DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"Linphone/Preview Content\""; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; @@ -863,6 +872,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -887,6 +897,7 @@ DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "$(inherited)"; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Linphone/Info.plist; INFOPLIST_KEY_NSCameraUsageDescription = "Camera usage is required for video VOIP calls"; @@ -909,6 +920,7 @@ "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.3; MARKETING_VERSION = 6.0; + OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; From 6bf6aa3aa6c2e58d0ce236d4358e9fe776ae00e0 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:57:13 +0100 Subject: [PATCH 14/16] Add encryption parameters to info.plist for upload on appstoreconnect --- Linphone/Info.plist | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 33e2c5a4d..0cc800b37 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -2,12 +2,10 @@ - UIUserInterfaceStyle - Light - NSCameraUsageDescription - Camera usage is required for video VOIP calls - NSMicrophoneUsageDescription - Microphone usage is required for VOIP calls + ITSAppUsesNonExemptEncryption + + ITSEncryptionExportComplianceCode + b5cb085f-772a-4a4f-8c77-5d1332b1f93f UIAppFonts NotoSans-Light.ttf From 2f1bd572b06c9f0f3fa3a5924b49b4d7401739b8 Mon Sep 17 00:00:00 2001 From: QuentinArguillere Date: Fri, 12 Jan 2024 15:58:03 +0100 Subject: [PATCH 15/16] Add USE_CRASHLYTICS flag management to podfile, and fix crashlytics log build --- Linphone/Utils/Log.swift | 2 +- Podfile | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Linphone/Utils/Log.swift b/Linphone/Utils/Log.swift index 23901a029..a2351724c 100644 --- a/Linphone/Utils/Log.swift +++ b/Linphone/Utils/Log.swift @@ -92,7 +92,7 @@ class Log: LoggingServiceDelegate { NSLog(log) } #if USE_CRASHLYTICS - Crashlytics.crashlytics().log("\(levelStr) [\(domain)] \(message)\n") + Crashlytics.crashlytics().log(log) #endif } diff --git a/Podfile b/Podfile index 0c727cb84..696a25e90 100644 --- a/Podfile +++ b/Podfile @@ -31,9 +31,35 @@ target 'Linphone' do end post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' - end - end -end \ No newline at end of file + app_project = Xcodeproj::Project.open(Dir.glob("*.xcodeproj")[0]) + app_project.native_targets.each do |target| + target.build_configurations.each do |config| + if target.name == "Linphone" || target.name == 'msgNotificationService' || target.name == 'msgNotificationContent' + if ENV['USE_CRASHLYTICS'].nil? + if config.name == "Debug" then + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) DEBUG=1' + else + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited)' + end + config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)' + else + # activate crashlytics + if config.name == "Debug" then + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) DEBUG=1 USE_CRASHLYTICS=1' + else + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) USE_CRASHLYTICS=1' + end + config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -DUSE_CRASHLYTICS' + end + end + + app_project.save + end + end + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' + end + end +end + From 937444c5d00f1d463d89aa7c9f1127c4a5fb4ecb Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 12 Jan 2024 16:01:04 +0100 Subject: [PATCH 16/16] Fix video display in call view --- Linphone/UI/Call/CallView.swift | 14 ++++++++------ Linphone/UI/Call/ViewModel/CallViewModel.swift | 3 --- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift index c7d34b298..709a814df 100644 --- a/Linphone/UI/Call/CallView.swift +++ b/Linphone/UI/Call/CallView.swift @@ -82,7 +82,7 @@ struct CallView: View { Button { callViewModel.toggleVideo() } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") + Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash") .renderingMode(.template) .resizable() .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) @@ -500,7 +500,7 @@ struct CallView: View { Spacer() - if callViewModel.cameraDisplayed { + if telecomManager.remoteVideo { Button { callViewModel.switchCamera() } label: { @@ -594,10 +594,12 @@ struct CallView: View { .scaledToFill() .clipped() .onTapGesture { - fullscreenVideo.toggle() + if telecomManager.remoteVideo { + fullscreenVideo.toggle() + } } - if callViewModel.cameraDisplayed { + if telecomManager.remoteVideo { HStack { Spacer() VStack { @@ -741,7 +743,7 @@ struct CallView: View { Button { callViewModel.toggleVideo() } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") + Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash") .renderingMode(.template) .resizable() .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) @@ -825,7 +827,7 @@ struct CallView: View { Button { callViewModel.toggleVideo() } label: { - Image(callViewModel.cameraDisplayed ? "video-camera" : "video-camera-slash") + Image(telecomManager.remoteVideo ? "video-camera" : "video-camera-slash") .renderingMode(.template) .resizable() .foregroundStyle((callViewModel.isPaused || telecomManager.isPausedByRemote) ? Color.gray500 : .white) diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift index 26f9dd2ed..b6abd4aa5 100644 --- a/Linphone/UI/Call/ViewModel/CallViewModel.swift +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -75,7 +75,6 @@ class CallViewModel: ObservableObject { //self.avatarModel = ??? self.micMutted = self.currentCall!.microphoneMuted - self.cameraDisplayed = self.currentCall!.cameraEnabled == true self.isRecording = self.currentCall!.params!.isRecording self.isPaused = self.isCallPaused() self.timeElapsed = 0 @@ -139,8 +138,6 @@ class CallViewModel: ObservableObject { "[CallViewModel] Updating call with video enabled set to \(params.videoEnabled)" ) try self.currentCall!.update(params: params) - - self.cameraDisplayed = self.currentCall!.cameraEnabled == true } catch { }