Local Push Notification - Network extension

This commit is contained in:
Christophe Deschamps 2023-09-15 11:03:28 +02:00 committed by QuentinArguillere
parent 1fb44f32f0
commit 31c57e5b28
18 changed files with 1126 additions and 76 deletions

View file

@ -121,7 +121,6 @@
- (void)applicationDidBecomeActive:(UIApplication *)application {
LOGI(@"%@", NSStringFromSelector(_cmd));
if (!startedInBackground || PhoneMainView.instance.currentView == nil) {
startedInBackground = TRUE;
// initialize UI
@ -295,7 +294,6 @@
[FIRApp configure];
#endif
if ([VFSUtil vfsEnabledWithGroupName:kLinphoneMsgNotificationAppGroupId]) {
if (TARGET_IPHONE_SIMULATOR) {
LOGW(@"[VFS] Can not active for simulators.");

View file

@ -505,6 +505,8 @@
[self setInteger:linphone_core_get_download_bandwidth(LC) forKey:@"download_bandwidth_preference"];
[self setBool:linphone_core_adaptive_rate_control_enabled(LC) forKey:@"adaptive_rate_control_preference"];
[self setObject:[lm lpConfigStringForKey:@"dns_server_ip"] forKey:@"dns_server_preference"];
[self setCString:[lm lpConfigStringForKey:@"ssids" inSection:@"local_push"].UTF8String forKey:@"local_push_ssids"];
}
// tunnel section
@ -1061,6 +1063,13 @@
[lm setDnsServer];
}
[lm lpConfigSetString:[self stringForKey:@"local_push_ssids"] forKey:@"ssids" inSection:@"local_push"];
if (@available(iOS 15.0, *)) {
[LocalPushManager.shared configureLocalPushWithCCoreConfig:lm.configDb];
} else {
LOGW(@"Local push notifications not available for this ios version (iOS 15 minimum)");
}
// tunnel section
if (linphone_core_tunnel_available()) {

View file

@ -686,6 +686,11 @@ static void linphone_iphone_global_state_changed(LinphoneCore *lc, LinphoneGloba
if (state == LinphoneGlobalOn) {
// reload friends
[self.fastAddressBook fetchContactsInBackGroundThread];
if (@available(iOS 15.0, *)) {
[LocalPushManager.shared configureLocalPushWithCCoreConfig:linphone_core_get_config(LC)];
} else {
LOGW(@"Local push notifications not available for this ios version (iOS 15 minimum)");
}
}
}
@ -715,6 +720,14 @@ static void linphone_iphone_configuring_status_changed(LinphoneCore *lc, Linphon
object:self
userInfo:dict];
});
if (status == LinphoneConfiguringSuccessful) {
if (@available(iOS 15.0, *)) {
[LocalPushManager.shared configureLocalPushWithCCoreConfig:linphone_core_get_config(LC)];
} else {
LOGW(@"Local push notifications not available for this ios version (iOS 15 minimum)");
}
}
}
#pragma mark - Registration State Functions

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -12,6 +12,7 @@
<outlet property="callQualityButton" destination="SKk-s0-5HE" id="22M-FN-kRs"/>
<outlet property="callSecurityButton" destination="27" id="29"/>
<outlet property="incallView" destination="0Vp-VF-wmX" id="mLI-RY-bfW"/>
<outlet property="localpushIndicator" destination="Lbe-fc-SC9" id="UZc-RF-pLQ"/>
<outlet property="outcallView" destination="lfO-I4-PXi" id="04e-SG-ViY"/>
<outlet property="registrationState" destination="Mhg-P6-RfU" id="xTR-Af-XBY"/>
<outlet property="view" destination="4" id="11"/>
@ -94,6 +95,13 @@
</button>
</subviews>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="local push active" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Lbe-fc-SC9" userLabel="LocalPushActive">
<rect key="frame" x="65" y="27" width="82" height="12"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" type="italicSystem" pointSize="10"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Mhg-P6-RfU" userLabel="registrationState" customClass="UIIconButton">
<rect key="frame" x="46" y="0.0" width="194" height="42"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" heightSizable="YES"/>

View file

@ -30,6 +30,7 @@
@property(nonatomic, strong) IBOutlet UIButton *callSecurityButton;
@property(weak, nonatomic) IBOutlet UIButton *voicemailButton;
@property(weak, nonatomic) IBOutlet UIButton *callQualityButton;
@property (weak, nonatomic) IBOutlet UILabel *localpushIndicator;
@property(weak, nonatomic) IBOutlet UIView *incallView;
@property(weak, nonatomic) IBOutlet UIView *outcallView;

View file

@ -78,6 +78,16 @@
[self accountUpdate:account];
[self updateUI:linphone_core_get_calls_nb(LC)];
[self updateVoicemail];
if (@available(iOS 15.0, *)) {
[LocalPushManager.shared addActiveCallBackObserverWithAction:^(BOOL active) {
_localpushIndicator.hidden = !active;
}];
} else {
_localpushIndicator.hidden = true;
}
}
- (void)viewWillDisappear:(BOOL)animated {

View file

@ -806,6 +806,12 @@ void update_hash_cbs(LinphoneAccountCreator *creator, LinphoneAccountCreatorStat
[hiddenKeys addObject:@"vfs_enabled"];
}
if (@available(iOS 15.0, *)) {} else {
[hiddenKeys addObject:@"local_push"];
[hiddenKeys addObject:@"local_ssids"];
}
return hiddenKeys;
}

View file

@ -0,0 +1,123 @@
/*
* 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
import NetworkExtension
import linphonesw
import Combine
let localPushProviderBundleIdentifier = "org.linphone.phone.localpushprovider"
@available(iOS 15.0, *)
@objc class LocalPushManager : NSObject, NEAppPushDelegate {
@objc static let shared = LocalPushManager()
private var appPushManager: NEAppPushManager?
private let isInitialized = MutableLiveData(false)
let isActive = MutableLiveData(false)
private let dispatchQueue = DispatchQueue(label: "DirectoryViewModel.dispatchQueue")
private let pushManagerIsActiveSubject = CurrentValueSubject<Bool, Never>(false)
private var pushManagerIsActiveCancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()
private(set) lazy var pushManagerIsActivePublisher = {
pushManagerIsActiveSubject
.debounce(for: .milliseconds(500), scheduler: dispatchQueue)
.eraseToAnyPublisher()
}()
override init() {
super.init()
NEAppPushManager.loadAllFromPreferences { managers, error in
if let error = error {
Log.e("[LocalPushManager] Failed to load all NEAppPushManager's from preferences: \(error)")
self.isInitialized.value = true
return
}
self.appPushManager = managers?.first ?? NEAppPushManager()
let appPushManager = self.appPushManager!
self.pushManagerIsActiveCancellable = NSObject.KeyValueObservingPublisher(object: appPushManager, keyPath: \.isActive, options: [.initial, .new])
.subscribe(self.pushManagerIsActiveSubject)
self.pushManagerIsActivePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] isAppPushManagerActive in
self?.isActive.value = isAppPushManagerActive
}
.store(in: &self.cancellables)
appPushManager.delegate = self
self.isInitialized.value = true
Log.i("[LocalPushManager] NEAppPushManager initialisation : enabled=\(String(describing: appPushManager.isEnabled)) ssids=\(String(describing: appPushManager.matchSSIDs))")
}
}
@objc func extensionIsActive() -> Bool {
return appPushManager?.isActive == true
}
private func applyConfig(coreConfig:Config) {
let appPushManager = self.appPushManager!
let ssids = coreConfig.getStringList(section: "local_push", key: "ssids", defaultList: []) // csv
let enabled = !ssids.isEmpty
appPushManager.isEnabled = enabled
appPushManager.matchSSIDs = ssids
appPushManager.providerConfiguration = [
"coreconfig": coreConfig.dump()
]
appPushManager.localizedDescription = NSLocalizedString("Local Push Manager", comment: "")
appPushManager.providerBundleIdentifier = localPushProviderBundleIdentifier
if (appPushManager.isEnabled) {
self.pushManagerIsActiveCancellable = NSObject.KeyValueObservingPublisher(object: appPushManager, keyPath: \.isActive, options: [.initial, .new])
.subscribe(self.pushManagerIsActiveSubject)
appPushManager.saveToPreferences { error in
if (error != nil) {
Log.e("[LocalPushManager] error saving Local Push preferences \(String(describing: error))")
} else {
Log.i("[LocalPushManager] NEAppPushManager saved : enabled=\(String(describing: appPushManager.isEnabled)) ssids=\(String(describing: appPushManager.matchSSIDs))")
}
}
} else {
pushManagerIsActiveSubject.send(false)
Log.i("[LocalPushManager] NEAppPushManager disabled.")
}
}
@objc func configureLocalPush(cCoreConfig:OpaquePointer) {
if (self.isInitialized.value != true ) {
self.isInitialized.observeOnce { _ in
self.applyConfig(coreConfig: Config.getSwiftObject(cObject: cCoreConfig))
}
} else {
applyConfig(coreConfig: Config.getSwiftObject(cObject: cCoreConfig))
}
}
func appPushManager(_ manager: NEAppPushManager, didReceiveIncomingCallWithUserInfo userInfo: [AnyHashable : Any] = [:]) {
// Call handling
}
@objc func addActiveCallBackObserver (action:@escaping(Bool) -> Void) {
isActive.readCurrentAndObserve { active in
action(active!)
}
}
}

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.app-push</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).LocalPushProvider</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-push-provider</string>
</array>
<key>com.apple.security.application-groups</key>
<array>
<string>group.org.linphone.phone.msgNotification</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1,132 @@
/*
* 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
import Combine
import NetworkExtension
import UserNotifications
import linphonesw
import os
let APP_GROUP_ID = "group.org.linphone.phone.msgNotification"
class LocalPushProvider: NEAppPushProvider, CoreDelegate {
var core: Core? = nil
let log = LoggingService.Instance
var logDelegate: LinphoneLoggingServiceManager!
var coreDelegateStub : CoreDelegateStub? = nil
let defaults = UserDefaults.init(suiteName: APP_GROUP_ID)
func createAndStartCore() {
guard let configString = providerConfiguration?["coreconfig"] as? String, let config = Config.newFromBuffer(buffer: configString) else {
self.log.error(message: "Unable to get core config through provider configuration")
return
}
// Ensure a separate UUID from app is used, use previously generated one or a new one if n/a.
if let uuid = defaults?.string(forKey: "misc_uuid") {
config.setString(section: "misc", key: "uuid", value: uuid)
} else {
config.cleanEntry(section: "misc", key: "uuid")
}
logDelegate = try! LinphoneLoggingServiceManager(config: config, log: log, domain: "LocalPushProvider")
core = try! Factory.Instance.createCoreWithConfig(config: config, systemContext: nil)
core?.autoIterateEnabled = false // 20ms auto-iterations are too frequent for NE, it gets killed by the OS. (limit of 150 wakeups per second over 300 seconds)
core!.addDelegate(delegate: self)
let timer = Timer(timeInterval: 0.5, target: self, selector: #selector(iterate), userInfo: nil, repeats: true) // 0.5 second
RunLoop.main.add(timer, forMode: .default)
try?core?.start()
core?.addDelegate(delegate: coreDelegateStub!)
log.message(message: "core started")
// Keep generated UUID to avoid re-creating one every time the NE is starting.
if (core!.config!.hasEntry(section: "misc", key: "uuid") != 0) {
defaults?.set(core!.config!.getString(section: "misc", key: "uuid", defaultString: ""), forKey: "misc_uuid")
self.log.message(message: "storing generated UUID \(String(describing: defaults?.string(forKey: "misc_uuid")))")
}
}
@objc func iterate() {
core?.iterate()
}
override init() {
super.init()
coreDelegateStub = CoreDelegateStub(
onMessageReceived: { (core:Core, chatRoom:ChatRoom, message:ChatMessage) -> Void in
self.showLocalNotification(message: message)
}
)
}
// MARK: - NEAppPushProvider Life Cycle
override func start() {
createAndStartCore()
}
override func stop(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
core?.stopAsync()
completionHandler()
}
override func handleTimerEvent() {
self.log.message(message: "Refreshing registers (handleTimerEvent)")
core?.refreshRegisters()
}
// MARK: - Notify User
let ignoredContentTypes = ["message/imdn+xml","application/im-iscomposing+xml"]
func showLocalNotification(message: ChatMessage) {
if (ignoredContentTypes.contains(message.contentType)) {
self.log.error(message: "Received unexpected content type.\(message.contentType)")
return
}
var messageContent = ""
if (message.hasConferenceInvitationContent()) {
messageContent = NSLocalizedString("📅 You are invited to a meeting", comment: "")
} else {
messageContent = message.hasTextContent() ? message.utf8Text : "🗻"
}
let fromAddr = message.chatRoom?.peerAddress?.asStringUriOnly()
var displayName = fromAddr?.getDisplayNameFromSipAddress(lc: core!, logger: log, groupId: APP_GROUP_ID)
displayName = displayName != nil ? displayName : message.chatRoom?.peerAddress?.displayName
displayName = displayName != nil && displayName?.isEmpty != true ? displayName :message.chatRoom?.peerAddress?.username
let content = UNMutableNotificationContent()
content.title = displayName!
content.body = messageContent
content.sound = .default
content.categoryIdentifier = "app_active"
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
self.log.error(message: "Error submitting local notification: \(error)")
return
}
self.log.message(message: "Local notification posted successfully")
}
}
}

View file

@ -64,6 +64,15 @@ target 'CallUITests' do
end
target 'LocalPushProvider' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
use_frameworks!
# Pods for CallUITests
all_pods
end
post_install do |installer|
system("sed 's/fileprivate let tableView =/public let tableView =/g' ./Pods/DropDown/DropDown/src/DropDown.swift > tmp.swift && mv -f tmp.swift ./Pods/DropDown/DropDown/src/DropDown.swift")
# Get the version of linphone-sdk

View file

@ -145,3 +145,16 @@ Any Linphone view is actually presented in the UICompositeView, with or without
The UICompositeView consists of 3 areas laid out vertically. From top to bottom: StatusBar, Content and TabBar.
The TabBar is usually the UIMainBar, which is used as a navigation controller: clicking on each of the buttons will trigger
a transition to another "view".
# Local Push Notifications
- application local push network extension
- requires local push entitlement
- enabled either inside settings/network from app UI
- by remote provisionning/configuration, section local_push, key ssids (CSV of SSIDs on which to enable)
- when extension is running under "connected" appears a "local push active" label
- provisionning profile need to be built on Apple console, the entitlement onboarding on profile is asked at last. (automatic provisionning/signing from Xcode will not pick it up - tested on 14.3.1)
- If deploying code from Xcode the extension must not be running, otherwise need device will need reboot to run it again. Just disabling wifi prior to deploying code from Xcode does the trick (14.3.1)
- use a unique uuid to avoid conflict with app core ([misc]uuid)
- use replica of app config

View file

@ -198,6 +198,30 @@
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
</dict>
<dict>
<key>Key</key>
<string>local_push</string>
<key>Title</key>
<string>Local push notifications</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>Key</key>
<string>local_push_ssids</string>
<key>Title</key>
<string>Enable on Wifi SSIDs:</string>
<key>Type</key>
<string>PSTextFieldSpecifier</string>
<key>AutocapitalizationType</key>
<string>None</string>
<key>AutocorrectionType</key>
<string>No</string>
<key>DefaultValue</key>
<string></string>
<key>IASKTextAlignment</key>
<string>IASKUITextAlignmentRight</string>
</dict>
</array>
</dict>
</plist>

View file

@ -30,5 +30,9 @@
<array>
<string>$(AppIdentifierPrefix)org.linphone.phone</string>
</array>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>app-push-provider</string>
</array>
</dict>
</plist>

File diff suppressed because it is too large Load diff

View file

@ -18,6 +18,7 @@
*/
import linphonesw
import os
#if USE_CRASHLYTICS
import Firebase
#endif
@ -43,6 +44,14 @@ class LinphoneLoggingServiceManager: LoggingServiceDelegate {
}
}
let levelToOSleLogLevel :[Int: OSLogType] =
[LogLevel.Debug.rawValue:.debug,
LogLevel.Trace.rawValue:.info,
LogLevel.Message.rawValue:.info,
LogLevel.Warning.rawValue:.error,
LogLevel.Error.rawValue:.error,
LogLevel.Fatal.rawValue:.fault];
func onLogMessageWritten(logService: LoggingService, domain: String, level: LogLevel, message: String) {
let levelStr: String
@ -66,6 +75,42 @@ class LinphoneLoggingServiceManager: LoggingServiceDelegate {
#if USE_CRASHLYTICS
Crashlytics.crashlytics().log("\(levelStr) [\(domain)] \(message)\n")
#endif
NSLog("\(levelStr) [\(domain)] \(message)\n")
if #available(iOS 10.0, *) {
os_log("%{public}@", type: levelToOSleLogLevel[level.rawValue] ?? .info,message)
} else {
NSLog("\(levelStr) [\(domain)] \(message)\n")
}
}
}
extension String {
func getDisplayNameFromSipAddress(lc:Core, logger:LoggingService, groupId:String) -> String? {
logger.message(message: "looking for display name for \(self)")
let defaults = UserDefaults.init(suiteName: groupId)
let addressBook = defaults?.dictionary(forKey: "addressBook")
if (addressBook == nil) {
logger.message(message: "address book not found in userDefaults")
return nil
}
var usePrefix = true;
if let account = lc.defaultAccount, let params = account.params {
usePrefix = params.useInternationalPrefixForCallsAndChats
}
if let simpleAddr = lc.interpretUrl(url: self, applyInternationalPrefix: usePrefix) {
simpleAddr.clean()
let nomalSipaddr = simpleAddr.asString()
if let displayName = addressBook?[nomalSipaddr] as? String {
logger.message(message: "display name for \(self): \(displayName)")
return displayName
}
}
logger.message(message: "display name for \(self) not found in userDefaults")
return nil
}
}

View file

@ -184,7 +184,7 @@ class NotificationService: UNNotificationServiceExtension {
let localUri = message.localAddr?.asStringUriOnly()
let peerUri = message.peerAddr?.asStringUriOnly()
let from: String
if let fromDisplayName = getDisplayNameFromSipAddress(sipAddr: message.fromAddr?.asStringUriOnly()) {
if let fromDisplayName = message.fromAddr?.asStringUriOnly().getDisplayNameFromSipAddress(lc: lc!, logger: NotificationService.log, groupId: APP_GROUP_ID) {
from = fromDisplayName
} else {
from = fromAddr!
@ -242,37 +242,4 @@ class NotificationService: UNNotificationServiceExtension {
return count
}
func getDisplayNameFromSipAddress(sipAddr: String?) -> String? {
if let sipAddr = sipAddr {
NotificationService.log.message(message: "looking for display name for \(sipAddr)")
if (sipAddr == "") { return nil }
let defaults = UserDefaults.init(suiteName: APP_GROUP_ID)
let addressBook = defaults?.dictionary(forKey: "addressBook")
if (addressBook == nil) {
NotificationService.log.message(message: "address book not found in userDefaults")
return nil
}
var usePrefix = true;
if let account = lc?.defaultAccount, let params = account.params {
usePrefix = params.useInternationalPrefixForCallsAndChats
}
if let simpleAddr = lc?.interpretUrl(url: sipAddr, applyInternationalPrefix: usePrefix) {
simpleAddr.clean()
let nomalSipaddr = simpleAddr.asString()
if let displayName = addressBook?[nomalSipaddr] as? String {
NotificationService.log.message(message: "display name for \(sipAddr): \(displayName)")
return displayName
}
}
NotificationService.log.message(message: "display name for \(sipAddr) not found in userDefaults")
return nil
}
return nil
}
}