mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-04-17 11:48:27 +00:00
Handle VoIP and APNs push received before shared filesystem is available (BFU)
Delay CoreContext initialization until AppServices.config is available.
When a VoIP push arrives before that, an EarlyPushkitDelegate reports
a temporary CallKit call, ends it as unanswered after 4s, and posts
a missed call notification to the user.
Handle message APNs in service extension without Core/Config availability
This commit is contained in:
parent
0132c5253f
commit
156231f8aa
8 changed files with 195 additions and 34 deletions
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
81
Linphone/TelecomManager/EarlyPushkitDelegate.swift
Normal file
81
Linphone/TelecomManager/EarlyPushkitDelegate.swift
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "<group>"; };
|
||||
C62817332C1C7C7400DBA646 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = "<group>"; };
|
||||
C642277A2E8E4AC50094FEDC /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||
C65270F72F879D2700FF248C /* EarlyPushkitDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarlyPushkitDelegate.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>"; };
|
||||
C67586B22C09F617002E77BF /* SingleSignOnManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleSignOnManager.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue