URI Handlers

This commit is contained in:
Christophe Deschamps 2024-05-31 12:38:58 +00:00
parent 1e16dbaa61
commit bd29389a40
9 changed files with 338 additions and 4 deletions

View file

@ -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 = "<group>"; };
66E56BCD2BA9A1F8006CE56F /* MeetingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingModel.swift; sourceTree = "<group>"; };
66F626B12BCEBB86003E2DEC /* AddParticipantsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddParticipantsFragment.swift; sourceTree = "<group>"; };
C67586AD2C09F23C002E77BF /* URLExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = "<group>"; };
C67586AF2C09F247002E77BF /* URIHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URIHandler.swift; sourceTree = "<group>"; };
D706BA812ADD72D100278F45 /* DeviceRotationViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceRotationViewModifier.swift; sourceTree = "<group>"; };
D70959F02B8DF3EC0014AC0B /* ConversationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationModel.swift; sourceTree = "<group>"; };
D70A26ED2B7CF60B006CC8FC /* ConversationsListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListBottomSheet.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View file

@ -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

View file

@ -2,6 +2,89 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>linphone-config</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sip</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sips</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sip-linphone</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>sips-linphone</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>linphone-sip</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>linphone-sips</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>org.linphone.phone</string>
<key>CFBundleURLSchemes</key>
<array>
<string>tel</string>
</array>
</dict>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<true/>
<key>ITSEncryptionExportComplianceCode</key>

View file

@ -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

View file

@ -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**." : {

View file

@ -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)

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}

View file

@ -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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}