diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift
index 2d62f70b3..fae6c0a92 100644
--- a/Linphone/Core/CoreContext.swift
+++ b/Linphone/Core/CoreContext.swift
@@ -65,7 +65,60 @@ class CoreContext: ObservableObject {
do {
try initialiseCore()
} catch {
-
+
+ }
+ observeMDMNotifications()
+ }
+
+ // MARK: - MDM notifications
+
+ private func observeMDMNotifications() {
+ NotificationCenter.default.addObserver(self, selector: #selector(onMDMConfigurationApplied(_:)), name: MDMManager.configurationAppliedNotification, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(onMDMConfigurationRemoved), name: MDMManager.configurationRemovedNotification, object: nil)
+ }
+
+ @objc private func onMDMConfigurationApplied(_ notification: Notification) {
+ Log.info("[CoreContext] MDM configuration applied, refreshing configuration")
+ CoreContext.shared.performActionOnCoreQueueWhenCoreIsStarted { core in
+ self.handleConfigurationChanged(status: .Successful)
+ }
+ }
+
+ @objc private func onMDMConfigurationRemoved() {
+ doOnCoreQueue { core in
+ Log.info("[CoreContext] MDM configuration removed, re-initializing app to default state")
+ var startCore = false
+ if core.globalState == .On {
+ core.stop()
+ startCore = true
+ }
+ AppServices.resetConfig()
+ self.mCore.config?.reload()
+ if startCore {
+ try?core.start()
+ }
+ self.handleConfigurationChanged(status: .Successful)
+ }
+ }
+
+ /// Shared handler for configuration changes (both from core provisioning and MDM).
+ private func handleConfigurationChanged(status: ConfiguringState) {
+ let themeMainColor = AppServices.corePreferences.themeMainColor
+ SharedMainViewModel.shared.updateConfigChanges()
+ if status == .Successful {
+ var accountModels: [AccountModel] = []
+ for account in self.mCore.accountList {
+ accountModels.append(AccountModel(account: account, core: self.mCore))
+ }
+ DispatchQueue.main.async {
+ self.accounts = accountModels
+ if accountModels.isEmpty {
+ self.loggingInProgress = false
+ self.loggedIn = false
+ }
+ ThemeManager.shared.applyTheme(named: themeMainColor)
+ self.reloadID = UUID()
+ }
}
}
@@ -143,8 +196,14 @@ class CoreContext: ObservableObject {
Log.info("Found existing linphonerc file, skip copying of linphonerc-default configuration")
}
}
-
+
+ MDMManager.shared.loadXMLConfigFromMdm(config: AppServices.config)
+
self.mCore = try? Factory.Instance.createSharedCoreWithConfig(config: AppServices.config, systemContext: Unmanaged.passUnretained(coreQueue).toOpaque(), appGroupId: SharedMainViewModel.appGroupName, mainCore: true)
+
+ MDMManager.shared.applyMdmConfigToCore(core: self.mCore)
+ self.startObservingMDMConfigurationUpdates()
+
self.mCore.callkitEnabled = true
self.mCore.pushNotificationEnabled = true
@@ -348,19 +407,7 @@ class CoreContext: ObservableObject {
}
}, onConfiguringStatus: { (_: Core, status: ConfiguringState, message: String) in
Log.info("New configuration state is \(status) = \(message)\n")
- let themeMainColor = AppServices.corePreferences.themeMainColor
- SharedMainViewModel.shared.updateConfigChanges()
- DispatchQueue.main.async {
- if status == ConfiguringState.Successful {
- var accountModels: [AccountModel] = []
- for account in self.mCore.accountList {
- accountModels.append(AccountModel(account: account, core: self.mCore))
- }
- self.accounts = accountModels
- ThemeManager.shared.applyTheme(named: themeMainColor)
- self.reloadID = UUID()
- }
- }
+ self.handleConfigurationChanged(status: status)
}, onLogCollectionUploadStateChanged: { (_: Core, _: Core.LogCollectionUploadState, info: String) in
if info.starts(with: "https") {
DispatchQueue.main.async {
@@ -563,6 +610,24 @@ class CoreContext: ObservableObject {
}
}
}
+
+ func startObservingMDMConfigurationUpdates() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(mdmConfigDidChange),
+ name: UserDefaults.didChangeNotification,
+ object: nil
+ )
+ }
+
+ @objc private func mdmConfigDidChange() {
+ guard MDMManager.shared.managedConfigChangedSinceLastCheck() else { return }
+ CoreContext.shared.doOnCoreQueue { core in
+ MDMManager.shared.applyMdmConfigToCore(core: core)
+ }
+ }
+
+
}
enum AppServices {
@@ -589,8 +654,30 @@ enum AppServices {
}
return config
}
+
+ static func resetConfig() {
+ if let rcDir = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: SharedMainViewModel.appGroupName)?
+ .appendingPathComponent("Library/Preferences/linphone") {
+ let rcFileUrl = rcDir.appendingPathComponent("linphonerc")
+ do {
+ try FileManager.default.createDirectory(at: rcDir, withIntermediateDirectories: true)
+ if let pathToDefaultConfig = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) {
+ if FileManager.default.fileExists(atPath: rcFileUrl.path) {
+ try FileManager.default.removeItem(at: rcFileUrl)
+ }
+ try FileManager.default.copyItem(at: URL(fileURLWithPath: pathToDefaultConfig), to: rcFileUrl)
+ Log.info("Successfully copied linphonerc-default configuration")
+ _config = nil
+ let _ = configIfAvailable
+ corePreferences = CorePreferences(config: config)
+ }
+ } catch let error {
+ Log.error("Failed to copy default linphonerc file: \(error.localizedDescription)")
+ }
+ }
+ }
- static let corePreferences = CorePreferences(config: config)
+ static var corePreferences = CorePreferences(config: config)
}
// swiftlint:enable line_length
diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift
index 4dfdb137c..5314de6ac 100644
--- a/Linphone/LinphoneApp.swift
+++ b/Linphone/LinphoneApp.swift
@@ -63,7 +63,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Set up notifications
UNUserNotificationCenter.current().delegate = self
-
+
return true
}
@@ -159,6 +159,9 @@ struct LinphoneApp: App {
private let voipRegistry = PKPushRegistry(queue: coreQueue)
init() {
+#if DEBUG
+ LinphoneApp.applyUITestMDMConfigIfNeeded()
+#endif
if !configAvailable {
voipRegistry.delegate = earlyPushDelegate
voipRegistry.desiredPushTypes = [.voIP]
@@ -168,6 +171,26 @@ struct LinphoneApp: App {
}
}
+#if DEBUG
+ /// UI-test hook: reads `UITEST_MDM_CONFIG` (JSON) or `UITEST_MDM_CONFIG_CLEAR=1` from
+ /// the launch environment and writes it to the managed config key before MDMManager runs.
+ /// Only active in DEBUG builds.
+ private static func applyUITestMDMConfigIfNeeded() {
+ let env = ProcessInfo.processInfo.environment
+ let key = "com.apple.configuration.managed"
+ if env["UITEST_MDM_CONFIG_CLEAR"] == "1" {
+ UserDefaults.standard.removeObject(forKey: key)
+ return
+ }
+ guard let json = env["UITEST_MDM_CONFIG"],
+ let data = json.data(using: .utf8),
+ let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return
+ }
+ UserDefaults.standard.set(dict, forKey: key)
+ }
+#endif
+
var body: some Scene {
WindowGroup {
if configAvailable {
diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift
index ee57021e3..40f495214 100644
--- a/Linphone/SplashScreen.swift
+++ b/Linphone/SplashScreen.swift
@@ -35,7 +35,8 @@ struct SplashScreen: View {
.foregroundColor(ThemeManager.shared.currentTheme.main500)
if showSpinner {
- ProgressView()
+ PopupLoadingView()
+ .background(.black.opacity(0.65))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift
index fe29dea76..a29fbf809 100644
--- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift
+++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift
@@ -180,6 +180,7 @@ struct PermissionsFragment: View {
)
.frame(maxWidth: SharedMainViewModel.shared.maxWidth)
.padding(.horizontal)
+ .accessibilityIdentifier("permissions_skip_button")
Button {
permissionManager.getPermissions()
diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift
index a9feaf894..a9b04e7e0 100644
--- a/Linphone/UI/Main/ContentView.swift
+++ b/Linphone/UI/Main/ContentView.swift
@@ -370,7 +370,7 @@ struct ContentView: View {
Button(action: {
resetFilter()
-
+
sharedMainViewModel.changeIndexView(indexViewInt: 1)
sharedMainViewModel.displayedFriend = nil
sharedMainViewModel.displayedConversation = nil
@@ -395,6 +395,7 @@ struct ContentView: View {
}
})
.padding(.top)
+ .accessibilityIdentifier("bottom_bar_calls_button")
}
Spacer()
@@ -435,7 +436,7 @@ struct ContentView: View {
.resizable()
.foregroundStyle(sharedMainViewModel.indexView == 2 ? Color.orangeMain500 : Color.grayMain2c600)
.frame(width: 25, height: 25)
-
+
if sharedMainViewModel.indexView == 2 {
Text("bottom_navigation_conversations_label")
.default_text_style_700(styleSize: 10)
@@ -446,6 +447,7 @@ struct ContentView: View {
}
})
.padding(.top)
+ .accessibilityIdentifier("bottom_bar_chat_button")
}
Spacer()
diff --git a/Linphone/UI/Welcome/WelcomeView.swift b/Linphone/UI/Welcome/WelcomeView.swift
index 31433f634..a62efe3e0 100644
--- a/Linphone/UI/Welcome/WelcomeView.swift
+++ b/Linphone/UI/Welcome/WelcomeView.swift
@@ -55,7 +55,7 @@ struct WelcomeView: View {
Text("welcome_carousel_skip")
.underline()
.default_text_style_600(styleSize: 15)
-
+
})
.padding(.top, -35)
.padding(.trailing, 20)
@@ -64,6 +64,7 @@ struct WelcomeView: View {
self.index = 2
}
)
+ .accessibilityIdentifier("welcome_skip_button")
Text("welcome_page_title")
.default_text_style_800(styleSize: 35)
.padding(.trailing, 100)
diff --git a/Linphone/Utils/MDMManager.swift b/Linphone/Utils/MDMManager.swift
new file mode 100644
index 000000000..d0360942c
--- /dev/null
+++ b/Linphone/Utils/MDMManager.swift
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2010-2023 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 CryptoKit
+import linphonesw
+
+class MDMManager {
+
+ static let shared = MDMManager()
+
+ static let configurationRemovedNotification = Notification.Name("MDMManager.configurationRemoved")
+ static let configurationAppliedNotification = Notification.Name("MDMManager.configurationApplied")
+
+ private static let hasMDMConfigKey = "MDMManager.hasMDMConfig"
+ private static let lastXMLConfigSHA256Key = "MDMManager.lastXMLConfigSHA256"
+ private static let lastCoreConfigSHA256Key = "MDMManager.lastCoreConfigSHA256"
+
+ private var hasMDMConfig: Bool {
+ get { UserDefaults.standard.bool(forKey: MDMManager.hasMDMConfigKey) }
+ set {
+ guard UserDefaults.standard.bool(forKey: MDMManager.hasMDMConfigKey) != newValue else { return }
+ UserDefaults.standard.set(newValue, forKey: MDMManager.hasMDMConfigKey)
+ }
+ }
+
+ private var lastXMLConfigSHA256: String? {
+ get { UserDefaults.standard.string(forKey: MDMManager.lastXMLConfigSHA256Key) }
+ set {
+ guard UserDefaults.standard.string(forKey: MDMManager.lastXMLConfigSHA256Key) != newValue else { return }
+ UserDefaults.standard.set(newValue, forKey: MDMManager.lastXMLConfigSHA256Key)
+ }
+ }
+
+ private var lastCoreConfigSHA256: String? {
+ get { UserDefaults.standard.string(forKey: MDMManager.lastCoreConfigSHA256Key) }
+ set {
+ guard UserDefaults.standard.string(forKey: MDMManager.lastCoreConfigSHA256Key) != newValue else { return }
+ UserDefaults.standard.set(newValue, forKey: MDMManager.lastCoreConfigSHA256Key)
+ }
+ }
+
+ private var isApplyingConfig = false
+
+ private var lastSeenManagedConfigSignature: String?
+
+ func managedConfigChangedSinceLastCheck() -> Bool {
+ let signature: String
+ if let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") {
+ signature = sha256Hash(of: mdmConfig)
+ } else {
+ signature = ""
+ }
+ if signature == lastSeenManagedConfigSignature { return false }
+ lastSeenManagedConfigSignature = signature
+ return true
+ }
+
+ func loadXMLConfigFromMdm(config: Config) {
+ guard let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed"),
+ let xmlConfig = mdmConfig["xml-config"] as? String else {
+ lastXMLConfigSHA256 = nil
+ return
+ }
+
+ let hash = sha256Hash(of: ["xml-config": xmlConfig])
+ if hash == lastXMLConfigSHA256 {
+ Log.info("[MDMManager] xml-config unchanged (SHA256: \(hash)), skipping")
+ return
+ }
+ lastXMLConfigSHA256 = hash
+
+ do {
+ try config.loadFromXmlString(buffer: xmlConfig)
+ Log.info("[MDMManager] xml-config applied (\(xmlConfig.count) chars)")
+ } catch let error {
+ Log.error("[MDMManager] Failed loading xml-config: error = \(error) xml = \(xmlConfig)")
+ }
+ }
+
+ func applyMdmConfigToCore(core: Core) {
+ guard !isApplyingConfig else { return }
+ isApplyingConfig = true
+ defer {
+ isApplyingConfig = false
+ _ = managedConfigChangedSinceLastCheck()
+ }
+
+ loadXMLConfigFromMdm(config: core.config!)
+
+ guard let mdmConfig = UserDefaults.standard.dictionary(forKey: "com.apple.configuration.managed") else {
+ if hasMDMConfig {
+ Log.info("[MDMManager] Managed configuration was removed")
+ lastXMLConfigSHA256 = nil
+ lastCoreConfigSHA256 = nil
+ hasMDMConfig = false
+ handleConfigurationRemoved()
+ } else {
+ Log.info("[MDMManager] Managed configuration is empty")
+ }
+ return
+ }
+
+ hasMDMConfig = true
+
+ let subset: [String: Any] = [
+ "root-ca": mdmConfig["root-ca"] ?? "",
+ "config-uri": mdmConfig["config-uri"] ?? ""
+ ]
+ let hash = sha256Hash(of: subset)
+ if hash == lastCoreConfigSHA256 {
+ Log.info("[MDMManager] root-ca/config-uri unchanged (SHA256: \(hash)), skipping")
+ NotificationCenter.default.post(name: MDMManager.configurationAppliedNotification, object: self, userInfo: ["config": mdmConfig])
+ return
+ }
+ lastCoreConfigSHA256 = hash
+
+ let currentProvisioningUri = core.provisioningUri
+
+ if let rootCa = mdmConfig["root-ca"] as? String {
+ core.rootCaData = rootCa
+ Log.info("[MDMManager] root-ca applied (\(rootCa.count) chars)")
+ }
+ if let configUri = mdmConfig["config-uri"] as? String {
+ do {
+ if configUri != currentProvisioningUri {
+ try core.setProvisioninguri(newValue: configUri)
+ Log.info("[MDMManager] config-uri applied \(configUri)")
+ }
+ } catch let error {
+ Log.error("[MDMManager] Failed setting provisioning URI: error = \(error) configUri = \(configUri)")
+ }
+ }
+
+ if core.globalState == .On {
+ do {
+ core.stop()
+ try core.start()
+ } catch let error {
+ Log.error("[MDMManager] Failed restarting core: error = \(error)")
+ }
+ }
+
+ NotificationCenter.default.post(name: MDMManager.configurationAppliedNotification, object: self, userInfo: ["config": mdmConfig])
+ }
+
+ private func handleConfigurationRemoved() {
+ Log.info("[MDMManager] handleConfigurationRemoved - posting notification")
+ NotificationCenter.default.post(name: MDMManager.configurationRemovedNotification, object: self)
+ }
+
+ private func sha256Hash(of dict: [String: Any]) -> String {
+ let description = dict.sorted(by: { $0.key < $1.key }).map { "\($0.key)=\($0.value)" }.joined(separator: "&")
+ let digest = SHA256.hash(data: Data(description.utf8))
+ return digest.map { String(format: "%02x", $0) }.joined()
+ }
+}
diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj
index dc6069089..daa1b8f28 100644
--- a/LinphoneApp.xcodeproj/project.pbxproj
+++ b/LinphoneApp.xcodeproj/project.pbxproj
@@ -50,6 +50,7 @@
C642277C2E8E4D900094FEDC /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C642277A2E8E4AC50094FEDC /* ThemeManager.swift */; };
C642277D2E8E4E2B0094FEDC /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D717071D2AC5922E0037746F /* ColorExtension.swift */; };
C65270F82F879D2700FF248C /* EarlyPushkitDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */; };
+ C65271062F87D3E600FF248C /* MDMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65271052F87D3E600FF248C /* MDMManager.swift */; };
C67586AE2C09F23C002E77BF /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AD2C09F23C002E77BF /* URLExtension.swift */; };
C67586B02C09F247002E77BF /* URIHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586AF2C09F247002E77BF /* URIHandler.swift */; };
C67586B52C09F617002E77BF /* SingleSignOnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C67586B22C09F617002E77BF /* SingleSignOnManager.swift */; };
@@ -240,6 +241,20 @@
remoteGlobalIDString = 660AAF7A2B839271004C0FA6;
remoteInfo = msgNotificationService;
};
+ C65271122F88D1CA00FF248C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D719ABB22ABC67BF00B41C10;
+ remoteInfo = LinphoneApp;
+ };
+ C6E408B32F8E1A33003E0F8C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = D719ABB22ABC67BF00B41C10;
+ remoteInfo = LinphoneApp;
+ };
D7458F372E0BDCF4000C957A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */;
@@ -309,6 +324,8 @@
C62817332C1C7C7400DBA646 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = ""; };
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; };
C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyPushkitDelegate.swift; sourceTree = ""; };
+ C65271052F87D3E600FF248C /* MDMManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MDMManager.swift; sourceTree = ""; };
+ C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LinphoneAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
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 = ""; };
C67586B22C09F617002E77BF /* SingleSignOnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSignOnManager.swift; sourceTree = ""; };
@@ -318,6 +335,7 @@
C6A5A9472C10B6A30070FEA4 /* AuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = ""; };
C6DC4E3C2C199C4E009096FD /* BundleExtenion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleExtenion.swift; sourceTree = ""; };
C6DC4E3E2C19C289009096FD /* SideMenuEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuEntry.swift; sourceTree = ""; };
+ C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LinphoneAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D703F7072DC8C5FF005B8F75 /* FilePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePicker.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 = ""; };
@@ -537,6 +555,8 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
+ C652710D2F88D1CA00FF248C /* LinphoneAppUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LinphoneAppUITests; sourceTree = ""; };
+ C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LinphoneAppTests; sourceTree = ""; };
D7458F302E0BDCF4000C957A /* linphoneExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D7458F3C2E0BDCF4000C957A /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = linphoneExtension; sourceTree = ""; };
D792F15B2F02BCC2002E3225 /* intentsExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D792F1642F02BCC2002E3225 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = intentsExtension; sourceTree = ""; };
/* End PBXFileSystemSynchronizedRootGroup section */
@@ -552,6 +572,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C65271092F88D1CA00FF248C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C6E408AC2F8E1A33003E0F8C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D719ABB02ABC67BF00B41C10 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -698,6 +732,7 @@
D717071C2AC591EF0037746F /* Utils */ = {
isa = PBXGroup;
children = (
+ C65271052F87D3E600FF248C /* MDMManager.swift */,
D7BC10D82F4EF64E00F09BDA /* AudioMode.swift */,
D7BC10D32F4EF18100F09BDA /* SoundPlayer.swift */,
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */,
@@ -732,6 +767,8 @@
660AAF7C2B839272004C0FA6 /* msgNotificationService */,
D7458F302E0BDCF4000C957A /* linphoneExtension */,
D792F15B2F02BCC2002E3225 /* intentsExtension */,
+ C652710D2F88D1CA00FF248C /* LinphoneAppUITests */,
+ C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */,
D7DF8BE52E2104DC003A3BC7 /* Frameworks */,
D719ABB42ABC67BF00B41C10 /* Products */,
D7AEB9462F29128500298546 /* Shared.xcconfig */,
@@ -745,6 +782,8 @@
660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */,
D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */,
D792F1582F02BCC2002E3225 /* intentsExtension.appex */,
+ C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */,
+ C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */,
);
name = Products;
sourceTree = "";
@@ -1261,6 +1300,52 @@
productReference = 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */;
productType = "com.apple.product-type.app-extension";
};
+ C652710B2F88D1CA00FF248C /* LinphoneAppUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C65271142F88D1CA00FF248C /* Build configuration list for PBXNativeTarget "LinphoneAppUITests" */;
+ buildPhases = (
+ C65271082F88D1CA00FF248C /* Sources */,
+ C65271092F88D1CA00FF248C /* Frameworks */,
+ C652710A2F88D1CA00FF248C /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ C65271132F88D1CA00FF248C /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ C652710D2F88D1CA00FF248C /* LinphoneAppUITests */,
+ );
+ name = LinphoneAppUITests;
+ packageProductDependencies = (
+ );
+ productName = LinphoneAppUITests;
+ productReference = C652710C2F88D1CA00FF248C /* LinphoneAppUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+ C6E408AE2F8E1A33003E0F8C /* LinphoneAppTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C6E408B52F8E1A33003E0F8C /* Build configuration list for PBXNativeTarget "LinphoneAppTests" */;
+ buildPhases = (
+ C6E408AB2F8E1A33003E0F8C /* Sources */,
+ C6E408AC2F8E1A33003E0F8C /* Frameworks */,
+ C6E408AD2F8E1A33003E0F8C /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ C6E408B42F8E1A33003E0F8C /* PBXTargetDependency */,
+ );
+ fileSystemSynchronizedGroups = (
+ C6E408B02F8E1A33003E0F8C /* LinphoneAppTests */,
+ );
+ name = LinphoneAppTests;
+ packageProductDependencies = (
+ );
+ productName = LinphoneAppTests;
+ productReference = C6E408AF2F8E1A33003E0F8C /* LinphoneAppTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
D719ABB22ABC67BF00B41C10 /* LinphoneApp */ = {
isa = PBXNativeTarget;
buildConfigurationList = D719ABC22ABC67BF00B41C10 /* Build configuration list for PBXNativeTarget "LinphoneApp" */;
@@ -1342,6 +1427,14 @@
CreatedOnToolsVersion = 15.0.1;
LastSwiftMigration = 1500;
};
+ C652710B2F88D1CA00FF248C = {
+ CreatedOnToolsVersion = 26.2;
+ TestTargetID = D719ABB22ABC67BF00B41C10;
+ };
+ C6E408AE2F8E1A33003E0F8C = {
+ CreatedOnToolsVersion = 26.2;
+ TestTargetID = D719ABB22ABC67BF00B41C10;
+ };
D719ABB22ABC67BF00B41C10 = {
CreatedOnToolsVersion = 14.3.1;
};
@@ -1392,6 +1485,8 @@
660AAF7A2B839271004C0FA6 /* msgNotificationService */,
D7458F2E2E0BDCF4000C957A /* linphoneExtension */,
D792F1572F02BCC2002E3225 /* intentsExtension */,
+ C652710B2F88D1CA00FF248C /* LinphoneAppUITests */,
+ C6E408AE2F8E1A33003E0F8C /* LinphoneAppTests */,
);
};
/* End PBXProject section */
@@ -1407,6 +1502,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C652710A2F88D1CA00FF248C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C6E408AD2F8E1A33003E0F8C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D719ABB12ABC67BF00B41C10 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1500,6 +1609,20 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C65271082F88D1CA00FF248C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C6E408AB2F8E1A33003E0F8C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
D719ABAF2ABC67BF00B41C10 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -1571,6 +1694,7 @@
D7E2E69F2CE356C90080DA0D /* PopupViewWithTextField.swift in Sources */,
C67586B52C09F617002E77BF /* SingleSignOnManager.swift in Sources */,
D7F4D9CB2B5FD27200CDCD76 /* CallsListFragment.swift in Sources */,
+ C65271062F87D3E600FF248C /* MDMManager.swift in Sources */,
C6A5A9482C10B6A30070FEA4 /* AuthState.swift in Sources */,
D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */,
D711B1342E93F18700DF8C71 /* LdapServerConfigurationFragment.swift in Sources */,
@@ -1712,6 +1836,16 @@
target = 660AAF7A2B839271004C0FA6 /* msgNotificationService */;
targetProxy = 660AAF7D2B839272004C0FA6 /* PBXContainerItemProxy */;
};
+ C65271132F88D1CA00FF248C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D719ABB22ABC67BF00B41C10 /* LinphoneApp */;
+ targetProxy = C65271122F88D1CA00FF248C /* PBXContainerItemProxy */;
+ };
+ C6E408B42F8E1A33003E0F8C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = D719ABB22ABC67BF00B41C10 /* LinphoneApp */;
+ targetProxy = C6E408B32F8E1A33003E0F8C /* PBXContainerItemProxy */;
+ };
D7458F382E0BDCF4000C957A /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D7458F2E2E0BDCF4000C957A /* linphoneExtension */;
@@ -1863,6 +1997,120 @@
};
name = Release;
};
+ C65271152F88D1CA00FF248C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Z2V957B3D6;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = LinphoneApp;
+ };
+ name = Debug;
+ };
+ C65271162F88D1CA00FF248C /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Z2V957B3D6;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_TARGET_NAME = LinphoneApp;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ C6E408B62F8E1A33003E0F8C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Z2V957B3D6;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LinphoneApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LinphoneApp";
+ };
+ name = Debug;
+ };
+ C6E408B72F8E1A33003E0F8C /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Z2V957B3D6;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 26.2;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = org.linphone.LinphoneAppTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = NO;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = NO;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LinphoneApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LinphoneApp";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
D719ABC02ABC67BF00B41C10 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D7AEB9462F29128500298546 /* Shared.xcconfig */;
@@ -2260,6 +2508,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ C65271142F88D1CA00FF248C /* Build configuration list for PBXNativeTarget "LinphoneAppUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C65271152F88D1CA00FF248C /* Debug */,
+ C65271162F88D1CA00FF248C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ C6E408B52F8E1A33003E0F8C /* Build configuration list for PBXNativeTarget "LinphoneAppTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C6E408B62F8E1A33003E0F8C /* Debug */,
+ C6E408B72F8E1A33003E0F8C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
D719ABAE2ABC67BF00B41C10 /* Build configuration list for PBXProject "LinphoneApp" */ = {
isa = XCConfigurationList;
buildConfigurations = (
diff --git a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme
index 1b4ab3ccf..3e0fa3af5 100644
--- a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme
+++ b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/Linphone.xcscheme
@@ -28,6 +28,30 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/LinphoneAppUITests.xcscheme b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/LinphoneAppUITests.xcscheme
new file mode 100644
index 000000000..25fbbf426
--- /dev/null
+++ b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/LinphoneAppUITests.xcscheme
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/intentsExtension.xcscheme b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/intentsExtension.xcscheme
new file mode 100644
index 000000000..3fa8ae201
--- /dev/null
+++ b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/intentsExtension.xcscheme
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/linphoneExtension.xcscheme b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/linphoneExtension.xcscheme
new file mode 100644
index 000000000..3ce32cff3
--- /dev/null
+++ b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/linphoneExtension.xcscheme
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinphoneApp.xcodeproj/xcshareddata/xcschemes/msgNotificationService.xcscheme b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/msgNotificationService.xcscheme
new file mode 100644
index 000000000..c689337b3
--- /dev/null
+++ b/LinphoneApp.xcodeproj/xcshareddata/xcschemes/msgNotificationService.xcscheme
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LinphoneAppTests/MDMManagerTests.swift b/LinphoneAppTests/MDMManagerTests.swift
new file mode 100644
index 000000000..4e51027ab
--- /dev/null
+++ b/LinphoneAppTests/MDMManagerTests.swift
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2010-2023 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 XCTest
+import linphonesw
+@testable import LinphoneApp
+
+class MDMManagerTests: XCTestCase {
+
+ private let managedKey = "com.apple.configuration.managed"
+ private let dummyRootCa = """
+ -----BEGIN CERTIFICATE-----
+ MIIDdummyTESTCERTIFICATEVALUEForUnitTestsOnly1234567890abcdefABCDEF
+ -----END CERTIFICATE-----
+ """
+
+ override func setUp() {
+ super.setUp()
+ UserDefaults.standard.removeObject(forKey: managedKey)
+ UserDefaults.standard.removeObject(forKey: "MDMManager.hasMDMConfig")
+ UserDefaults.standard.removeObject(forKey: "MDMManager.lastConfigSHA256")
+ }
+
+ override func tearDown() {
+ UserDefaults.standard.removeObject(forKey: managedKey)
+ UserDefaults.standard.removeObject(forKey: "MDMManager.hasMDMConfig")
+ UserDefaults.standard.removeObject(forKey: "MDMManager.lastConfigSHA256")
+ super.tearDown()
+ }
+
+ func testApplyMdmConfigSetsRootCa() throws {
+ let mdmConfig: [String: Any] = ["root-ca": dummyRootCa]
+ UserDefaults.standard.set(mdmConfig, forKey: managedKey)
+
+ let config = Config.newForSharedCore(
+ appGroupId: Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_NAME") as? String ?? "group.test",
+ configFilename: "linphonerc-test",
+ factoryConfigFilename: nil
+ )
+
+ let core = try Factory.Instance.createCoreWithConfig(config: config!, systemContext: nil)
+
+ let appliedExpectation = expectation(forNotification: MDMManager.configurationAppliedNotification, object: nil) { notification in
+ guard let config = notification.userInfo?["config"] as? [String: Any] else { return false }
+ return (config["root-ca"] as? String) == self.dummyRootCa
+ }
+
+ MDMManager.shared.applyMdmConfigToCore(core: core)
+
+ wait(for: [appliedExpectation], timeout: 5)
+
+ XCTAssertEqual(core.rootCaData, dummyRootCa,
+ "core.rootCaData should equal the MDM-provided root-ca after applyMdmConfigToCore")
+ }
+
+}
diff --git a/LinphoneAppUITests/MDMChatFeatureUITests.swift b/LinphoneAppUITests/MDMChatFeatureUITests.swift
new file mode 100644
index 000000000..e905ef4f0
--- /dev/null
+++ b/LinphoneAppUITests/MDMChatFeatureUITests.swift
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2010-2023 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 XCTest
+
+class MDMChatFeatureUITests: XCTestCase {
+
+ let app = XCUIApplication()
+ private var permissionMonitor: NSObjectProtocol?
+
+ override func setUp() {
+ super.setUp()
+ continueAfterFailure = false
+
+ permissionMonitor = addUIInterruptionMonitor(withDescription: "System Permission Alert") { alert in
+ let prefixMatches = ["Share All", "Allow"]
+ for prefix in prefixMatches {
+ let match = alert.buttons.matching(NSPredicate(format: "label BEGINSWITH %@", prefix)).firstMatch
+ if match.exists {
+ match.tap()
+ return true
+ }
+ }
+ let allowLabels = ["Continue", "OK", "Allow Once", "Always Allow"]
+ for label in allowLabels {
+ let button = alert.buttons[label]
+ if button.exists {
+ button.tap()
+ return true
+ }
+ }
+ return false
+ }
+ }
+
+ override func tearDown() {
+ if let monitor = permissionMonitor {
+ removeUIInterruptionMonitor(monitor)
+ }
+ super.tearDown()
+ }
+
+ private func requiredEnv(_ name: String) -> String {
+ guard let value = ProcessInfo.processInfo.environment[name], !value.isEmpty else {
+ XCTFail("Missing required env var \(name). Export TEST_RUNNER_\(name) before running scripts/run-mdm-tests.sh")
+ return ""
+ }
+ return value
+ }
+
+ private func launchWithMDM(_ mdm: [String: Any]) {
+ let data = try! JSONSerialization.data(withJSONObject: mdm)
+ app.launchEnvironment["UITEST_MDM_CONFIG"] = String(data: data, encoding: .utf8)!
+ app.launch()
+ _ = app.wait(for: .runningForeground, timeout: 15)
+ }
+
+ private func skipOnboardingIfShown() {
+ let welcomeSkip = app.buttons["welcome_skip_button"]
+ if welcomeSkip.waitForExistence(timeout: 5) {
+ welcomeSkip.tap()
+ }
+ let permissionsSkip = app.buttons["permissions_skip_button"]
+ if permissionsSkip.waitForExistence(timeout: 5) {
+ permissionsSkip.tap()
+ }
+ }
+
+ private func waitForMainPage(timeout: TimeInterval) -> Bool {
+ let callsButton = app.buttons.matching(NSPredicate(format: "label == %@", "Calls")).firstMatch
+ let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+ let alertPrefixes = ["Share All", "Allow", "Continue", "OK"]
+
+ let deadline = Date().addingTimeInterval(timeout)
+ while Date() < deadline {
+ for prefix in alertPrefixes {
+ let button = springboard.buttons.matching(
+ NSPredicate(format: "label BEGINSWITH %@", prefix)
+ ).firstMatch
+ if button.exists {
+ button.tap()
+ break
+ }
+ }
+ if callsButton.exists {
+ return true
+ }
+ _ = callsButton.waitForExistence(timeout: 1)
+ }
+ return false
+ }
+
+ private func dumpOnFailure(_ label: String) {
+ let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
+ print("=== APP DEBUG DESCRIPTION (\(label)) ===")
+ print(app.debugDescription)
+ print("=== SPRINGBOARD DEBUG DESCRIPTION =====================")
+ print(springboard.debugDescription)
+ print("=======================================================")
+ }
+
+ func testChatButtonHiddenWithMDMDisableChat() {
+ let username = requiredEnv("LINPHONE_TEST_USERNAME")
+ let ha1 = requiredEnv("LINPHONE_TEST_HA1")
+ let domain = ProcessInfo.processInfo.environment["LINPHONE_TEST_DOMAIN"] ?? "sip.linphone.org"
+
+ let xmlConfig = """
+
+
+
+
+ sip:\(username)@\(domain)
+ <sip:\(domain);transport=tls>
+ <sip:\(domain);transport=tls>
+ \(domain)
+ https://lime.linphone.org:443/lime-server/lime-server.php
+
+
+ \(username)
+ \(domain)
+ \(ha1)
+ \(domain)
+ MD5
+
+
+ """
+
+ launchWithMDM(["xml-config": xmlConfig])
+ skipOnboardingIfShown()
+
+ let landed = waitForMainPage(timeout: 30)
+ if !landed { dumpOnFailure("main screen not reached") }
+ XCTAssertTrue(landed, "Should have landed on the main screen after Welcome + Permissions")
+
+ let chatButton = app.buttons.matching(NSPredicate(format: "label == %@", "Conversations")).firstMatch
+ XCTAssertFalse(chatButton.exists,
+ "Chat button should NOT be visible when MDM disables chat feature")
+ }
+
+ func testConfigUriMDMLandsOnMainPage() {
+ let configUri = requiredEnv("LINPHONE_TEST_CONFIG_URI")
+
+ launchWithMDM(["config-uri": configUri])
+ skipOnboardingIfShown()
+
+ let landed = waitForMainPage(timeout: 60)
+ if !landed { dumpOnFailure("main screen not reached with config-uri \(configUri)") }
+ XCTAssertTrue(landed,
+ "Should have landed on the main screen after remote provisioning from \(configUri)")
+ }
+}
diff --git a/README.md b/README.md
index eb049f870..3bf80f471 100644
--- a/README.md
+++ b/README.md
@@ -133,3 +133,117 @@ cmake --preset=ios-sdk -G Ninja -B spm-ios && cmake --build spm-ios
- Add it manually if needed.

+
+# MDM (Mobile Device Management) configuration
+
+Linphone iOS supports managed app configuration via the standard iOS MDM
+`com.apple.configuration.managed` mechanism. When the app is deployed through
+an MDM server, administrators can push a configuration dictionary that the app
+reads at startup and whenever the managed configuration changes at runtime.
+
+The following keys are supported:
+
+| Key | Type | Description |
+|---------------|--------|-------------------------------------------------------------------------------------------------|
+| `xml-config` | String | A Linphone configuration in XML format (same schema as `linphonerc`). Applied via `Config.loadFromXmlString`. |
+| `root-ca` | String | A PEM-encoded root CA certificate used by the Linphone SDK for TLS operations (SIPS, HTTPS provisioning, …). Applied to `core.rootCaData`. |
+| `config-uri` | String | URI to a remote provisioning file. When set, it takes precedence over any `config-uri` that may be defined inside `xml-config`, and triggers a core restart to fetch the remote configuration. |
+
+Notes:
+- All three keys are optional and can be combined.
+- If `config-uri` is present, it is set last and the core is restarted so that
+ remote provisioning takes effect; any `config-uri` value embedded in
+ `xml-config` is therefore overridden.
+- Applying and removing the managed configuration at runtime is supported:
+ removing it resets the core to its default configuration and returns to the
+ assistant/login screen.
+
+## Testing MDM configuration
+
+Two kinds of tests are provided:
+
+### UI tests (end-to-end)
+
+Located in `LinphoneAppUITests/MDMChatFeatureUITests.swift`. They inject a
+managed configuration at launch via the app's DEBUG-only
+`UITEST_MDM_CONFIG` launch-environment hook (implemented in
+`Linphone/LinphoneApp.swift`), so no `xcrun simctl` setup is needed.
+
+Note: the tests only cover MDM *application* (fresh launch with a managed
+config). Live removal of MDM while the app is running cannot be simulated
+from an XCUITest process (UserDefaults is per-process and we want the app
+to stay alive for a realistic removal scenario), so that path is covered by
+manual testing only.
+
+Each MDM test case represents "a fresh device receiving a specific managed
+configuration", so we uninstall the app before every test to avoid any
+leakage of UserDefaults / keychain / provisioning / accounts between cases.
+The wrapper script `scripts/run-mdm-tests.sh` does this for you.
+
+The tests need a real SIP account to reach the main screen (the MDM XML
+embeds proxy + auth_info sections) and a remote provisioning URL for the
+config-uri test. Credentials can be provided three ways — the script
+resolves them in this order, highest first:
+
+1. CLI flags: `--username`, `--ha1`, `--domain`, `--config-uri` (and
+ `--device` for the sim UUID)
+2. Shell env vars: `LINPHONE_TEST_USERNAME`, `LINPHONE_TEST_HA1`,
+ `LINPHONE_TEST_DOMAIN`, `LINPHONE_TEST_CONFIG_URI`
+3. The gitignored file `scripts/test-credentials.env` (copy from `.env.example`)
+
+Examples:
+
+```bash
+scripts/run-mdm-tests.sh --device --username alice --ha1 --config-uri https://example.com/provisioning.xml
+```
+
+```bash
+cp scripts/test-credentials.env.example scripts/test-credentials.env
+# edit scripts/test-credentials.env, fill in LINPHONE_TEST_USERNAME /
+# LINPHONE_TEST_HA1 / LINPHONE_TEST_CONFIG_URI
+scripts/run-mdm-tests.sh
+```
+
+It will create+boot a throwaway simulator if `DEVICE_UUID` is not set,
+uninstall the app before each test, run the tests one at a time with
+`-parallel-testing-enabled NO`, and clean up at the end. To reuse an
+already-booted simulator:
+
+```bash
+DEVICE_UUID= scripts/run-mdm-tests.sh
+```
+
+`-parallel-testing-enabled NO` avoids flaky UI test launch failures caused by
+Xcode spinning up multiple simulator clones in parallel (the test-runner app
+can fail to launch on a clone under pressure).
+
+Covered cases:
+- `testChatButtonHiddenWithMDMDisableChat` — MDM `xml-config` with
+ `disable_chat_feature=1`; the test reaches the main screen and asserts the
+ chat button is hidden.
+- `testConfigUriMDMLandsOnMainPage` — MDM `config-uri` pointing at the URL
+ supplied via `--config-uri` / `LINPHONE_TEST_CONFIG_URI`; the test verifies
+ that remote provisioning completes and the app lands on the main screen.
+
+### Unit tests (MDMManager)
+
+Located in `LinphoneAppTests/MDMManagerTests.swift`. The unit test covers
+only `root-ca` application: it calls
+`MDMManager.shared.applyMdmConfigToCore(core:)` directly on a throwaway
+`Core` and asserts `core.rootCaData` matches the MDM-provided certificate.
+The `config-uri` and `xml-config` paths are exercised end-to-end by the UI
+tests above.
+
+This requires a **Unit Testing Bundle** target in Xcode (separate from the UI
+test target, because `@testable import Linphone` only works from a unit test
+bundle):
+
+1. Xcode → File → New → Target → iOS → Unit Testing Bundle
+2. Name it `LinphoneAppTests`, set "Target to be Tested" to `LinphoneApp`
+3. Add `LinphoneAppTests/MDMManagerTests.swift` to that target
+
+Then run:
+
+```bash
+xcodebuild test -project LinphoneApp.xcodeproj -scheme LinphoneAppTests -destination "platform=iOS Simulator,id=$DEVICE_UUID"
+```
diff --git a/scripts/run-mdm-tests.sh b/scripts/run-mdm-tests.sh
new file mode 100755
index 000000000..52d11e9fd
--- /dev/null
+++ b/scripts/run-mdm-tests.sh
@@ -0,0 +1,155 @@
+#!/usr/bin/env bash
+#
+# Run MDM UI tests with a clean app install before each test case.
+#
+# Each MDM UI test scenario represents a fresh device receiving a specific
+# managed configuration. To reproduce that "closest to reality", we uninstall
+# the app before every single test so no state (UserDefaults, keychain,
+# provisioning, accounts) leaks between runs.
+#
+# Usage:
+# scripts/run-mdm-tests.sh [--device ] [--username ] [--ha1 ] [--domain ]
+#
+# Or with environment variables / the gitignored `scripts/test-credentials.env`:
+# DEVICE_UUID= LINPHONE_TEST_USERNAME=... LINPHONE_TEST_HA1=... scripts/run-mdm-tests.sh
+#
+# Resolution order (highest first): CLI flag > shell env > test-credentials.env > default.
+#
+# If no device UUID is given, the script creates and boots a throwaway
+# "iPhone 15" simulator, then deletes it at the end.
+#
+# SIP test credentials are required for tests that need a working account.
+# They are forwarded to the UI test runner via the `TEST_RUNNER_` prefix,
+# which Xcode strips before exposing them to the test process.
+# LINPHONE_TEST_USERNAME SIP username (required)
+# LINPHONE_TEST_HA1 md5(username:realm:password) (required)
+# LINPHONE_TEST_DOMAIN defaults to sip.linphone.org
+# LINPHONE_TEST_CONFIG_URI remote provisioning URL (required for the
+# config-uri UI test)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+if [[ -f "$SCRIPT_DIR/test-credentials.env" ]]; then
+ # shellcheck source=/dev/null
+ source "$SCRIPT_DIR/test-credentials.env"
+fi
+
+CLI_DEVICE=""
+CLI_USERNAME=""
+CLI_HA1=""
+CLI_DOMAIN=""
+CLI_CONFIG_URI=""
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --device) CLI_DEVICE="$2"; shift 2 ;;
+ --username) CLI_USERNAME="$2"; shift 2 ;;
+ --ha1) CLI_HA1="$2"; shift 2 ;;
+ --domain) CLI_DOMAIN="$2"; shift 2 ;;
+ --config-uri) CLI_CONFIG_URI="$2"; shift 2 ;;
+ -h|--help)
+ sed -n '3,20p' "$0"
+ exit 0
+ ;;
+ -*)
+ echo "Unknown flag: $1" >&2
+ exit 2
+ ;;
+ *)
+ # First bare positional arg = device UUID (back-compat).
+ if [[ -z "$CLI_DEVICE" ]]; then CLI_DEVICE="$1"; shift
+ else echo "Unexpected argument: $1" >&2; exit 2
+ fi
+ ;;
+ esac
+done
+
+LINPHONE_TEST_USERNAME="${CLI_USERNAME:-${LINPHONE_TEST_USERNAME:-}}"
+LINPHONE_TEST_HA1="${CLI_HA1:-${LINPHONE_TEST_HA1:-}}"
+LINPHONE_TEST_DOMAIN="${CLI_DOMAIN:-${LINPHONE_TEST_DOMAIN:-sip.linphone.org}}"
+LINPHONE_TEST_CONFIG_URI="${CLI_CONFIG_URI:-${LINPHONE_TEST_CONFIG_URI:-}}"
+
+: "${LINPHONE_TEST_USERNAME:?LINPHONE_TEST_USERNAME is required (pass --username, export it, or put it in scripts/test-credentials.env)}"
+: "${LINPHONE_TEST_HA1:?LINPHONE_TEST_HA1 is required (pass --ha1, export it, or put it in scripts/test-credentials.env)}"
+: "${LINPHONE_TEST_CONFIG_URI:?LINPHONE_TEST_CONFIG_URI is required (pass --config-uri, export it, or put it in scripts/test-credentials.env)}"
+export TEST_RUNNER_LINPHONE_TEST_USERNAME="$LINPHONE_TEST_USERNAME"
+export TEST_RUNNER_LINPHONE_TEST_HA1="$LINPHONE_TEST_HA1"
+export TEST_RUNNER_LINPHONE_TEST_DOMAIN="$LINPHONE_TEST_DOMAIN"
+export TEST_RUNNER_LINPHONE_TEST_CONFIG_URI="$LINPHONE_TEST_CONFIG_URI"
+
+BUNDLE_ID="org.linphone.phone"
+PROJECT="LinphoneApp.xcodeproj"
+SCHEME="LinphoneAppUITests"
+TEST_CLASS="LinphoneAppUITests/MDMChatFeatureUITests"
+
+# App groups survive `simctl uninstall`, so linphonerc / SDK state from a
+# previous test leaks into the next fresh install. We nuke these between
+# tests so each test really starts from scratch.
+APP_GROUPS=(
+ "group.org.linphone.phone.msgNotification"
+ "group.org.linphone.phone.linphoneExtension"
+)
+
+TESTS=(
+ "testChatButtonHiddenWithMDMDisableChat"
+ "testConfigUriMDMLandsOnMainPage"
+)
+
+CREATED_DEVICE=0
+DEVICE_UUID="${CLI_DEVICE:-${DEVICE_UUID:-}}"
+
+if [[ -z "$DEVICE_UUID" ]]; then
+ echo "No DEVICE_UUID provided, creating a throwaway simulator..."
+ DEVICE_UUID=$(xcrun simctl create "LinphoneMDMTest" "iPhone 15")
+ xcrun simctl boot "$DEVICE_UUID"
+ CREATED_DEVICE=1
+else
+ # Make sure the simulator is booted — simctl uninstall fails on Shutdown.
+ # `simctl boot` is a no-op on an already-booted device except for exit code,
+ # so we ignore that specific error.
+ xcrun simctl boot "$DEVICE_UUID" 2>/dev/null || true
+fi
+
+cleanup() {
+ if [[ "$CREATED_DEVICE" == "1" ]]; then
+ xcrun simctl shutdown "$DEVICE_UUID" || true
+ xcrun simctl delete "$DEVICE_UUID" || true
+ fi
+}
+trap cleanup EXIT
+
+for test in "${TESTS[@]}"; do
+ echo ""
+ echo "=============================================="
+ echo "Running $test on $DEVICE_UUID"
+ echo "=============================================="
+ # Wipe app group containers BEFORE uninstall — get_app_container needs the
+ # app installed to resolve the group path. On the very first iteration the
+ # app isn't installed yet (nothing to leak), so the lookup silently no-ops.
+ for group in "${APP_GROUPS[@]}"; do
+ group_path=$(xcrun simctl get_app_container "$DEVICE_UUID" "$BUNDLE_ID" "$group" 2>/dev/null || true)
+ if [[ -n "$group_path" && -d "$group_path" ]]; then
+ echo "Wiping app group container: $group_path"
+ rm -rf "$group_path"/* "$group_path"/.[!.]* 2>/dev/null || true
+ fi
+ done
+ xcrun simctl uninstall "$DEVICE_UUID" "$BUNDLE_ID" || true
+ # Keychain survives simctl uninstall — once the core materializes an account
+ # from MDM it saves auth info to the keychain, which would make the next
+ # fresh install skip the welcome flow. Reset the keychain between tests.
+ xcrun simctl keychain "$DEVICE_UUID" reset || true
+ # Pre-grant privacy-sensitive permissions so no system dialogs interrupt the
+ # flow (Contacts triggers an extra "Share All N Contacts" limited-access
+ # sheet on iOS 18 that UIInterruptionMonitor can only dismiss if another app
+ # interaction follows — not worth juggling). Must happen after uninstall +
+ # before the test launches the app. Ignore failures (not all sims/iOS
+ # versions support every service).
+ for service in contacts notifications location location-always camera microphone photos; do
+ xcrun simctl privacy "$DEVICE_UUID" grant "$service" "$BUNDLE_ID" 2>/dev/null || true
+ done
+ xcodebuild test -project "$PROJECT" -scheme "$SCHEME" -destination "platform=iOS Simulator,id=$DEVICE_UUID" -only-testing:"$TEST_CLASS/$test" -parallel-testing-enabled NO
+done
+
+echo ""
+echo "All MDM UI tests passed."