diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 24027cc55..eaffc2479 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ 66FBFC492B83BD2400BC6AB1 /* ConfigExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */; }; 66FBFC4A2B83BD3300BC6AB1 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FE2B24D4AC00CEA16D /* FileUtils.swift */; }; 66FBFC4B2B83BD7B00BC6AB1 /* CoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FA2B24D32600CEA16D /* CoreExtension.swift */; }; + C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AD2C09F23C002E77BF /* URLExtension.swift */; }; + C67586B02C09F247002E77BF /* URIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AF2C09F247002E77BF /* URIHandler.swift */; }; D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */; }; D70959F12B8DF3EC0014AC0B /* ConversationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */; }; D70A26EE2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */; }; @@ -192,6 +194,8 @@ 66E56BCB2BA9A1E0006CE56F /* MeetingsListItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingsListItemModel.swift; sourceTree = ""; }; 66E56BCD2BA9A1F8006CE56F /* MeetingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingModel.swift; sourceTree = ""; }; 66F626B12BCEBB86003E2DEC /* AddParticipantsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsFragment.swift; sourceTree = ""; }; + C67586AD2C09F23C002E77BF /* URLExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; + C67586AF2C09F247002E77BF /* URIHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URIHandler.swift; sourceTree = ""; }; D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = ""; }; D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = ""; }; D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = ""; }; @@ -343,6 +347,7 @@ 66C491F72B24D25A00CEA16D /* Extensions */ = { isa = PBXGroup; children = ( + C67586AD2C09F23C002E77BF /* URLExtension.swift */, D717071D2AC5922E0037746F /* ColorExtension.swift */, 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */, D76005F52B0798B00054B79A /* IntExtension.swift */, @@ -426,6 +431,7 @@ D732A9082AFD235500DB42BA /* ShareSheetController.swift */, D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */, D7173EBD2B7A5C0A00BCC481 /* LinphoneUtils.swift */, + C67586AF2C09F247002E77BF /* URIHandler.swift */, ); path = Utils; sourceTree = ""; @@ -993,6 +999,7 @@ D717630D2BD7BD0E00464097 /* ParticipantsListFragment.swift in Sources */, D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, + C67586B02C09F247002E77BF /* URIHandler.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, @@ -1019,6 +1026,7 @@ D78E06302BEA6A4A00CE3783 /* ChangeLayoutBottomSheet.swift in Sources */, 66E56BC92BA4A6D7006CE56F /* MeetingsListViewModel.swift in Sources */, D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */, + C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */, 6613A0B02BAEB7F4008923A4 /* MeetingsListFragment.swift in Sources */, D76005F62B0798B00054B79A /* IntExtension.swift in Sources */, D78E06282BE3811D00CE3783 /* CallStatsModel.swift in Sources */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 2bc9c37ca..a5ecf668e 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -48,7 +48,10 @@ final class CoreContext: ObservableObject { let monitor = NWPathMonitor() private var mCorePushIncomingDelegate: CoreDelegate! - + private var actionsToPerformOnCoreQueueWhenCoreIsStarted : [((Core)->Void)] = [] + private var callStateCallBacks : [((Call.State)->Void)] = [] + private var configuringStateCallBacks : [((ConfiguringState)->Void)] = [] + private init() { do { try initialiseCore() @@ -141,6 +144,8 @@ final class CoreContext: ObservableObject { account.params = newParams } + self.actionsToPerformOnCoreQueueWhenCoreIsStarted.forEach {$0(cbVal.core)} + self.actionsToPerformOnCoreQueueWhenCoreIsStarted.removeAll() } }) @@ -291,6 +296,24 @@ final class CoreContext: ObservableObject { func crashForCrashlytics() { fatalError("Crashing app to test crashlytics") } + + func performActionOnCoreQueueWhenCoreIsStarted(action: @escaping (_ core: Core)->Void ) { + if (coreIsStarted) { + CoreContext.shared.doOnCoreQueue { core in + action(core) + } + } else { + actionsToPerformOnCoreQueueWhenCoreIsStarted.append(action) + } + } + + func addCoreDelegateStub(delegate: CoreDelegateStub) { + mCore.addDelegate(delegate: delegate) + } + func removeCoreDelegateStub(delegate: CoreDelegateStub) { + mCore.removeDelegate(delegate: delegate) + } + } // swiftlint:enable large_tuple diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 1639aabd3..c683a3dcf 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -2,6 +2,89 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + linphone-config + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + sip + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + sips + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + sip-linphone + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + sips-linphone + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + linphone-sip + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + linphone-sips + + + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + tel + + + ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 2082758b3..bd735d195 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -86,7 +86,12 @@ struct LinphoneApp: App { WindowGroup { if coreContext.coreIsStarted { if !sharedMainViewModel.welcomeViewDisplayed { - WelcomeView() + ZStack { + WelcomeView() + + ToastView() + .zIndex(3) + } } else if !coreContext.hasDefaultAccount || sharedMainViewModel.displayProfileMode { ZStack { AssistantView() @@ -119,9 +124,13 @@ struct LinphoneApp: App { conversationViewModel: conversationViewModel!, meetingsListViewModel: meetingsListViewModel!, scheduleMeetingViewModel: scheduleMeetingViewModel! - ) + ).onOpenURL { url in + URIHandler.handleURL(url: url) + } } else { - SplashScreen() + SplashScreen().onOpenURL { url in + URIHandler.handleURL(url: url) + } } } else { SplashScreen() @@ -137,6 +146,8 @@ struct LinphoneApp: App { conversationViewModel = ConversationViewModel() meetingsListViewModel = MeetingsListViewModel() scheduleMeetingViewModel = ScheduleMeetingViewModel() + }.onOpenURL { url in + URIHandler.handleURL(url: url) } } }.onChange(of: scenePhase) { newPhase in diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 129e33904..0890a892c 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -242,6 +242,9 @@ }, "Bluetooth" : { + }, + "Call failed" : { + }, "Call has been successfully transferred" : { @@ -284,6 +287,12 @@ }, "Conditions de service" : { + }, + "Configuration failed" : { + + }, + "Configuration successfully applied" : { + }, "Connexion à la réunion" : { @@ -777,6 +786,12 @@ }, "UDP" : { + }, + "Unable to call, invalid address" : { + + }, + "Unable to retrieve configuration, invalid address" : { + }, "Une application de communication **sécurisée**, **open source** et **française**." : { diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index e42c834c3..b68c15604 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -136,7 +136,42 @@ struct ToastView: View { .foregroundStyle(Color.redDanger500) .default_text_style(styleSize: 15) .padding(8) + + case "Failed_uri_handler_call_failed": + Text("Call failed") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + case "Failed_uri_handler_config_failed": + Text("Configuration failed") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case "uri_handler_config_success": + Text("Configuration successfully applied") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Failed_uri_handler_bad_call_address": + Text("Unable to call, invalid address") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Failed_uri_handler_bad_config_address": + Text("Unable to retrieve configuration, invalid address") + .multilineTextAlignment(.center) + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 15) + .padding(8) + default: Text("Error") .multilineTextAlignment(.center) diff --git a/Linphone/Utils/Extensions/URLExtension.swift b/Linphone/Utils/Extensions/URLExtension.swift new file mode 100644 index 000000000..10fb96e93 --- /dev/null +++ b/Linphone/Utils/Extensions/URLExtension.swift @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation + +extension URL { + func withNewScheme(_ value: String) -> URL? { + let components = NSURLComponents.init(url: self, resolvingAgainstBaseURL: true) + components?.scheme = value + return components?.url + } + var resourceSpecifier: String { + get { + let nrl : NSURL = self as NSURL + return nrl.resourceSpecifier ?? self.absoluteString + } + } +} diff --git a/Linphone/Utils/LinphoneUtils.swift b/Linphone/Utils/LinphoneUtils.swift index ded14ef87..9b6a4e065 100644 --- a/Linphone/Utils/LinphoneUtils.swift +++ b/Linphone/Utils/LinphoneUtils.swift @@ -59,4 +59,9 @@ class LinphoneUtils: NSObject { public class func getChatRoomId(localSipUri: String, remoteSipUri: String) -> String { return "\(localSipUri)#~#\(remoteSipUri)" } + + public class func applyInternationalPrefix(core: Core, account: Account? = nil) -> Bool { + return account?.params?.useInternationalPrefixForCallsAndChats == true || core.defaultAccount?.params?.useInternationalPrefixForCallsAndChats == true + } + } diff --git a/Linphone/Utils/URIHandler.swift b/Linphone/Utils/URIHandler.swift new file mode 100644 index 000000000..8643c7101 --- /dev/null +++ b/Linphone/Utils/URIHandler.swift @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import Foundation +import linphonesw +import Combine + +class URIHandler { + + // Need to cover all Info.plist URL schemes. + private static let callSchemes = ["sip", "sip-linphone", "linphone-sip", "tel"] + private static let secureCallSchemes = ["sips", "sips-linphone", "linphone-sips"] + private static let configurationSchemes = ["linphone-config"] + + private static var uriHandlerCoreDelegate: CoreDelegateStub? = nil + + static func addCoreDelegate() { + uriHandlerCoreDelegate = CoreDelegateStub( + onCallStateChanged: { (core: Core, call: Call, state: Call.State, message: String) in + if state == .Error { + toast("Failed_uri_handler_call_failed") + CoreContext.shared.removeCoreDelegateStub(delegate: uriHandlerCoreDelegate!) + } + if state == .End { + CoreContext.shared.removeCoreDelegateStub(delegate: uriHandlerCoreDelegate!) + } + }, + onConfiguringStatus: { (core:Core, state:ConfiguringState, status: String) in + if state == .Failed { + toast("Failed_uri_handler_config_failed") + CoreContext.shared.removeCoreDelegateStub(delegate: uriHandlerCoreDelegate!) + } + if state == .Successful { + toast("uri_handler_config_success") + CoreContext.shared.removeCoreDelegateStub(delegate: uriHandlerCoreDelegate!) + } + }) + CoreContext.shared.addCoreDelegateStub(delegate: uriHandlerCoreDelegate!) + } + + + static func handleURL(url: URL) { + Log.info("[URIHandler] handleURL: \(url)") + if let scheme = url.scheme { + if secureCallSchemes.contains(scheme) { + initiateCall(url: url, withScheme: "sips") + } else if callSchemes.contains(scheme) { + initiateCall(url: url, withScheme: "sip") + } else if configurationSchemes.contains(scheme) { + initiateConfiguration(url: url) + } else { + Log.error("[URIHandler] unhandled URL \(url) (check Info.plist)") + } + } else { + Log.error("[URIHandler] invalid scheme for URL \(url)") + } + } + + private static func initiateCall(url: URL, withScheme newScheme: String) { + CoreContext.shared.performActionOnCoreQueueWhenCoreIsStarted { core in + if let newSchemeUrl = url.withNewScheme(newScheme), + let address = core.interpretUrl(url: newSchemeUrl.absoluteString, + applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { + Log.info("[URIHandler] initiating call to address : \(address.asString())") + addCoreDelegate() + TelecomManager.shared.doCallWithCore(addr: address, isVideo: false, isConference: false) + } else { + Log.error("[URIHandler] unable to call with \(url.resourceSpecifier)") + toast("Failed_uri_handler_bad_call_address") + } + } + } + + private static func initiateConfiguration(url: URL) { + if autoRemoteProvisioningOnConfigUriHandler() { + CoreContext.shared.performActionOnCoreQueueWhenCoreIsStarted { core in + Log.info("[URIHandler] provisioning app with URI: \(url.resourceSpecifier)") + do { + addCoreDelegate() + core.config?.setString(section: "misc", key: "config-uri", value: url.resourceSpecifier) + try core.setProvisioninguri(newValue: url.resourceSpecifier) + core.stop() + try core.start() + } catch { + Log.error("[URIHandler] unable to configure the app with \(url.resourceSpecifier) \(error)") + toast("Failed_uri_handler_bad_config_address") + } + } + } else { + Log.warn("[URIHandler] received configuration request, but automatic provisioning is disabled.") + } + } + + private static func autoRemoteProvisioningOnConfigUriHandler() -> Bool { + return Config.get().getBool(section: "app", key: "auto_apply_provisioning_config_uri_handler", defaultValue: true) + } + + private static func toast(_ message: String) { + DispatchQueue.main.async { + ToastViewModel.shared.toastMessage = message + ToastViewModel.shared.displayToast = true + } + } +}