diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 901406bbe..ecccf8226 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -68,6 +68,9 @@ D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; }; D7ADF6002AFE356400212231 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADF5FF2AFE356400212231 /* Avatar.swift */; }; D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; }; + D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5678D2B28888F00DE63EB /* CallView.swift */; }; + D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */; }; + D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */; }; D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; }; D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; }; D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */; }; @@ -157,6 +160,9 @@ D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; + D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = ""; }; + D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = ""; }; + D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; }; D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; }; D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = ""; }; @@ -232,6 +238,7 @@ D74C9D002ACB098C0021626A /* PermissionManager.swift */, D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, D732A9082AFD235500DB42BA /* ShareSheetController.swift */, + D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */, ); path = Utils; sourceTree = ""; @@ -286,6 +293,7 @@ isa = PBXGroup; children = ( D719ABCA2ABC761800B41C10 /* Assistant */, + D7B5678C2B28883700DE63EB /* Call */, D719ABC62ABC6F0200B41C10 /* Main */, D7702EF02AC7200600557C00 /* Welcome */, ); @@ -473,6 +481,23 @@ path = Ressources; sourceTree = ""; }; + D7B5678C2B28883700DE63EB /* Call */ = { + isa = PBXGroup; + children = ( + D7B99E972B29B37F00BE7BF2 /* ViewModel */, + D7B5678D2B28888F00DE63EB /* CallView.swift */, + ); + path = Call; + sourceTree = ""; + }; + D7B99E972B29B37F00BE7BF2 /* ViewModel */ = { + isa = PBXGroup; + children = ( + D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; D7D24D0C2AC1B4C700C6F35B /* Fonts */ = { isa = PBXGroup; children = ( @@ -611,6 +636,7 @@ D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */, D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */, D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */, + D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */, D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */, D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */, 662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */, @@ -633,6 +659,7 @@ D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */, 66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, + D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, @@ -660,6 +687,7 @@ D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, 66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */, D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */, + D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index b3982d479..aadf76990 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -313,6 +313,9 @@ }, "I understand" : { + }, + "Incoming call" : { + }, "Incoming Call" : { @@ -352,6 +355,9 @@ }, "Message" : { + }, + "Missed call" : { + }, "My Profile" : { @@ -385,6 +391,9 @@ }, "Other actions" : { + }, + "Outgoing call" : { + }, "Outgoing Call" : { diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift index 348512a04..61443c185 100644 --- a/Linphone/TelecomManager/ProviderDelegate.swift +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -24,6 +24,7 @@ import UIKit import linphonesw import AVFoundation import os +import SwiftUI class CallInfo { var callId: String = "" @@ -207,6 +208,12 @@ extension ProviderDelegate: CXProviderDelegate { let uuid = action.callUUID let callInfo = callInfos[uuid] let callId = callInfo?.callId ?? "" + + DispatchQueue.main.async { + withAnimation { + TelecomManager.shared.callInProgress = true + } + } CoreContext.shared.doOnCoreQueue { core in Log.info("CallKit: answer call with call-id: \(String(describing: callId)) and UUID: \(uuid.description).") @@ -225,7 +232,11 @@ extension ProviderDelegate: CXProviderDelegate { } TelecomManager.shared.callkitAudioSessionActivated = false core.configureAudioSession() - TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false) + + if call != nil { + TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false) + } + action.fulfill() } } diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift index b92ae1eca..db1a80851 100644 --- a/Linphone/TelecomManager/TelecomManager.swift +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -24,6 +24,7 @@ import UserNotifications import os import CallKit import AVFoundation +import SwiftUI class CallAppData: NSObject { var batteryWarningShown = false @@ -32,13 +33,16 @@ class CallAppData: NSObject { } -class TelecomManager { +class TelecomManager: ObservableObject { static let shared = TelecomManager() static var uuidReplacedCall: String? let providerDelegate: ProviderDelegate // to support callkit let callController: CXCallController // to support callkit + @Published var callInProgress: Bool = false + @Published var callStarted: Bool = false + var actionToFulFill: CXCallAction? var callkitAudioSessionActivated: Bool? var nextCallIsTransfer: Bool = false @@ -78,7 +82,17 @@ class TelecomManager { sCall.userData = UnsafeMutableRawPointer(Unmanaged.passRetained(appData!).toOpaque()) } } - + + func doCallWithCore(addr: Address) { + CoreContext.shared.doOnCoreQueue { core in + do { + try self.doCall(core: core, addr: addr, isSas: false, isVideo: false) + } catch { + + } + } + } + func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws { // let displayName = FastAddressBook.displayName(for: addr.getCobject) @@ -135,6 +149,13 @@ class TelecomManager { /* will be used later to notify user if video was not activated because of the linphone core*/ } } + + DispatchQueue.main.async { + self.callStarted = true + withAnimation { + self.callInProgress = true + } + } } } @@ -171,6 +192,10 @@ class TelecomManager { } try call.acceptWithParams(params: callParams) + + DispatchQueue.main.async { + self.callStarted = true + } } catch { Log.error("accept call failed \(error)") } @@ -195,7 +220,18 @@ class TelecomManager { } func incomingDisplayName(call: Call) -> String { - // TODO + if call.remoteAddress != nil { + let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!) + if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { + return friend!.address!.displayName! + } else { + if call.remoteAddress!.displayName != nil { + return call.remoteAddress!.displayName! + } else if call.remoteAddress!.username != nil { + return call.remoteAddress!.username! + } + } + } return "IncomingDisplayName" } @@ -361,6 +397,13 @@ class TelecomManager { } case .End, .Error: + + DispatchQueue.main.async { + withAnimation { + self.callInProgress = false + self.callStarted = false + } + } var displayName = "Unknown" if call.dir == .Incoming { displayName = incomingDisplayName(call: call) diff --git a/Linphone/UI/Call/CallView.swift b/Linphone/UI/Call/CallView.swift new file mode 100644 index 000000000..bc3baf8fa --- /dev/null +++ b/Linphone/UI/Call/CallView.swift @@ -0,0 +1,276 @@ +/* + * 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 . + */ + +import SwiftUI + +struct CallView: View { + + @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var telecomManager = TelecomManager.shared + @ObservedObject private var contactsManager = ContactsManager.shared + + @ObservedObject var callViewModel: CallViewModel + + @State var startDate = Date.now + @State var timeElapsed: Int = 0 + + let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + VStack { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + if callViewModel.direction == .Outgoing { + Image("outgoing-call") + .resizable() + .frame(width: 15, height: 15) + .padding(.horizontal) + + Text("Outgoing call") + .foregroundStyle(.white) + } else { + Image("incoming-call") + .resizable() + .frame(width: 15, height: 15) + .padding(.horizontal) + + Text("Incoming call") + .foregroundStyle(.white) + } + + Spacer() + } + .frame(height: 40) + + 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() + } + + if !telecomManager.callStarted { + VStack { + ActivityIndicator() + .frame(width: 20, height: 20) + .padding(.top, 100) + + Text(counterToMinutes()) + .onReceive(timer) { firedDate in + timeElapsed = Int(firedDate.timeIntervalSince(startDate)) + + } + .padding(.top) + .foregroundStyle(.white) + + Spacer() + } + .background(.clear) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray600) + .cornerRadius(20) + .padding(.horizontal, 4) + + if telecomManager.callStarted { + HStack(spacing: 12) { + Button { + 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("video-camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Button { + } label: { + Image("microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + + Button { + } label: { + Image("speaker-high") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + .background(Color.gray500) + .cornerRadius(40) + } + .padding(.horizontal, 25) + .padding(.top, 20) + } else { + HStack(spacing: 12) { + + Spacer() + + Button { + 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) + + Button { + //telecomManager.callStarted.toggle() + } label: { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.greenSuccess500) + .cornerRadius(40) + + Spacer() + } + .padding(.horizontal, 25) + .padding(.top, 20) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray900) + } + + func terminateCall() { + coreContext.doOnCoreQueue { core in + do { + // Terminates the call, whether it is ringing or running + try core.currentCall?.terminate() + } catch { NSLog(error.localizedDescription) } + } + timer.upstream.connect().cancel() + } + + func counterToMinutes() -> String { + let currentTime = timeElapsed + let seconds = currentTime % 60 + let minutes = String(format: "%02d", Int(currentTime / 60)) + let hours = String(format: "%02d", Int(currentTime / 3600)) + + if Int(currentTime / 3600) > 0 { + return "\(hours):\(minutes):\(seconds < 10 ? "0" : "")\(seconds)" + } else { + return "\(minutes):\(seconds < 10 ? "0" : "")\(seconds)" + } + } +} + +#Preview { + CallView(callViewModel: CallViewModel()) +} diff --git a/Linphone/UI/Call/ViewModel/CallViewModel.swift b/Linphone/UI/Call/ViewModel/CallViewModel.swift new file mode 100644 index 000000000..efb3ab588 --- /dev/null +++ b/Linphone/UI/Call/ViewModel/CallViewModel.swift @@ -0,0 +1,55 @@ +/* + * 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 . + */ + +import linphonesw + +class CallViewModel: ObservableObject { + + var coreContext = CoreContext.shared + var telecomManager = TelecomManager.shared + + @Published var displayName: String = "Example Linphone" + @Published var direction: Call.Dir = .Outgoing + @Published var remoteAddressString: String = "example.linphone@sip.linphone.org" + @Published var remoteAddress: Address? + @Published var avatarModel: ContactAvatarModel? + + init() { + coreContext.doOnCoreQueue { core in + if core.currentCall != nil && core.currentCall!.remoteAddress != nil { + DispatchQueue.main.async { + self.direction = .Incoming + self.remoteAddressString = String(core.currentCall!.remoteAddress!.asStringUriOnly().dropFirst(4)) + self.remoteAddress = core.currentCall!.remoteAddress! + + let friend = ContactsManager.shared.getFriendWithAddress(address: core.currentCall!.remoteAddress!) + if friend != nil && friend!.address != nil && friend!.address!.displayName != nil { + self.displayName = friend!.address!.displayName! + } else { + if core.currentCall!.remoteAddress!.displayName != nil { + self.displayName = core.currentCall!.remoteAddress!.displayName! + } else if core.currentCall!.remoteAddress!.username != nil { + self.displayName = core.currentCall!.remoteAddress!.username! + } + } + } + } + } + } +} diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index 99333cd53..0e256c39e 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -22,6 +22,7 @@ import SwiftUI struct ContactInnerActionsFragment: View { @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject private var telecomManager = TelecomManager.shared @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @@ -62,8 +63,7 @@ struct ContactInnerActionsFragment: View { VStack(spacing: 0) { if contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { ForEach(0.. Void + var body: some View { ForEach(0..