diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift
index b73fb63b0..12d054ce8 100644
--- a/Linphone/Core/CoreContext.swift
+++ b/Linphone/Core/CoreContext.swift
@@ -566,15 +566,30 @@ class CoreContext: ObservableObject {
}
enum AppServices {
- static let config = Config.newForSharedCore(
- appGroupId: Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_NAME") as? String
- ?? {
- fatalError("APP_GROUP_NAME not defined in Info.plist")
- }(),
- configFilename: "linphonerc",
- factoryConfigFilename: FileUtil.bundleFilePath("linphonerc-factory")
- )!
-
+ private static var _config: Config?
+
+ static var configIfAvailable: Config? {
+ if let existing = _config {
+ return existing
+ }
+ _config = Config.newForSharedCore(
+ appGroupId: Bundle.main.object(forInfoDictionaryKey: "APP_GROUP_NAME") as? String
+ ?? {
+ fatalError("APP_GROUP_NAME not defined in Info.plist")
+ }(),
+ configFilename: "linphonerc",
+ factoryConfigFilename: FileUtil.bundleFilePath("linphonerc-factory")
+ )
+ return _config
+ }
+
+ static var config: Config {
+ guard let config = configIfAvailable else {
+ fatalError("AppServices.config accessed before it was available")
+ }
+ return config
+ }
+
static let corePreferences = CorePreferences(config: config)
}
diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift
index 9827e0693..4dfdb137c 100644
--- a/Linphone/LinphoneApp.swift
+++ b/Linphone/LinphoneApp.swift
@@ -21,6 +21,7 @@ import SwiftUI
import linphonesw
import UserNotifications
import Intents
+import PushKit
let accountTokenNotification = Notification.Name("AccountCreationTokenReceived")
var displayedChatroomPeerAddr: String?
@@ -151,28 +152,69 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
@main
struct LinphoneApp: App {
- @Environment(\.scenePhase) var scenePhase
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
+ @State private var configAvailable = AppServices.configIfAvailable != nil
+ private let earlyPushDelegate = EarlyPushkitDelegate()
+ private let voipRegistry = PKPushRegistry(queue: coreQueue)
+
+ init() {
+ if !configAvailable {
+ voipRegistry.delegate = earlyPushDelegate
+ voipRegistry.desiredPushTypes = [.voIP]
+ waitForConfig()
+ } else {
+ let _ = CoreContext.shared
+ }
+ }
+
+ var body: some Scene {
+ WindowGroup {
+ if configAvailable {
+ AppView(delegate: delegate)
+ } else {
+ SplashScreen(showSpinner: true)
+ .onAppear {
+ waitForConfig()
+ }
+ }
+ }
+ }
+
+ private func waitForConfig() {
+ if AppServices.configIfAvailable != nil {
+ let _ = CoreContext.shared
+ configAvailable = true
+ } else {
+ Log.warn("AppServices.config not available yet, retrying in 1s...")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+ waitForConfig()
+ }
+ }
+ }
+}
+
+struct AppView: View {
+ @Environment(\.scenePhase) var scenePhase
+ let delegate: AppDelegate
+
@StateObject private var coreContext = CoreContext.shared
@StateObject private var navigationManager = NavigationManager()
@StateObject private var telecomManager = TelecomManager.shared
@StateObject private var sharedMainViewModel = SharedMainViewModel.shared
- var body: some Scene {
- WindowGroup {
- RootView(
- coreContext: coreContext,
- telecomManager: telecomManager,
- sharedMainViewModel: sharedMainViewModel,
- navigationManager: navigationManager,
- appDelegate: delegate
- )
- .environmentObject(coreContext)
- .environmentObject(navigationManager)
- .environmentObject(telecomManager)
- .environmentObject(sharedMainViewModel)
- }
+ var body: some View {
+ RootView(
+ coreContext: coreContext,
+ telecomManager: telecomManager,
+ sharedMainViewModel: sharedMainViewModel,
+ navigationManager: navigationManager,
+ appDelegate: delegate
+ )
+ .environmentObject(coreContext)
+ .environmentObject(navigationManager)
+ .environmentObject(telecomManager)
+ .environmentObject(sharedMainViewModel)
.onChange(of: scenePhase) { newPhase in
if !telecomManager.callInProgress {
switch newPhase {
diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings
index 3fc91a12c..f9ac3f3e8 100644
--- a/Linphone/Localizable/en.lproj/Localizable.strings
+++ b/Linphone/Localizable/en.lproj/Localizable.strings
@@ -616,3 +616,6 @@
"welcome_page_title" = "Welcome";
"You will change this mode later" = "You will change this mode later";
"ZRTP" = "ZRTP";
+"early_push_unknown_caller" = "Unknown";
+"early_push_missed_call_title" = "Missed call";
+"early_push_missed_call_body" = "You received a call while your device was locked. Please unlock and reopen the app.";
diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings
index 77015b251..d1cab3098 100644
--- a/Linphone/Localizable/fr.lproj/Localizable.strings
+++ b/Linphone/Localizable/fr.lproj/Localizable.strings
@@ -616,3 +616,6 @@
"welcome_page_title" = "Bienvenue";
"You will change this mode later" = "You will change this mode later";
"ZRTP" = "ZRTP";
+"early_push_unknown_caller" = "Inconnu";
+"early_push_missed_call_title" = "Appel manqué";
+"early_push_missed_call_body" = "Vous avez reçu un appel alors que votre appareil était verrouillé. Veuillez déverrouiller et rouvrir l'application.";
diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift
index e0959326f..ee57021e3 100644
--- a/Linphone/SplashScreen.swift
+++ b/Linphone/SplashScreen.swift
@@ -20,17 +20,23 @@
import SwiftUI
struct SplashScreen: View {
+ var showSpinner: Bool = false
+
var body: some View {
ZStack {
Color.white
.ignoresSafeArea()
-
+
Image("linphone")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.frame(width: 240, height: 128)
.foregroundColor(ThemeManager.shared.currentTheme.main500)
+
+ if showSpinner {
+ ProgressView()
+ }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.ignoresSafeArea(.all)
diff --git a/Linphone/TelecomManager/EarlyPushkitDelegate.swift b/Linphone/TelecomManager/EarlyPushkitDelegate.swift
new file mode 100644
index 000000000..cc875a32f
--- /dev/null
+++ b/Linphone/TelecomManager/EarlyPushkitDelegate.swift
@@ -0,0 +1,81 @@
+/*
+ * 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 PushKit
+import CallKit
+import UserNotifications
+
+class EarlyPushkitDelegate: NSObject, PKPushRegistryDelegate, CXProviderDelegate {
+ private var activeCalls: [UUID: CXProvider] = [:]
+
+ func providerDidReset(_ provider: CXProvider) {}
+
+ func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
+ Log.info("[EarlyPushkitDelegate] User tried to answer, ending call as device is locked")
+ action.fail()
+ provider.reportCall(with: action.callUUID, endedAt: .init(), reason: .unanswered)
+ activeCalls.removeValue(forKey: action.callUUID)
+ postMissedCallNotification()
+ }
+
+ func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
+ Log.info("[EarlyPushkitDelegate] Received push credentials, ignoring until core is ready")
+ }
+
+ func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
+ Log.info("[EarlyPushkitDelegate] Received incoming push while core is not ready, reporting call to CallKit")
+ let providerConfig = CXProviderConfiguration()
+ providerConfig.supportsVideo = false
+ let provider = CXProvider(configuration: providerConfig)
+ provider.setDelegate(self, queue: .main)
+
+ let update = CXCallUpdate()
+ update.remoteHandle = CXHandle(type: .generic, value: NSLocalizedString("early_push_unknown_caller", comment: ""))
+ update.hasVideo = false
+ let uuid = UUID()
+ activeCalls[uuid] = provider
+
+ provider.reportNewIncomingCall(with: uuid, update: update) { error in
+ if let error = error {
+ Log.error("[EarlyPushkitDelegate] Failed to report call to CallKit: \(error.localizedDescription)")
+ }
+ completion()
+ }
+
+ DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in
+ guard let self = self, let provider = self.activeCalls.removeValue(forKey: uuid) else { return }
+ Log.info("[EarlyPushkitDelegate] Ending unanswered call after timeout")
+ provider.reportCall(with: uuid, endedAt: .init(), reason: .unanswered)
+ self.postMissedCallNotification()
+ }
+ }
+
+ private func postMissedCallNotification() {
+ let content = UNMutableNotificationContent()
+ content.title = NSLocalizedString("early_push_missed_call_title", comment: "")
+ content.body = NSLocalizedString("early_push_missed_call_body", comment: "")
+ content.sound = .default
+ let request = UNNotificationRequest(identifier: "early_push_missed_call", content: content, trigger: nil)
+ UNUserNotificationCenter.current().add(request) { error in
+ if let error = error {
+ Log.error("[EarlyPushkitDelegate] Failed to post missed call notification: \(error.localizedDescription)")
+ }
+ }
+ }
+}
diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj
index c2d9fe18f..593f47d09 100644
--- a/LinphoneApp.xcodeproj/project.pbxproj
+++ b/LinphoneApp.xcodeproj/project.pbxproj
@@ -49,6 +49,7 @@
C642277B2E8E4AC50094FEDC /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C642277A2E8E4AC50094FEDC /* ThemeManager.swift */; };
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 */; };
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 */; };
@@ -307,6 +308,7 @@
C62817312C1C400A00DBA646 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; };
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 = ""; };
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 = ""; };
@@ -593,6 +595,7 @@
662B69D72B25DDF6007118BF /* TelecomManager */ = {
isa = PBXGroup;
children = (
+ C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */,
662B69D82B25DE18007118BF /* TelecomManager.swift */,
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */,
);
@@ -1525,6 +1528,7 @@
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
D7AEB9742F324A6F00298546 /* ConversationDocumentsListFragment.swift in Sources */,
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */,
+ C65270F82F879D2700FF248C /* EarlyPushkitDelegate.swift in Sources */,
D7C2DA1D2CA44DE400A2441B /* EventModel.swift in Sources */,
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
D70A26F22B7F5D95006CC8FC /* ConversationFragment.swift in Sources */,
diff --git a/msgNotificationService/NotificationService.swift b/msgNotificationService/NotificationService.swift
index a6437fac1..3678548d0 100644
--- a/msgNotificationService/NotificationService.swift
+++ b/msgNotificationService/NotificationService.swift
@@ -112,7 +112,11 @@ class NotificationService: UNNotificationServiceExtension {
return
}
- createCore()
+ if !createCore() {
+ bestAttemptContent.title = String(localized: "notification_chat_message_received_title")
+ contentHandler(bestAttemptContent)
+ return
+ }
if !lc!.config!.getBool(section: "app", key: "disable_chat_feature", defaultValue: false) {
Log.info("received push payload : \(bestAttemptContent.userInfo.debugDescription)")
@@ -314,14 +318,17 @@ class NotificationService: UNNotificationServiceExtension {
return msgData
}
- func createCore() {
- Log.info("[msgNotificationService] create core")
-
- let factoryPath = FileUtil.bundleFilePath("linphonerc-factory")!
- let config = Config.newForSharedCore(appGroupId: appGroupName, configFilename: "linphonerc", factoryConfigFilename: factoryPath)!
-
- lc = try? Factory.Instance.createSharedCoreWithConfig(config: config, systemContext: nil, appGroupId: appGroupName, mainCore: false)
- }
+ func createCore() -> Bool {
+ Log.info("[msgNotificationService] create core")
+
+ let factoryPath = FileUtil.bundleFilePath("linphonerc-factory")!
+ if let config = Config.newForSharedCore(appGroupId: appGroupName, configFilename: "linphonerc", factoryConfigFilename: factoryPath) {
+ lc = try? Factory.Instance.createSharedCoreWithConfig(config: config, systemContext: nil, appGroupId: appGroupName, mainCore: false)
+ return lc != nil
+ } else {
+ return false
+ }
+ }
func stopCore() {
Log.info("stop core")