mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Merge branch 'feature/start_call' into 'master'
Start call / Mute / Edit Audio route See merge request BC/private/linphone-iphone-6.0!15
This commit is contained in:
commit
5173b655f2
33 changed files with 2284 additions and 308 deletions
|
|
@ -7,6 +7,8 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
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 */; };
|
||||
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FA2B24D32600CEA16D /* CoreExtension.swift */; };
|
||||
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */; };
|
||||
|
|
@ -66,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 */; };
|
||||
|
|
@ -91,6 +96,8 @@
|
|||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
662B69D82B25DE18007118BF /* TelecomManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelecomManager.swift; sourceTree = "<group>"; };
|
||||
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = "<group>"; };
|
||||
66C491F82B24D25A00CEA16D /* ConfigExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigExtension.swift; sourceTree = "<group>"; };
|
||||
66C491FA2B24D32600CEA16D /* CoreExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreExtension.swift; sourceTree = "<group>"; };
|
||||
66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteUtils.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -153,6 +160,9 @@
|
|||
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
|
||||
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
|
||||
D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
|
||||
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = "<group>"; };
|
||||
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
|
||||
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
|
||||
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
|
||||
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -188,6 +198,15 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
662B69D72B25DDF6007118BF /* TelecomManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
662B69D82B25DE18007118BF /* TelecomManager.swift */,
|
||||
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */,
|
||||
);
|
||||
path = TelecomManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
66C491F72B24D25A00CEA16D /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -219,6 +238,7 @@
|
|||
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
|
||||
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
|
||||
D732A9082AFD235500DB42BA /* ShareSheetController.swift */,
|
||||
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -247,6 +267,7 @@
|
|||
D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */,
|
||||
D777DBB12AE12C4000565A99 /* Contacts */,
|
||||
D719ABC72ABC6FB200B41C10 /* Core */,
|
||||
662B69D72B25DDF6007118BF /* TelecomManager */,
|
||||
D719ABC52ABC6EE800B41C10 /* UI */,
|
||||
D717071C2AC591EF0037746F /* Utils */,
|
||||
D719ABBA2ABC67BF00B41C10 /* Assets.xcassets */,
|
||||
|
|
@ -272,6 +293,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
D719ABCA2ABC761800B41C10 /* Assistant */,
|
||||
D7B5678C2B28883700DE63EB /* Call */,
|
||||
D719ABC62ABC6F0200B41C10 /* Main */,
|
||||
D7702EF02AC7200600557C00 /* Welcome */,
|
||||
);
|
||||
|
|
@ -459,6 +481,23 @@
|
|||
path = Ressources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7B5678C2B28883700DE63EB /* Call */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
|
||||
D7B5678D2B28888F00DE63EB /* CallView.swift */,
|
||||
);
|
||||
path = Call;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7B99E972B29B37F00BE7BF2 /* ViewModel */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7D24D0C2AC1B4C700C6F35B /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -597,8 +636,10 @@
|
|||
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 */,
|
||||
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
|
||||
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */,
|
||||
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
|
||||
|
|
@ -618,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 */,
|
||||
|
|
@ -641,9 +683,11 @@
|
|||
D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */,
|
||||
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */,
|
||||
D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */,
|
||||
662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */,
|
||||
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 */,
|
||||
|
|
|
|||
21
Linphone/Assets.xcassets/notebook.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/notebook.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "notebook.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/notebook.imageset/notebook.svg
vendored
Normal file
1
Linphone/Assets.xcassets/notebook.imageset/notebook.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M184,112a8,8,0,0,1-8,8H112a8,8,0,0,1,0-16h64A8,8,0,0,1,184,112Zm-8,24H112a8,8,0,0,0,0,16h64a8,8,0,0,0,0-16Zm48-88V208a16,16,0,0,1-16,16H48a16,16,0,0,1-16-16V48A16,16,0,0,1,48,32H208A16,16,0,0,1,224,48ZM48,208H72V48H48Zm160,0V48H88V208H208Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 363 B |
21
Linphone/Assets.xcassets/screencast.imageset/Contents.json
vendored
Normal file
21
Linphone/Assets.xcassets/screencast.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "screencast.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Linphone/Assets.xcassets/screencast.imageset/screencast.svg
vendored
Normal file
1
Linphone/Assets.xcassets/screencast.imageset/screencast.svg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M232,56V200a16,16,0,0,1-16,16H144a8,8,0,0,1,0-16h72V56H40V96a8,8,0,0,1-16,0V56A16,16,0,0,1,40,40H216A16,16,0,0,1,232,56ZM32,184a8,8,0,0,0,0,16,8,8,0,0,1,8,8,8,8,0,0,0,16,0A24,24,0,0,0,32,184Zm0-32a8,8,0,0,0,0,16,40,40,0,0,1,40,40,8,8,0,0,0,16,0A56.06,56.06,0,0,0,32,152Zm0-32a8,8,0,0,0,0,16,72.08,72.08,0,0,1,72,72,8,8,0,0,0,16,0A88.1,88.1,0,0,0,32,120Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 477 B |
|
|
@ -47,7 +47,6 @@ final class ContactsManager: ObservableObject {
|
|||
if core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off {
|
||||
print("\(#function) - Core is being stopped or already destroyed, abort")
|
||||
} else {
|
||||
|
||||
do {
|
||||
self.friendList = try core.getFriendListByName(name: self.nativeAddressBookFriendList) ?? core.createFriendList()
|
||||
} catch let error {
|
||||
|
|
@ -81,6 +80,7 @@ final class ContactsManager: ObservableObject {
|
|||
linphoneFriendList.displayName = self.linphoneAddressBookFriendList
|
||||
core.addFriendList(list: linphoneFriendList)
|
||||
}
|
||||
linphoneFriendList.subscriptionsEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ final class ContactsManager: ObservableObject {
|
|||
return imagePath
|
||||
}
|
||||
|
||||
func awaitDataWrite(data: Data, name: String, prefix: String,completion: @escaping ((), String) -> Void) {
|
||||
func awaitDataWrite(data: Data, name: String, prefix: String, completion: @escaping ((), String) -> Void) {
|
||||
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
||||
|
||||
if directory != nil {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@
|
|||
*/
|
||||
|
||||
// swiftlint:disable large_tuple
|
||||
// swiftlint:disable line_length
|
||||
|
||||
import linphonesw
|
||||
import Combine
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class CoreContext: ObservableObject {
|
||||
|
||||
|
|
@ -33,8 +36,9 @@ final class CoreContext: ObservableObject {
|
|||
@Published var coreIsStarted: Bool = false
|
||||
|
||||
private var mCore: Core!
|
||||
private var mIteratePublisher: AnyCancellable?
|
||||
|
||||
private var mIterateSuscription: AnyCancellable?
|
||||
private var mCoreSuscriptions = Set<AnyCancellable?>()
|
||||
|
||||
private init() {
|
||||
do {
|
||||
try initialiseCore()
|
||||
|
|
@ -85,13 +89,10 @@ final class CoreContext: ObservableObject {
|
|||
}
|
||||
|
||||
self.mCore.autoIterateEnabled = false
|
||||
self.mCore.friendsDatabasePath = "\(configDir)/friends.db"
|
||||
self.mCore.callkitEnabled = true
|
||||
self.mCore.pushNotificationEnabled = true
|
||||
|
||||
self.mCore.friendListSubscriptionEnabled = true
|
||||
|
||||
self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in
|
||||
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
|
||||
self.coreIsStarted = true
|
||||
|
|
@ -99,13 +100,13 @@ final class CoreContext: ObservableObject {
|
|||
self.defaultAccount = nil
|
||||
self.coreIsStarted = 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
|
||||
self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in
|
||||
NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n")
|
||||
if cbVal.status == Config.ConfiguringState.Successful {
|
||||
ToastViewModel.shared.toastMessage = "Successful"
|
||||
|
|
@ -114,11 +115,9 @@ final class CoreContext: ObservableObject {
|
|||
ToastViewModel.shared.toastMessage = "Failed"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue {(cbVal:
|
||||
(core: Core, account: Account, state: RegistrationState, message: String)
|
||||
) in
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnMainQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in
|
||||
// If account has been configured correctly, we will go through Progress and Ok states
|
||||
// Otherwise, we will be Failed.
|
||||
NSLog("New registration state is \(cbVal.state) for user id " +
|
||||
|
|
@ -137,7 +136,9 @@ final class CoreContext: ObservableObject {
|
|||
self.loggingInProgress = false
|
||||
self.loggedIn = false
|
||||
}
|
||||
}.postOnCoreQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in
|
||||
})
|
||||
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onAccountRegistrationStateChanged?.postOnCoreQueue { (cbVal: (core: Core, account: Account, state: RegistrationState, message: String)) in
|
||||
// If registration failed, remove account from core
|
||||
if cbVal.state != .Ok && cbVal.state != .Progress {
|
||||
let params = cbVal.account.params
|
||||
|
|
@ -149,9 +150,26 @@ final class CoreContext: ObservableObject {
|
|||
cbVal.core.clearAccounts()
|
||||
cbVal.core.clearAllAuthInfo()
|
||||
}
|
||||
}
|
||||
TelecomManager.shared.onAccountRegistrationStateChanged(core: cbVal.core, account: cbVal.account, state: cbVal.state, message: cbVal.message)
|
||||
})
|
||||
|
||||
self.mIteratePublisher = Timer.publish(every: 0.02, on: .main, in: .common)
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onCallStateChanged?.postOnCoreQueue { (cbVal: (core: Core, call: Call, state: Call.State, message: String)) in
|
||||
TelecomManager.shared.onCallStateChanged(core: cbVal.core, call: cbVal.call, state: cbVal.state, message: cbVal.message)
|
||||
})
|
||||
|
||||
self.mCoreSuscriptions.insert(self.mCore.publisher?.onLogCollectionUploadStateChanged?.postOnMainQueue { (cbValue: (_: Core, _: Core.LogCollectionUploadState, info: String)) in
|
||||
if cbValue.info.starts(with: "https") {
|
||||
UIPasteboard.general.setValue(
|
||||
cbValue.info,
|
||||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
}
|
||||
})
|
||||
|
||||
self.mIterateSuscription = Timer.publish(every: 0.02, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.receive(on: coreQueue)
|
||||
.sink { _ in
|
||||
|
|
@ -187,3 +205,4 @@ final class CoreContext: ObservableObject {
|
|||
}
|
||||
|
||||
// swiftlint:enable large_tuple
|
||||
// swiftlint:enable line_length
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Camera usage is required for video VOIP calls</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Microphone usage is required for VOIP calls</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>NotoSans-Light.ttf</string>
|
||||
|
|
|
|||
|
|
@ -187,9 +187,15 @@
|
|||
},
|
||||
"Block the number" : {
|
||||
|
||||
},
|
||||
"Bluetooth" : {
|
||||
|
||||
},
|
||||
"Call history" : {
|
||||
|
||||
},
|
||||
"Call list" : {
|
||||
|
||||
},
|
||||
"Calls" : {
|
||||
|
||||
|
|
@ -217,6 +223,9 @@
|
|||
},
|
||||
"Contacts" : {
|
||||
|
||||
},
|
||||
"Content" : {
|
||||
|
||||
},
|
||||
"Continue" : {
|
||||
|
||||
|
|
@ -262,6 +271,9 @@
|
|||
},
|
||||
"Display Name" : {
|
||||
|
||||
},
|
||||
"Disposition" : {
|
||||
|
||||
},
|
||||
"Do you really want to delete all calls history?" : {
|
||||
|
||||
|
|
@ -271,6 +283,9 @@
|
|||
},
|
||||
"Don’t save modifications?" : {
|
||||
|
||||
},
|
||||
"Earpiece" : {
|
||||
|
||||
},
|
||||
"Edit" : {
|
||||
|
||||
|
|
@ -301,6 +316,9 @@
|
|||
},
|
||||
"First name*" : {
|
||||
|
||||
},
|
||||
"Headphones" : {
|
||||
|
||||
},
|
||||
"History has been deleted" : {
|
||||
|
||||
|
|
@ -310,6 +328,9 @@
|
|||
},
|
||||
"I understand" : {
|
||||
|
||||
},
|
||||
"Incoming call" : {
|
||||
|
||||
},
|
||||
"Incoming Call" : {
|
||||
|
||||
|
|
@ -349,6 +370,12 @@
|
|||
},
|
||||
"Message" : {
|
||||
|
||||
},
|
||||
"Messages" : {
|
||||
|
||||
},
|
||||
"Missed call" : {
|
||||
|
||||
},
|
||||
"My Profile" : {
|
||||
|
||||
|
|
@ -382,9 +409,15 @@
|
|||
},
|
||||
"Other actions" : {
|
||||
|
||||
},
|
||||
"Outgoing call" : {
|
||||
|
||||
},
|
||||
"Outgoing Call" : {
|
||||
|
||||
},
|
||||
"Participants" : {
|
||||
|
||||
},
|
||||
"password" : {
|
||||
"extractionState" : "manual",
|
||||
|
|
@ -402,6 +435,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pause" : {
|
||||
|
||||
},
|
||||
"Personnalize your profil mode" : {
|
||||
|
||||
|
|
@ -423,6 +459,9 @@
|
|||
},
|
||||
"QR code validated!" : {
|
||||
|
||||
},
|
||||
"Record" : {
|
||||
|
||||
},
|
||||
"Register" : {
|
||||
|
||||
|
|
@ -435,6 +474,9 @@
|
|||
},
|
||||
"Scan QR code" : {
|
||||
|
||||
},
|
||||
"Screen share" : {
|
||||
|
||||
},
|
||||
"Search contact or history call" : {
|
||||
|
||||
|
|
@ -471,6 +513,9 @@
|
|||
},
|
||||
"Skip" : {
|
||||
|
||||
},
|
||||
"Speaker" : {
|
||||
|
||||
},
|
||||
"Start" : {
|
||||
|
||||
|
|
@ -486,6 +531,9 @@
|
|||
},
|
||||
"This contact will be deleted definitively." : {
|
||||
|
||||
},
|
||||
"Title" : {
|
||||
|
||||
},
|
||||
"TLS" : {
|
||||
|
||||
|
|
|
|||
397
Linphone/TelecomManager/ProviderDelegate.swift
Normal file
397
Linphone/TelecomManager/ProviderDelegate.swift
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// swiftlint:disable line_length
|
||||
|
||||
import Foundation
|
||||
import CallKit
|
||||
import UIKit
|
||||
import linphonesw
|
||||
import AVFoundation
|
||||
import os
|
||||
import SwiftUI
|
||||
|
||||
class CallInfo {
|
||||
var callId: String = ""
|
||||
var toAddr: Address?
|
||||
var isOutgoing = false
|
||||
var sasEnabled = false
|
||||
var connected = false
|
||||
var reason: Reason = Reason.None
|
||||
var displayName: String?
|
||||
var videoEnabled = false
|
||||
var isConference = false
|
||||
|
||||
static func newIncomingCallInfo(callId: String) -> CallInfo {
|
||||
let callInfo = CallInfo()
|
||||
callInfo.callId = callId
|
||||
return callInfo
|
||||
}
|
||||
|
||||
static func newOutgoingCallInfo(addr: Address, isSas: Bool, displayName: String, isVideo: Bool, isConference: Bool) -> CallInfo {
|
||||
let callInfo = CallInfo()
|
||||
callInfo.isOutgoing = true
|
||||
callInfo.sasEnabled = isSas
|
||||
callInfo.toAddr = addr
|
||||
callInfo.displayName = displayName
|
||||
callInfo.videoEnabled = isVideo
|
||||
callInfo.isConference = isConference
|
||||
return callInfo
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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()
|
||||
// providerConfiguration.ringtoneSound = ConfigManager.instance().lpConfigBoolForKey(key: "use_device_ringtone") ? nil : "notes_of_the_optimistic.caf"
|
||||
providerConfiguration.supportsVideo = true
|
||||
providerConfiguration.iconTemplateImageData = UIImage(named: "callkit_logo")?.pngData()
|
||||
providerConfiguration.supportedHandleTypes = [.generic, .phoneNumber, .emailAddress]
|
||||
|
||||
providerConfiguration.maximumCallsPerCallGroup = 10
|
||||
providerConfiguration.maximumCallGroups = 10
|
||||
|
||||
// not show app's calls in tel's history
|
||||
// providerConfiguration.includesCallsInRecents = YES;
|
||||
|
||||
return providerConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
func reportIncomingCall(call: Call?, uuid: UUID, handle: String, hasVideo: Bool, displayName: String) {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: handle)
|
||||
update.hasVideo = hasVideo
|
||||
update.localizedCallerName = displayName
|
||||
|
||||
let callInfo = callInfos[uuid]
|
||||
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
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
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
|
||||
provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error == nil {
|
||||
if TelecomManager.shared.endCallkit {
|
||||
CoreContext.shared.doOnCoreQueue(synchronous: true) { core in
|
||||
let call = core.getCallByCallid(callId: callId)
|
||||
if call?.state == .PushIncomingReceived {
|
||||
try? call?.terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.error("CallKit: cannot complete incoming call with call-id: [\(callId)] and UUID: [\(uuid.description)] from [\(handle)] caused by [\(error!.localizedDescription)]")
|
||||
let code = (error as NSError?)?.code
|
||||
switch code {
|
||||
case CXErrorCodeIncomingCallError.filteredByDoNotDisturb.rawValue:
|
||||
callInfo?.reason = Reason.Busy // This answer is only for this device. Using Reason.DoNotDisturb will make all other end point stop ringing.
|
||||
case CXErrorCodeIncomingCallError.filteredByBlockList.rawValue:
|
||||
callInfo?.reason = Reason.DoNotDisturb
|
||||
default:
|
||||
callInfo?.reason = Reason.Unknown
|
||||
}
|
||||
self.callInfos.updateValue(callInfo!, forKey: uuid)
|
||||
CoreContext.shared.doOnCoreQueue(synchronous: true) { _ in
|
||||
try? call?.decline(reason: callInfo!.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateCall(uuid: UUID, handle: String, hasVideo: Bool = false, displayName: String) {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: handle)
|
||||
update.localizedCallerName = displayName
|
||||
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)
|
||||
}
|
||||
|
||||
func endCall(uuid: UUID) {
|
||||
provider.reportCall(with: uuid, endedAt: .init(), reason: .failed)
|
||||
}
|
||||
|
||||
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
|
||||
let callId = TelecomManager.shared.providerDelegate.callInfos[uuid]?.callId
|
||||
if callId == nil {
|
||||
// callkit already ended
|
||||
return
|
||||
}
|
||||
if core.getCallByCallid(callId: callId ?? "") == nil {
|
||||
Log.info("CallKit: terminate call with call-id: \(String(describing: callId)) and UUID: \(uuid) which does not exist.")
|
||||
self.endCall(uuid: uuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CXProviderDelegate
|
||||
extension ProviderDelegate: CXProviderDelegate {
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
|
||||
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!)
|
||||
}
|
||||
callInfos.removeValue(forKey: uuid)
|
||||
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
if let call = core.getCallByCallid(callId: callId ?? "") {
|
||||
TelecomManager.shared.terminateCall(call: call)
|
||||
Log.info("CallKit: Call ended with call-id: \(String(describing: callId)) an UUID: \(uuid.description).")
|
||||
}
|
||||
action.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
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).")
|
||||
|
||||
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
|
||||
}
|
||||
} else {
|
||||
call?.cameraEnabled = false // Disable camera while app is not on foreground
|
||||
}
|
||||
}
|
||||
TelecomManager.shared.callkitAudioSessionActivated = false
|
||||
core.configureAudioSession()
|
||||
|
||||
if call != nil {
|
||||
TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false)
|
||||
}
|
||||
|
||||
action.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
||||
let uuid = action.callUUID
|
||||
let callId = callInfos[uuid]?.callId ?? ""
|
||||
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
let call = core.getCallByCallid(callId: callId)
|
||||
|
||||
if call == nil {
|
||||
Log.error("CXSetHeldCallAction: no call !")
|
||||
action.fail()
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if call?.conference != nil && action.isOnHold {
|
||||
_ = call?.conference?.leave()
|
||||
Log.info("CallKit: call-id: [\(callId)] leaving conference")
|
||||
NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self)
|
||||
action.fulfill()
|
||||
} else {
|
||||
let state = action.isOnHold ? "Paused" : "Resumed"
|
||||
Log.info("CallKit: Call with call-id: [\(callId)] and UUID: [\(uuid)] paused status changed to: [\(state)]")
|
||||
if action.isOnHold {
|
||||
TelecomManager.shared.speakerBeforePause = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(core: core, call: call)
|
||||
try call!.pause()
|
||||
// fullfill() the action now to indicate to Callkit that this call is no longer active, even if the
|
||||
// SIP transaction is not completed yet. At this stage, the media streams are off.
|
||||
// If callkit is not aware that the pause action is completed, it will terminate this call if we
|
||||
// attempt to resume another one.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Log.error("CallKit: Call set held (paused or resumed) \(uuid) failed because \(error)")
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
let uuid = action.callUUID
|
||||
let callInfo = callInfos[uuid]
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = action.handle
|
||||
update.localizedCallerName = callInfo?.displayName
|
||||
self.provider.reportCall(with: action.callUUID, updated: update)
|
||||
|
||||
let addr = callInfo?.toAddr
|
||||
if addr == nil {
|
||||
Log.info("CallKit: can not call a null address!")
|
||||
action.fail()
|
||||
} else {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
do {
|
||||
core.configureAudioSession()
|
||||
try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: callInfo?.videoEnabled ?? false, isConference: callInfo?.isConference ?? false)
|
||||
action.fulfill()
|
||||
} catch {
|
||||
Log.info("CallKit: Call started failed because \(error)")
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)).")
|
||||
TelecomManager.shared.addAllToLocalConference(core: core)
|
||||
action.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
let uuid = action.callUUID
|
||||
let callId = callInfos[uuid]?.callId
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
Log.info( "CallKit: Call muted with call-id: \(String(describing: callId)) an UUID: \(uuid.description).")
|
||||
core.micEnabled = !core.micEnabled
|
||||
action.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
|
||||
let uuid = action.callUUID
|
||||
let callId = callInfos[uuid]?.callId ?? ""
|
||||
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
Log.info("CallKit: Call send dtmf with call-id: \(callId) an UUID: \(uuid.description).")
|
||||
if let call = core.getCallByCallid(callId: callId) {
|
||||
let digit = (action.digits.cString(using: String.Encoding.utf8)?[0])!
|
||||
do {
|
||||
try call.sendDtmf(dtmf: digit)
|
||||
} catch {
|
||||
Log.error("CallKit: Call send dtmf \(uuid) failed because \(error)")
|
||||
}
|
||||
}
|
||||
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.")
|
||||
core.activateAudioSession(actived: true)
|
||||
TelecomManager.shared.callkitAudioSessionActivated = true
|
||||
}
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
Log.info("CallKit: audio session deactivated.")
|
||||
core.activateAudioSession(actived: false)
|
||||
TelecomManager.shared.callkitAudioSessionActivated = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable line_length
|
||||
549
Linphone/TelecomManager/TelecomManager.swift
Normal file
549
Linphone/TelecomManager/TelecomManager.swift
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// swiftlint:disable cyclomatic_complexity
|
||||
|
||||
import Foundation
|
||||
import linphonesw
|
||||
import UserNotifications
|
||||
import os
|
||||
import CallKit
|
||||
import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
class CallAppData: NSObject {
|
||||
var batteryWarningShown = false
|
||||
var videoRequested = false /*set when user has requested for video*/
|
||||
var isConference = false
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
var speakerBeforePause: Bool = false
|
||||
var endCallkit: Bool = false
|
||||
var endCallKitReplacedCall: Bool = true
|
||||
|
||||
var backgroundContextCall: Call?
|
||||
var backgroundContextCameraIsEnabled: Bool = false
|
||||
|
||||
var referedFromCall: String?
|
||||
var referedToCall: String?
|
||||
var actionsToPerformOnceWhenCoreIsOn: [(() -> Void)] = []
|
||||
|
||||
private init() {
|
||||
providerDelegate = ProviderDelegate()
|
||||
callController = CXCallController()
|
||||
}
|
||||
|
||||
func addAllToLocalConference(core: Core) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
static func getAppData(sCall: Call) -> CallAppData? {
|
||||
if sCall.userData == nil {
|
||||
return nil
|
||||
}
|
||||
return Unmanaged<CallAppData>.fromOpaque(sCall.userData!).takeUnretainedValue()
|
||||
}
|
||||
static func setAppData(sCall: Call, appData: CallAppData?) {
|
||||
if sCall.userData != nil {
|
||||
Unmanaged<CallAppData>.fromOpaque(sCall.userData!).release()
|
||||
}
|
||||
if appData == nil {
|
||||
sCall.userData = nil
|
||||
} else {
|
||||
sCall.userData = UnsafeMutableRawPointer(Unmanaged.passRetained(appData!).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
func startCallCallKit(core: Core, addr: Address?, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
|
||||
if addr == nil {
|
||||
Log.info("Can not start a call with null address!")
|
||||
return
|
||||
}
|
||||
|
||||
if TelecomManager.callKitEnabled(core: core) {// && !nextCallIsTransfer != true {
|
||||
let uuid = UUID()
|
||||
let name = "outgoingTODO" // FastAddressBook.displayName(for: addr) ?? "unknow"
|
||||
let handle = CXHandle(type: .generic, value: addr?.asStringUriOnly() ?? "")
|
||||
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
|
||||
let transaction = CXTransaction(action: startCallAction)
|
||||
|
||||
let callInfo = CallInfo.newOutgoingCallInfo(addr: addr!, isSas: isSas, displayName: name, isVideo: isVideo, isConference: isConference)
|
||||
providerDelegate.callInfos.updateValue(callInfo, forKey: uuid)
|
||||
providerDelegate.uuids.updateValue(uuid, forKey: "")
|
||||
|
||||
// setHeldOtherCalls(core: core, exceptCallid: "")
|
||||
requestTransaction(transaction, action: "startCall")
|
||||
} else {
|
||||
try doCall(core: core, addr: addr!, isSas: isSas, isVideo: isVideo, isConference: isConference)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(core: Core, addr: String, isSas: Bool = false, isVideo: Bool, isConference: Bool = false) {
|
||||
do {
|
||||
let address = try Factory.Instance.createAddress(addr: addr)
|
||||
try startCallCallKit(core: core, addr: address, isSas: isSas, isVideo: isVideo, isConference: isConference)
|
||||
} catch {
|
||||
Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr) \(error) ")
|
||||
}
|
||||
}
|
||||
|
||||
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) ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 nextCallIsTransfer {
|
||||
let call = core.currentCall
|
||||
try call?.transferTo(referTo: addr)
|
||||
nextCallIsTransfer = false
|
||||
} else {
|
||||
// We set the record file name here because we can't do it after the call is started.
|
||||
// let writablePath = AppManager.recordingFilePathFromCall(address: addr.username! )
|
||||
// Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)")
|
||||
// lcallParams.recordFile = writablePath
|
||||
if isSas {
|
||||
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
|
||||
}*/
|
||||
} else {
|
||||
lcallParams.videoEnabled = isVideo
|
||||
}
|
||||
|
||||
if let call = core.inviteAddressWithParams(addr: addr, params: lcallParams) {
|
||||
// The LinphoneCallAppData object should be set on call creation with callback
|
||||
// - (void)onCall:StateChanged:withMessage:. If not, we are in big trouble and expect it to crash
|
||||
// We are NOT responsible for creating the AppData.
|
||||
if let data = TelecomManager.getAppData(sCall: call) {
|
||||
data.isConference = isConference
|
||||
data.videoRequested = lcallParams.videoEnabled
|
||||
TelecomManager.setAppData(sCall: call, appData: data)
|
||||
} else {
|
||||
Log.error("New call instanciated but app data was not set. Expect it to crash.")
|
||||
/* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func acceptCall(core: Core, call: Call, hasVideo: Bool) {
|
||||
do {
|
||||
let callParams = try core.createCallParams(call: call)
|
||||
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
|
||||
}*/
|
||||
|
||||
// We set the record file name here because we can't do it after the call is started.
|
||||
// let address = call.callLog?.fromAddress
|
||||
// let writablePath = AppManager.recordingFilePathFromCall(address: address?.username ?? "")
|
||||
// Log.directLog(BCTBX_LOG_MESSAGE, text: "Record file path: \(String(describing: writablePath))")
|
||||
// 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 call.callLog?.wasConference() == true {
|
||||
// Prevent incoming group call to start in audio only layout
|
||||
// Do the same as the conference waiting room
|
||||
callParams.videoEnabled = true
|
||||
callParams.videoDirection = core.videoActivationPolicy?.automaticallyInitiate == true ? .SendRecv : .RecvOnly
|
||||
Log.info("[Context] Enabling video on call params to prevent audio-only layout when answering")
|
||||
}
|
||||
|
||||
try call.acceptWithParams(params: callParams)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.callStarted = true
|
||||
}
|
||||
} catch {
|
||||
Log.error("accept call failed \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func terminateCall(call: Call) {
|
||||
do {
|
||||
try call.terminate()
|
||||
Log.info("Call terminated")
|
||||
} catch {
|
||||
Log.error("Failed to terminate call failed because \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func displayIncomingCall(call: Call?, handle: String, hasVideo: Bool, callId: String, displayName: String) {
|
||||
let uuid = UUID()
|
||||
let callInfo = CallInfo.newIncomingCallInfo(callId: callId)
|
||||
|
||||
providerDelegate.callInfos.updateValue(callInfo, forKey: uuid)
|
||||
providerDelegate.uuids.updateValue(uuid, forKey: callId)
|
||||
providerDelegate.reportIncomingCall(call: call, uuid: uuid, handle: handle, hasVideo: hasVideo, displayName: displayName)
|
||||
}
|
||||
|
||||
func incomingDisplayName(call: Call) -> String {
|
||||
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"
|
||||
}
|
||||
|
||||
static func callKitEnabled(core: Core) -> Bool {
|
||||
#if !targetEnvironment(simulator)
|
||||
return core.callkitEnabled
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
|
||||
func requestTransaction(_ transaction: CXTransaction, action: String) {
|
||||
callController.request(transaction) { error in
|
||||
if let error = error {
|
||||
Log.error("CallKit: Requested transaction \(action) failed because: \(error)")
|
||||
} else {
|
||||
Log.info("CallKit: Requested transaction \(action) successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState, message: String) {
|
||||
if core.accountList.count == 1 && (state == .Failed || state == .Cleared) {
|
||||
// terminate callkit immediately when registration failed or cleared, supporting single account configuration
|
||||
for call in providerDelegate.uuids {
|
||||
let callId = providerDelegate.callInfos[call.value]?.callId
|
||||
if callId != nil {
|
||||
let call = core.getCallByCallid(callId: callId!)
|
||||
if call != nil && call?.state != .PushIncomingReceived {
|
||||
// sometimes (for example) due to network, registration failed, in this case, keep the call
|
||||
continue
|
||||
}
|
||||
}
|
||||
providerDelegate.endCall(uuid: call.value)
|
||||
}
|
||||
endCallkit = true
|
||||
} else {
|
||||
endCallkit = false
|
||||
}
|
||||
}
|
||||
|
||||
func onCallStateChanged(core: Core, call: Call, state cstate: Call.State, message: String) {
|
||||
let callLog = call.callLog
|
||||
let callId = callLog?.callId ?? ""
|
||||
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)
|
||||
|
||||
if call.userData == nil {
|
||||
let appData = CallAppData()
|
||||
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)
|
||||
}
|
||||
*/
|
||||
switch cstate {
|
||||
case .IncomingReceived:
|
||||
let addr = call.remoteAddress
|
||||
let displayName = incomingDisplayName(call: call)
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
DispatchQueue.main.async {
|
||||
withAnimation {
|
||||
TelecomManager.shared.callInProgress = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if call.replacedCall != nil {
|
||||
endCallKitReplacedCall = false
|
||||
|
||||
let uuid = providerDelegate.uuids["\(TelecomManager.uuidReplacedCall ?? "")"]
|
||||
let callInfo = providerDelegate.callInfos[uuid!]
|
||||
callInfo!.callId = referedToCall ?? ""
|
||||
if callInfo != nil && uuid != nil && addr != nil {
|
||||
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)
|
||||
}
|
||||
} 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 uuid = providerDelegate.uuids["\(callId)"]
|
||||
if call.replacedCall == nil {
|
||||
TelecomManager.uuidReplacedCall = callId
|
||||
}
|
||||
|
||||
if uuid != nil {
|
||||
// Tha app is now registered, updated the call already existed.
|
||||
providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName)
|
||||
} else {
|
||||
displayIncomingCall(call: call, handle: addr!.asStringUriOnly(), hasVideo: video, 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)
|
||||
} */
|
||||
case .StreamsRunning:
|
||||
if TelecomManager.callKitEnabled(core: core) {
|
||||
let uuid = providerDelegate.uuids["\(callId)"]
|
||||
if uuid != nil {
|
||||
let callInfo = providerDelegate.callInfos[uuid!]
|
||||
if callInfo != nil && callInfo!.isOutgoing && !callInfo!.connected {
|
||||
Log.info("CallKit: outgoing call connected with uuid \(uuid!) and callId \(callId)")
|
||||
providerDelegate.reportOutgoingCallConnected(uuid: uuid!)
|
||||
callInfo!.connected = true
|
||||
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if speakerBeforePause {
|
||||
speakerBeforePause = false
|
||||
AudioRouteUtils.routeAudioToSpeaker(core: core)
|
||||
}
|
||||
*/
|
||||
|
||||
actionToFulFill?.fulfill()
|
||||
actionToFulFill = nil
|
||||
case .Paused:
|
||||
actionToFulFill?.fulfill()
|
||||
actionToFulFill = nil
|
||||
case .OutgoingInit,
|
||||
.OutgoingProgress,
|
||||
.OutgoingRinging,
|
||||
.OutgoingEarlyMedia:
|
||||
if TelecomManager.callKitEnabled(core: core) {
|
||||
let uuid = providerDelegate.uuids[""]
|
||||
if uuid != nil && callId.isEmpty {
|
||||
let callInfo = providerDelegate.callInfos[uuid!]
|
||||
callInfo!.callId = callId
|
||||
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
|
||||
providerDelegate.uuids.removeValue(forKey: "")
|
||||
providerDelegate.uuids.updateValue(uuid!, forKey: callId)
|
||||
|
||||
Log.info("CallKit: outgoing call started connecting with uuid \(uuid!) and callId \(callId)")
|
||||
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) */
|
||||
} else {
|
||||
referedToCall = callId
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
// disable this because I don't find anygood reason for it: _bluetoothAvailable = FALSE;
|
||||
// furthermore it introduces a bug when calling multiple times since route may not be
|
||||
// reconfigured between cause leading to bluetooth being disabled while it should not
|
||||
// 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)
|
||||
|
||||
// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if TelecomManager.callKitEnabled(core: core) {
|
||||
var uuid = providerDelegate.uuids["\(callId)"]
|
||||
if callId == referedToCall {
|
||||
// refered call ended before connecting
|
||||
Log.info("Callkit: end refered to call: \(String(describing: referedToCall))")
|
||||
referedFromCall = nil
|
||||
referedToCall = nil
|
||||
}
|
||||
if uuid == nil {
|
||||
// the call not yet connected
|
||||
uuid = providerDelegate.uuids[""]
|
||||
}
|
||||
if uuid != nil {
|
||||
if callId == referedFromCall {
|
||||
Log.info("Callkit: end refered from call: \(String(describing: referedFromCall))")
|
||||
referedFromCall = nil
|
||||
let callInfo = providerDelegate.callInfos[uuid!]
|
||||
callInfo!.callId = referedToCall ?? ""
|
||||
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
|
||||
providerDelegate.uuids.removeValue(forKey: callId)
|
||||
providerDelegate.uuids.updateValue(uuid!, forKey: callInfo!.callId)
|
||||
referedToCall = nil
|
||||
break
|
||||
}
|
||||
if endCallKitReplacedCall {
|
||||
let transaction = CXTransaction(action: CXEndCallAction(call: uuid!))
|
||||
requestTransaction(transaction, action: "endCall")
|
||||
} else {
|
||||
endCallKitReplacedCall = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
case .Released:
|
||||
TelecomManager.setAppData(sCall: call, appData: nil)
|
||||
case .Referred:
|
||||
referedFromCall = call.callLog?.callId
|
||||
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: [
|
||||
AnyHashable("call"): NSValue.init(pointer: UnsafeRawPointer(call.getCobject)),
|
||||
AnyHashable("state"): NSNumber(value: cstate.rawValue),
|
||||
AnyHashable("message"): message
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable cyclomatic_complexity
|
||||
660
Linphone/UI/Call/CallView.swift
Normal file
660
Linphone/UI/Call/CallView.swift
Normal file
|
|
@ -0,0 +1,660 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
import SwiftUI
|
||||
import CallKit
|
||||
import AVFAudio
|
||||
|
||||
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
|
||||
|
||||
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
|
||||
@State private var orientation = UIDevice.current.orientation
|
||||
|
||||
let pub = NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
|
||||
|
||||
@State var startDate = Date.now
|
||||
@State var audioRouteSheet: Bool = false
|
||||
@State var hideButtonsSheet: Bool = false
|
||||
@State var options: Int = 1
|
||||
|
||||
@State var imageAudioRoute: String = ""
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
if #available(iOS 16.4, *) {
|
||||
innerView(geoHeight: geo.size.height)
|
||||
.sheet(isPresented: .constant(telecomManager.callStarted && !hideButtonsSheet && idiom != .pad && !(orientation == .landscapeLeft || orientation == .landscapeRight
|
||||
|| UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height))) {
|
||||
GeometryReader { _ in
|
||||
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("video-camera")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color.gray500)
|
||||
.cornerRadius(40)
|
||||
|
||||
Button {
|
||||
callViewModel.muteCall()
|
||||
} 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) { (output) in
|
||||
self.getAudioRouteImage()
|
||||
}
|
||||
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color.gray500)
|
||||
.cornerRadius(40)
|
||||
}
|
||||
.frame(height: geo.size.height * 0.15)
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
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 {
|
||||
} 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("record-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.background(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)
|
||||
.presentationBackground(.black)
|
||||
.presentationDetents([.fraction(0.1), .medium])
|
||||
.interactiveDismissDisabled()
|
||||
.presentationBackgroundInteraction(.enabled)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $audioRouteSheet, onDismiss: {
|
||||
audioRouteSheet = false
|
||||
hideButtonsSheet = false
|
||||
}) {
|
||||
VStack(spacing: 0) {
|
||||
Button(action: {
|
||||
options = 1
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
||||
if callViewModel.isHeadPhoneAvailable() {
|
||||
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Receiver") }).first)
|
||||
} else {
|
||||
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.first)
|
||||
}
|
||||
} catch _ {
|
||||
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(options == 1 ? "radio-button-fill" : "radio-button")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
|
||||
Text(!callViewModel.isHeadPhoneAvailable() ? "Earpiece" : "Headphones")
|
||||
.default_text_style_white(styleSize: 15)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(!callViewModel.isHeadPhoneAvailable() ? "ear" : "headset")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
}
|
||||
})
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
Button(action: {
|
||||
options = 2
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
|
||||
} catch _ {
|
||||
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(options == 2 ? "radio-button-fill" : "radio-button")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
|
||||
Text("Speaker")
|
||||
.default_text_style_white(styleSize: 15)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("speaker-high")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
}
|
||||
})
|
||||
.frame(maxHeight: .infinity)
|
||||
|
||||
Button(action: {
|
||||
options = 3
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
|
||||
try AVAudioSession.sharedInstance().setPreferredInput(AVAudioSession.sharedInstance().availableInputs?.filter({ $0.portType.rawValue.contains("Bluetooth") }).first)
|
||||
} catch _ {
|
||||
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(options == 3 ? "radio-button-fill" : "radio-button")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
|
||||
Text("Bluetooth")
|
||||
.default_text_style_white(styleSize: 15)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("bluetooth")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 25, height: 25, alignment: .leading)
|
||||
}
|
||||
})
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.presentationBackground(Color.gray600)
|
||||
.presentationDetents([.fraction(0.3)])
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func innerView(geoHeight: CGFloat) -> 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(callViewModel.counterToMinutes())
|
||||
.onReceive(callViewModel.timer) { firedDate in
|
||||
callViewModel.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 {
|
||||
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)
|
||||
}
|
||||
.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 {
|
||||
} 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 {
|
||||
callViewModel.muteCall()
|
||||
} 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 {
|
||||
} label: {
|
||||
Image("speaker-high")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
}
|
||||
.frame(width: 60, height: 60)
|
||||
.background(Color.gray500)
|
||||
.cornerRadius(40)
|
||||
}
|
||||
.frame(height: geoHeight * 0.15)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
} else {
|
||||
HStack(spacing: 12) {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
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)
|
||||
|
||||
Button {
|
||||
callViewModel.acceptCall()
|
||||
} label: {
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 32, height: 32)
|
||||
|
||||
}
|
||||
.frame(width: 90, height: 60)
|
||||
.background(Color.greenSuccess500)
|
||||
.cornerRadius(40)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 60)
|
||||
}
|
||||
.padding(.horizontal, 25)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.gray900)
|
||||
}
|
||||
|
||||
func getAudioRouteImage() {
|
||||
imageAudioRoute = AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty
|
||||
? (
|
||||
AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty
|
||||
? (
|
||||
callViewModel.isHeadPhoneAvailable()
|
||||
? "headset"
|
||||
: "speaker-slash"
|
||||
)
|
||||
: "bluetooth"
|
||||
)
|
||||
: "speaker-high"
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CallView(callViewModel: CallViewModel())
|
||||
}
|
||||
// swiftlint:enable type_body_length
|
||||
144
Linphone/UI/Call/ViewModel/CallViewModel.swift
Normal file
144
Linphone/UI/Call/ViewModel/CallViewModel.swift
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import SwiftUI
|
||||
import linphonesw
|
||||
import AVFAudio
|
||||
|
||||
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?
|
||||
@Published var audioSessionImage: String = ""
|
||||
@State var micMutted: Bool = false
|
||||
@State var timeElapsed: Int = 0
|
||||
|
||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
init() {
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
} catch _ {
|
||||
|
||||
}
|
||||
|
||||
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!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func terminateCall() {
|
||||
withAnimation {
|
||||
telecomManager.callInProgress = false
|
||||
telecomManager.callStarted = false
|
||||
}
|
||||
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if core.currentCall != nil {
|
||||
self.telecomManager.terminateCall(call: core.currentCall!)
|
||||
}
|
||||
}
|
||||
|
||||
timer.upstream.connect().cancel()
|
||||
}
|
||||
|
||||
func acceptCall() {
|
||||
withAnimation {
|
||||
telecomManager.callInProgress = true
|
||||
telecomManager.callStarted = true
|
||||
}
|
||||
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if core.currentCall != nil {
|
||||
self.telecomManager.acceptCall(core: core, call: core.currentCall!, hasVideo: false)
|
||||
}
|
||||
}
|
||||
|
||||
timer.upstream.connect().cancel()
|
||||
}
|
||||
|
||||
func muteCall() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if core.currentCall != nil {
|
||||
self.micMutted = !self.micMutted
|
||||
core.currentCall!.microphoneMuted = self.micMutted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)"
|
||||
}
|
||||
}
|
||||
|
||||
func isHeadPhoneAvailable() -> Bool {
|
||||
guard let availableInputs = AVAudioSession.sharedInstance().availableInputs else {return false}
|
||||
for inputDevice in availableInputs {
|
||||
if inputDevice.portType == .headsetMic || inputDevice.portType == .headphones {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAudioRoute() -> Int {
|
||||
if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue == "Speaker" }).isEmpty {
|
||||
if AVAudioSession.sharedInstance().currentRoute.outputs.filter({ $0.portType.rawValue.contains("Bluetooth") }).isEmpty {
|
||||
return 1
|
||||
} else {
|
||||
return 3
|
||||
}
|
||||
} else {
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count, id: \.self) { index in
|
||||
Button {
|
||||
} label: {
|
||||
HStack {
|
||||
HStack {
|
||||
VStack {
|
||||
Text("SIP address :")
|
||||
|
|
@ -82,30 +82,22 @@ struct ContactInnerActionsFragment: View {
|
|||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.stringToCopy = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
telecomManager.doCallWithCore(
|
||||
addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index]
|
||||
)
|
||||
}
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.2) {
|
||||
contactViewModel.stringToCopy = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
|
||||
showingSheet.toggle()
|
||||
}
|
||||
|
||||
if !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.isEmpty
|
||||
|| index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count - 1 {
|
||||
|
|
@ -117,8 +109,7 @@ struct ContactInnerActionsFragment: View {
|
|||
}
|
||||
|
||||
ForEach(0..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count, id: \.self) { index in
|
||||
Button {
|
||||
} label: {
|
||||
HStack {
|
||||
HStack {
|
||||
VStack {
|
||||
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label != nil
|
||||
|
|
@ -138,37 +129,16 @@ struct ContactInnerActionsFragment: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.stringToCopy =
|
||||
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(.white)
|
||||
.onLongPressGesture(minimumDuration: 0.2) {
|
||||
contactViewModel.stringToCopy =
|
||||
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
|
||||
showingSheet.toggle()
|
||||
}
|
||||
|
||||
if index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count - 1 {
|
||||
VStack {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ struct ContactInnerFragment: View {
|
|||
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
||||
@ObservedObject var contactAvatarModel: ContactAvatarModel
|
||||
@ObservedObject var contactViewModel: ContactViewModel
|
||||
|
|
@ -150,6 +151,7 @@ struct ContactInnerFragment: View {
|
|||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
telecomManager.doCallWithCore(addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.address!)
|
||||
}, label: {
|
||||
VStack {
|
||||
HStack(alignment: .center) {
|
||||
|
|
@ -158,11 +160,6 @@ struct ContactInnerFragment: View {
|
|||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.grayMain2c200)
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ struct ContactsInnerFragment: View {
|
|||
|
||||
VStack {
|
||||
List {
|
||||
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)}
|
||||
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet, startCallFunc: {addr in })}
|
||||
.listStyle(.plain)
|
||||
.overlay(
|
||||
VStack {
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ struct ContactsListFragment: View {
|
|||
|
||||
@Binding var showingSheet: Bool
|
||||
|
||||
var startCallFunc: (_ addr: Address) -> Void
|
||||
|
||||
var body: some View {
|
||||
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
|
||||
Button {
|
||||
} label: {
|
||||
HStack {
|
||||
HStack {
|
||||
if index == 0
|
||||
|| contactsManager.lastSearch[index].friend?.name!.lowercased().folding(
|
||||
|
|
@ -77,21 +78,19 @@ struct ContactsListFragment: View {
|
|||
.foregroundStyle(Color.orangeMain500)
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
if contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.address != nil {
|
||||
startCallFunc(contactsManager.lastSearch[index].friend!.address!)
|
||||
}
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.2) {
|
||||
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
|
|
@ -99,5 +98,5 @@ struct ContactsListFragment: View {
|
|||
}
|
||||
|
||||
#Preview {
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: {_ in })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ struct FavoriteContactsListFragment: View {
|
|||
HStack {
|
||||
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
|
||||
if contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.starred == true {
|
||||
Button {
|
||||
} label: {
|
||||
VStack {
|
||||
VStack {
|
||||
if contactsManager.lastSearch[index].friend!.photo != nil
|
||||
&& !contactsManager.lastSearch[index].friend!.photo!.isEmpty {
|
||||
|
|
@ -51,21 +50,16 @@ struct FavoriteContactsListFragment: View {
|
|||
.frame( maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
contactViewModel.indexDisplayedFriend = index
|
||||
}
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.2) {
|
||||
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
|
||||
showingSheet.toggle()
|
||||
}
|
||||
.frame(minWidth: 70, maxWidth: 70)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import Foundation
|
||||
import linphonesw
|
||||
import Combine
|
||||
|
||||
class ContactAvatarModel: ObservableObject {
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ class ContactAvatarModel: ObservableObject {
|
|||
|
||||
@Published var presenceStatus: ConsolidatedPresence
|
||||
|
||||
private var friendDelegate: FriendDelegate?
|
||||
private var friendSuscription: AnyCancellable?
|
||||
|
||||
init(friend: Friend?, withPresence: Bool?) {
|
||||
self.friend = friend
|
||||
|
|
@ -51,47 +52,39 @@ class ContactAvatarModel: ObservableObject {
|
|||
self.lastPresenceInfo = ""
|
||||
}
|
||||
|
||||
if self.friendDelegate != nil {
|
||||
self.friend!.removeDelegate(delegate: self.friendDelegate!)
|
||||
self.friendDelegate = nil
|
||||
if self.friendSuscription != nil {
|
||||
self.friendSuscription = nil
|
||||
}
|
||||
|
||||
addDelegate()
|
||||
addSubscription()
|
||||
} else {
|
||||
self.lastPresenceInfo = ""
|
||||
self.presenceStatus = .Offline
|
||||
}
|
||||
}
|
||||
|
||||
func addDelegate() {
|
||||
let newFriendDelegate = FriendDelegateStub(
|
||||
onPresenceReceived: { (linphoneFriend: Friend) -> Void in
|
||||
DispatchQueue.main.sync {
|
||||
self.presenceStatus = linphoneFriend.consolidatedPresence
|
||||
if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.consolidatedPresence == .Busy {
|
||||
if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.presenceModel!.latestActivityTimestamp != -1 {
|
||||
self.lastPresenceInfo = linphoneFriend.consolidatedPresence == .Online ? "Online" : self.getCallTime(startDate: linphoneFriend.presenceModel!.latestActivityTimestamp)
|
||||
} else {
|
||||
self.lastPresenceInfo = "Away"
|
||||
}
|
||||
} else {
|
||||
self.lastPresenceInfo = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
func addSubscription() {
|
||||
|
||||
friendDelegate = newFriendDelegate
|
||||
if friendDelegate != nil {
|
||||
friend!.addDelegate(delegate: friendDelegate!)
|
||||
friendSuscription = self.friend?.publisher?.onPresenceReceived?.postOnMainQueue { (cbValue: (Friend)) in
|
||||
print("publisherpublisher onLogCollectionUploadStateChanged \(cbValue.address?.asStringUriOnly() ?? "")")
|
||||
|
||||
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)
|
||||
} else {
|
||||
self.lastPresenceInfo = "Away"
|
||||
}
|
||||
} else {
|
||||
self.lastPresenceInfo = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllDelegate() {
|
||||
if friendDelegate != nil {
|
||||
func removeAllSuscription() {
|
||||
if friendSuscription != nil {
|
||||
presenceStatus = .Offline
|
||||
friend!.removeDelegate(delegate: friendDelegate!)
|
||||
friendDelegate = nil
|
||||
friendSuscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ struct ContentView: View {
|
|||
|
||||
@ObservedObject private var coreContext = CoreContext.shared
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
var magicSearch = MagicSearchSingleton.shared
|
||||
|
|
@ -445,19 +446,21 @@ struct ContentView: View {
|
|||
})
|
||||
: ContactAvatarModel(friend: nil, withPresence: false)
|
||||
|
||||
HistoryContactFragment(
|
||||
contactAvatarModel: contactAvatarModel!,
|
||||
historyViewModel: historyViewModel,
|
||||
historyListViewModel: historyListViewModel,
|
||||
contactViewModel: contactViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup,
|
||||
isShowEditContactFragment: $isShowEditContactFragment,
|
||||
indexPage: $index
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray100)
|
||||
.ignoresSafeArea(.keyboard)
|
||||
if contactAvatarModel != nil {
|
||||
HistoryContactFragment(
|
||||
contactAvatarModel: contactAvatarModel!,
|
||||
historyViewModel: historyViewModel,
|
||||
historyListViewModel: historyListViewModel,
|
||||
contactViewModel: contactViewModel,
|
||||
editContactViewModel: editContactViewModel,
|
||||
isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup,
|
||||
isShowEditContactFragment: $isShowEditContactFragment,
|
||||
indexPage: $index
|
||||
)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray100)
|
||||
.ignoresSafeArea(.keyboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
|
@ -508,19 +511,56 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
if isShowStartCallFragment {
|
||||
StartCallFragment(
|
||||
startCallViewModel: startCallViewModel,
|
||||
isShowStartCallFragment: $isShowStartCallFragment,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .bottom))
|
||||
.halfSheet(showSheet: $showingDialer) {
|
||||
DialerBottomSheet(
|
||||
|
||||
if #available(iOS 16.4, *) {
|
||||
if idiom != .pad {
|
||||
StartCallFragment(
|
||||
startCallViewModel: startCallViewModel,
|
||||
isShowStartCallFragment: $isShowStartCallFragment,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .bottom))
|
||||
.sheet(isPresented: $showingDialer) {
|
||||
DialerBottomSheet(
|
||||
startCallViewModel: startCallViewModel,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
.presentationDetents([.medium])
|
||||
// .interactiveDismissDisabled()
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
|
||||
}
|
||||
} else {
|
||||
StartCallFragment(
|
||||
startCallViewModel: startCallViewModel,
|
||||
isShowStartCallFragment: $isShowStartCallFragment,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .bottom))
|
||||
.halfSheet(showSheet: $showingDialer) {
|
||||
DialerBottomSheet(
|
||||
startCallViewModel: startCallViewModel,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
} onDismiss: {}
|
||||
}
|
||||
|
||||
} else {
|
||||
StartCallFragment(
|
||||
startCallViewModel: startCallViewModel,
|
||||
isShowStartCallFragment: $isShowStartCallFragment,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
} onDismiss: {}
|
||||
.zIndex(3)
|
||||
.transition(.move(edge: .bottom))
|
||||
.halfSheet(showSheet: $showingDialer) {
|
||||
DialerBottomSheet(
|
||||
startCallViewModel: startCallViewModel,
|
||||
showingDialer: $showingDialer
|
||||
)
|
||||
} onDismiss: {}
|
||||
}
|
||||
}
|
||||
|
||||
if isShowDeleteContactPopup {
|
||||
|
|
@ -620,10 +660,16 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
|
||||
//if sharedMainViewModel.displayToast {
|
||||
if telecomManager.callInProgress {
|
||||
CallView(callViewModel: CallViewModel())
|
||||
.zIndex(3)
|
||||
.transition(.scale.combined(with: .move(edge: .top)))
|
||||
}
|
||||
|
||||
// if sharedMainViewModel.displayToast {
|
||||
ToastView()
|
||||
.zIndex(3)
|
||||
//}
|
||||
// }
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
|
|
@ -649,6 +695,9 @@ struct ContentView: View {
|
|||
coreContext.onForeground()
|
||||
if !isShowStartCallFragment {
|
||||
contactsManager.fetchContacts()
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
||||
historyListViewModel.computeCallLogsList()
|
||||
}
|
||||
}
|
||||
print("Active")
|
||||
} else if newPhase == .inactive {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ struct SideMenu: View {
|
|||
|
||||
@ObservedObject private var coreContext = CoreContext.shared
|
||||
|
||||
@State private var coreDelegate: CoreDelegate?
|
||||
|
||||
let width: CGFloat
|
||||
let isOpen: Bool
|
||||
let menuClose: () -> Void
|
||||
|
|
@ -46,7 +44,7 @@ struct SideMenu: View {
|
|||
HStack {
|
||||
List {
|
||||
Text("My Profile").onTapGesture {
|
||||
print("My Profile")
|
||||
print("My Profile")
|
||||
}
|
||||
Text("Send logs").onTapGesture {
|
||||
sendLogs()
|
||||
|
|
@ -75,37 +73,6 @@ struct SideMenu: View {
|
|||
func sendLogs() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
core.uploadLogCollection()
|
||||
|
||||
let newCoreDelegate = CoreDelegateStub(
|
||||
onLogCollectionUploadStateChanged: { core, logCollectionUploadState, logString in
|
||||
|
||||
if logString.starts(with: "https") {
|
||||
UIPasteboard.general.setValue(
|
||||
logString,
|
||||
forPasteboardType: UTType.plainText.identifier
|
||||
)
|
||||
|
||||
removeAllDelegate()
|
||||
|
||||
ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard"
|
||||
ToastViewModel.shared.displayToast.toggle()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
coreDelegate = newCoreDelegate
|
||||
if coreDelegate != nil {
|
||||
core.addDelegate(delegate: coreDelegate!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllDelegate() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if coreDelegate != nil {
|
||||
core.removeDelegate(delegate: coreDelegate!)
|
||||
coreDelegate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
import linphonesw
|
||||
|
||||
struct DialerBottomSheet: View {
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ struct DialerBottomSheet: View {
|
|||
@ObservedObject private var magicSearch = MagicSearchSingleton.shared
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
||||
@ObservedObject var startCallViewModel: StartCallViewModel
|
||||
|
||||
|
|
@ -270,6 +272,14 @@ struct DialerBottomSheet: View {
|
|||
Spacer()
|
||||
|
||||
Button {
|
||||
if !startCallViewModel.searchField.isEmpty {
|
||||
do {
|
||||
let address = try Factory.Instance.createAddress(addr: String("sip:" + startCallViewModel.searchField + "@" + startCallViewModel.domain))
|
||||
telecomManager.doCallWithCore(addr: address)
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image("phone")
|
||||
.renderingMode(.template)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ struct HistoryContactFragment: View {
|
|||
|
||||
@State private var orientation = UIDevice.current.orientation
|
||||
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
|
||||
|
||||
@ObservedObject var contactAvatarModel: ContactAvatarModel
|
||||
@ObservedObject var historyViewModel: HistoryViewModel
|
||||
|
|
@ -372,6 +373,15 @@ struct HistoryContactFragment: View {
|
|||
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!
|
||||
)
|
||||
}
|
||||
}, label: {
|
||||
VStack {
|
||||
HStack(alignment: .center) {
|
||||
|
|
@ -380,11 +390,6 @@ struct HistoryContactFragment: View {
|
|||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c600)
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color.grayMain2c200)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import linphonesw
|
|||
struct HistoryListFragment: View {
|
||||
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
||||
@ObservedObject var historyListViewModel: HistoryListViewModel
|
||||
@ObservedObject var historyViewModel: HistoryViewModel
|
||||
|
|
@ -33,8 +34,7 @@ struct HistoryListFragment: View {
|
|||
VStack {
|
||||
List {
|
||||
ForEach(0..<historyListViewModel.callLogs.count, id: \.self) { index in
|
||||
Button {
|
||||
} label: {
|
||||
HStack {
|
||||
HStack {
|
||||
let fromAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!)
|
||||
let toAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!)
|
||||
|
|
@ -51,6 +51,11 @@ struct HistoryListFragment: View {
|
|||
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
|
||||
if contactAvatarModel != nil {
|
||||
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45)
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
} else {
|
||||
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
|
||||
|
|
@ -95,7 +100,12 @@ struct HistoryListFragment: View {
|
|||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: 45, height: 45)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
|
|
@ -148,26 +158,38 @@ struct HistoryListFragment: View {
|
|||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.trailing, 5)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
|
||||
telecomManager.doCallWithCore(
|
||||
addr: historyListViewModel.callLogs[index].toAddress!
|
||||
)
|
||||
} else if historyListViewModel.callLogs[index].fromAddress != nil {
|
||||
telecomManager.doCallWithCore(
|
||||
addr: historyListViewModel.callLogs[index].fromAddress!
|
||||
)
|
||||
}
|
||||
historyViewModel.displayedCall = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.listRowInsets(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20))
|
||||
.listRowSeparator(.hidden)
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
historyViewModel.selectedCall = historyListViewModel.callLogs[index]
|
||||
showingSheet.toggle()
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
withAnimation {
|
||||
historyViewModel.displayedCall = historyListViewModel.callLogs[index]
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(.white)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
historyViewModel.displayedCall = historyListViewModel.callLogs[index]
|
||||
}
|
||||
}
|
||||
.onLongPressGesture(minimumDuration: 0.2) {
|
||||
historyViewModel.selectedCall = historyListViewModel.callLogs[index]
|
||||
showingSheet.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ struct StartCallFragment: View {
|
|||
|
||||
@ObservedObject var contactsManager = ContactsManager.shared
|
||||
@ObservedObject var magicSearch = MagicSearchSingleton.shared
|
||||
@ObservedObject private var telecomManager = TelecomManager.shared
|
||||
|
||||
@ObservedObject var startCallViewModel: StartCallViewModel
|
||||
|
||||
|
|
@ -154,7 +155,23 @@ struct StartCallFragment: View {
|
|||
.padding(.horizontal, 16)
|
||||
}
|
||||
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
|
||||
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: { addr in
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
}
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
telecomManager.doCallWithCore(addr: addr)
|
||||
}
|
||||
})
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
|
|
@ -220,20 +237,29 @@ struct StartCallFragment: View {
|
|||
.foregroundStyle(Color.orangeMain500)
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
showingDialer = false
|
||||
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
|
||||
magicSearch.searchForContacts(
|
||||
sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)
|
||||
}
|
||||
|
||||
startCallViewModel.searchField = ""
|
||||
magicSearch.currentFilterSuggestions = ""
|
||||
delayColorDismiss()
|
||||
|
||||
withAnimation {
|
||||
isShowStartCallFragment.toggle()
|
||||
if contactsManager.lastSearchSuggestions[index].address != nil {
|
||||
telecomManager.doCallWithCore(
|
||||
addr: contactsManager.lastSearchSuggestions[index].address!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.simultaneousGesture(
|
||||
LongPressGesture()
|
||||
.onEnded { _ in
|
||||
|
||||
}
|
||||
)
|
||||
.highPriorityGesture(
|
||||
TapGesture()
|
||||
.onEnded { _ in
|
||||
|
||||
}
|
||||
)
|
||||
.buttonStyle(.borderless)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import linphonesw
|
||||
import Combine
|
||||
|
||||
class HistoryListViewModel: ObservableObject {
|
||||
|
||||
|
|
@ -26,12 +27,9 @@ class HistoryListViewModel: ObservableObject {
|
|||
@Published var callLogs: [CallLog] = []
|
||||
var callLogsTmp: [CallLog] = []
|
||||
|
||||
@Published private var coreDelegate: CoreDelegate?
|
||||
|
||||
var callLogsAddressToDelete = ""
|
||||
|
||||
var callLogSubscription: AnyCancellable?
|
||||
init() {
|
||||
removeAllDelegate()
|
||||
computeCallLogsList()
|
||||
}
|
||||
|
||||
|
|
@ -50,25 +48,19 @@ class HistoryListViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.coreDelegate = CoreDelegateStub(
|
||||
onCallLogUpdated: { (_: Core, _: CallLog) -> Void in
|
||||
DispatchQueue.main.sync {
|
||||
let account = core.defaultAccount
|
||||
let logs = account != nil ? account!.callLogs : core.callLogs
|
||||
|
||||
self.callLogs.removeAll()
|
||||
self.callLogsTmp.removeAll()
|
||||
|
||||
logs.forEach { log in
|
||||
self.callLogs.append(log)
|
||||
self.callLogsTmp.append(log)
|
||||
}
|
||||
}
|
||||
self.callLogSubscription = core.publisher?.onCallLogUpdated?.postOnCoreQueue { (_: (_: Core, _: CallLog)) in
|
||||
print("publisherpublisher onCallLogUpdated")
|
||||
let account = core.defaultAccount
|
||||
let logs = account != nil ? account!.callLogs : core.callLogs
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.callLogs.removeAll()
|
||||
self.callLogsTmp.removeAll()
|
||||
|
||||
logs.forEach { log in
|
||||
self.callLogs.append(log)
|
||||
self.callLogsTmp.append(log)
|
||||
}
|
||||
)
|
||||
if self.coreDelegate != nil {
|
||||
core.addDelegate(delegate: self.coreDelegate!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -209,14 +201,4 @@ class HistoryListViewModel: ObservableObject {
|
|||
let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId})
|
||||
self.callLogsTmp.remove(at: indexTmp!)
|
||||
}
|
||||
|
||||
func removeAllDelegate() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
if self.coreDelegate != nil {
|
||||
core.removeDelegate(delegate: self.coreDelegate!)
|
||||
self.coreDelegate = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ import linphonesw
|
|||
class StartCallViewModel: ObservableObject {
|
||||
|
||||
@Published var searchField: String = ""
|
||||
|
||||
var domain: String = ""
|
||||
|
||||
init() {}
|
||||
init() {
|
||||
CoreContext.shared.doOnCoreQueue { core in
|
||||
self.domain = core.defaultAccount?.params?.domain ?? ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
Linphone/Utils/ActivityIndicator.swift
Normal file
33
Linphone/Utils/ActivityIndicator.swift
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// ActivityIndicator.swift
|
||||
// Linphone
|
||||
//
|
||||
// Created by Martins Benoît on 13/12/2023.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ActivityIndicator: View {
|
||||
|
||||
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
|
||||
@State var animate = false
|
||||
let color1 = Color.white
|
||||
let color2 = Color.white.opacity(0.5)
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(
|
||||
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
|
||||
.rotationEffect(Angle(degrees: animate ? 360: 0))
|
||||
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
|
||||
}.onAppear {
|
||||
self.animate.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ActivityIndicator()
|
||||
}
|
||||
|
|
@ -23,44 +23,59 @@ import linphonesw
|
|||
struct Avatar: View {
|
||||
|
||||
@ObservedObject var contactAvatarModel: ContactAvatarModel
|
||||
|
||||
let avatarSize: CGFloat
|
||||
let hidePresence: Bool
|
||||
|
||||
init(contactAvatarModel: ContactAvatarModel, avatarSize: CGFloat, hidePresence: Bool = false) {
|
||||
self.contactAvatarModel = contactAvatarModel
|
||||
self.avatarSize = avatarSize
|
||||
self.hidePresence = hidePresence
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
case .success(let image):
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
if contactAvatarModel.friend != nil {
|
||||
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in
|
||||
switch image {
|
||||
case .empty:
|
||||
ProgressView()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
case .success(let image):
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
HStack {
|
||||
Spacer()
|
||||
if contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy {
|
||||
Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy")
|
||||
.resizable()
|
||||
.frame(width: avatarSize/4, height: avatarSize/4)
|
||||
.padding(.trailing, avatarSize == 45 ? 1 : 3)
|
||||
.padding(.bottom, avatarSize == 45 ? 1 : 3)
|
||||
VStack {
|
||||
Spacer()
|
||||
if !hidePresence && (contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy) {
|
||||
Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy")
|
||||
.resizable()
|
||||
.frame(width: avatarSize/4, height: avatarSize/4)
|
||||
.padding(.trailing, avatarSize == 45 ? 1 : 3)
|
||||
.padding(.bottom, avatarSize == 45 ? 1 : 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
}
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
case .failure:
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
case .failure:
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
@unknown default:
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
Image("profil-picture-default")
|
||||
.resizable()
|
||||
.frame(width: avatarSize, height: avatarSize)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
import Foundation
|
||||
import linphonesw
|
||||
|
||||
|
|
@ -26,10 +24,10 @@ import linphonesw
|
|||
|
||||
extension Config {
|
||||
|
||||
private static var _instance : Config?
|
||||
private static var _instance: Config?
|
||||
|
||||
public func getDouble(section:String, key:String, defaultValue:Double) -> Double {
|
||||
if (self.hasEntry(section: section, key: key) != 1) {
|
||||
public func getDouble(section: String, key: String, defaultValue: Double) -> Double {
|
||||
if self.hasEntry(section: section, key: key) != 1 {
|
||||
return defaultValue
|
||||
}
|
||||
let stringValue = self.getString(section: section, key: key, defaultString: "")
|
||||
|
|
@ -51,7 +49,7 @@ extension Config {
|
|||
static let appGroupName = "group.org.linphone.phone.logs"
|
||||
// Needs to be the same name in App Group (capabilities in ALL targets - app & extensions - content + service), can't be stored in the Config itself the Config needs this value to get created
|
||||
static let teamID = Config.get().getString(section: "app", key: "team_id", defaultString: "")
|
||||
static let earlymediaContentExtensionCagetoryIdentifier = Config.get().getString(section: "app", key: "extension_category", defaultString: "")
|
||||
static let earlymediaContentExtCatIdentifier = Config.get().getString(section: "app", key: "extension_category", defaultString: "")
|
||||
|
||||
// Default values in app
|
||||
static let serveraddress = Config.get().getString(section: "app", key: "server", defaultString: "")
|
||||
|
|
|
|||
|
|
@ -84,19 +84,18 @@ class Log: LoggingServiceDelegate {
|
|||
private func output(_ message: String, _ level: Int, _ domain: String = Bundle.main.bundleIdentifier!) {
|
||||
let log = "[\(domain)][\(levelToStrings[level] ?? "Unkown")] \(message)\n"
|
||||
if #available(iOS 10.0, *) {
|
||||
os_log("%{public}@", type: levelToOSleLogLevel[level] ?? .info,log)
|
||||
os_log("%{public}@", type: levelToOSleLogLevel[level] ?? .info, log)
|
||||
} else {
|
||||
NSLog(log)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func onLogMessageWritten(logService: linphonesw.LoggingService, domain: String, level: linphonesw.LogLevel, message: String) {
|
||||
output(message, level.rawValue, domain)
|
||||
}
|
||||
|
||||
public class func stackTrace() {
|
||||
Thread.callStackSymbols.forEach{ print($0) }
|
||||
Thread.callStackSymbols.forEach { print($0) }
|
||||
}
|
||||
|
||||
// Debug
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import linphonesw
|
||||
import Combine
|
||||
|
||||
final class MagicSearchSingleton: ObservableObject {
|
||||
|
||||
|
|
@ -40,6 +41,8 @@ final class MagicSearchSingleton: ObservableObject {
|
|||
@Published var allContact = false
|
||||
private var domainDefaultAccount = ""
|
||||
|
||||
var searchSubscription: AnyCancellable?
|
||||
|
||||
private init() {
|
||||
coreContext.doOnCoreQueue { core in
|
||||
self.domainDefaultAccount = core.defaultAccount?.params?.domain ?? ""
|
||||
|
|
@ -47,7 +50,7 @@ final class MagicSearchSingleton: ObservableObject {
|
|||
self.magicSearch = try? core.createMagicSearch()
|
||||
self.magicSearch.limitedSearch = false
|
||||
|
||||
self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in
|
||||
self.searchSubscription = self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in
|
||||
self.needUpdateLastSearchContacts = true
|
||||
|
||||
var lastSearchFriend: [SearchResult] = []
|
||||
|
|
@ -72,7 +75,7 @@ final class MagicSearchSingleton: ObservableObject {
|
|||
})
|
||||
|
||||
self.contactsManager.avatarListModel.forEach { contactAvatarModel in
|
||||
contactAvatarModel.removeAllDelegate()
|
||||
contactAvatarModel.removeAllSuscription()
|
||||
}
|
||||
|
||||
self.contactsManager.avatarListModel.removeAll()
|
||||
|
|
|
|||
|
|
@ -31,8 +31,7 @@ class PermissionManager: ObservableObject {
|
|||
|
||||
private init() {}
|
||||
|
||||
|
||||
func getPermissions(){
|
||||
func getPermissions() {
|
||||
photoLibraryRequestPermission()
|
||||
cameraRequestPermission()
|
||||
contactsRequestPermission()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue