ios: receive message in NSE (#742)

This commit is contained in:
Evgeny Poberezkin
2022-06-19 19:49:39 +01:00
committed by GitHub
parent c5c65f813b
commit 291096d87f
8 changed files with 132 additions and 181 deletions

View File

@@ -47,7 +47,7 @@ enum TerminalItem: Identifiable {
}
}
private func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
func beginBGTask(_ handler: (() -> Void)? = nil) -> (() -> Void) {
var id: UIBackgroundTaskIdentifier!
var running = true
let endTask = {
@@ -143,6 +143,20 @@ func apiStartChat() throws -> Bool {
}
}
func apiStopChat() throws {
let r = chatSendCmdSync(.apiStopChat)
switch r {
case .chatStopped: return
default: throw r
}
}
func apiSetAppPhase(appPhase: AgentPhase) {
let r = chatSendCmdSync(.apiSetAppPhase(appPhase: appPhase))
if case .cmdOk = r { return }
logger.error("apiSetAppPhase error: \(String(describing: r))")
}
func apiSetFilesFolder(filesFolder: String) throws {
let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder))
if case .cmdOk = r { return }
@@ -512,7 +526,7 @@ class ChatReceiver {
func receiveMsgLoop() async {
let msg = await chatRecvMsg()
self._lastMsgTime = .now
processReceivedMsg(msg)
await processReceivedMsg(msg)
if self.receiveMessages {
do { try await Task.sleep(nanoseconds: 7_500_000) }
catch { logger.error("receiveMsgLoop: Task.sleep error: \(error.localizedDescription)") }
@@ -528,9 +542,9 @@ class ChatReceiver {
}
}
func processReceivedMsg(_ res: ChatResponse) {
func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared
DispatchQueue.main.async {
await MainActor.run {
m.terminalItems.append(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
@@ -682,6 +696,8 @@ func processReceivedMsg(_ res: ChatResponse) {
m.callCommand = .end
// CallController.shared.reportCallRemoteEnded(call: call)
}
case let .appPhase(appPhase):
setAppState(AppState(appPhase: appPhase))
default:
logger.debug("unsupported event: \(res.responseType)")
}

View File

@@ -11,13 +11,6 @@ import SimpleXChat
let logger = Logger()
let machMessenger = MachMessenger(APP_MACH_PORT, callback: receivedNSEMachMessage)
func receivedNSEMachMessage(msgId: Int32, msg: String) -> String? {
logger.debug("MachMessenger: receivedNSEMachMessage \"\(msg)\" from NSE, replying")
return "reply from App to: \(msg)"
}
@main
struct SimpleXApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@@ -34,7 +27,6 @@ struct SimpleXApp: App {
UserDefaults.standard.register(defaults: appDefaults)
BGManager.shared.register()
NtfManager.shared.registerCategories()
machMessenger.start()
}
var body: some Scene {
@@ -50,20 +42,18 @@ struct SimpleXApp: App {
}
.onChange(of: scenePhase) { phase in
logger.debug("scenePhase \(String(describing: scenePhase))")
let res = machMessenger.sendMessageWithReply(NSE_MACH_PORT, msg: "App scenePhase changed to \(String(describing: scenePhase))")
logger.debug("MachMessenger \(String(describing: res), privacy: .public)")
setAppState(phase)
switch (phase) {
case .background:
pauseApp()
BGManager.shared.schedule()
if userAuthorized == true {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
machMessenger.stop()
case .active:
setAppState(.active)
apiSetAppPhase(appPhase: .active)
doAuthenticate = authenticationExpired()
machMessenger.start()
default:
break
}
@@ -71,6 +61,18 @@ struct SimpleXApp: App {
}
}
private func pauseApp() {
setAppState(.pausing)
apiSetAppPhase(appPhase: .paused)
let endTask = beginBGTask {
if getAppState() != .active {
setAppState(.suspending)
apiSetAppPhase(appPhase: .suspended)
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + maxTaskDuration, execute: endTask)
}
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30

View File

@@ -12,29 +12,28 @@ import SimpleXChat
let logger = Logger()
let machMessenger = MachMessenger(NSE_MACH_PORT, callback: receivedAppMachMessage)
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
logger.debug("NotificationService.didReceive")
machMessenger.start()
let res = machMessenger.sendMessageWithReply(APP_MACH_PORT, msg: "starting NSE didReceive")
logger.debug("MachMessenger \(String(describing: res), privacy: .public)")
if getAppState() != .background {
let appState = getAppState()
if appState.running {
contentHandler(request.content)
machMessenger.stop()
return
}
logger.debug("NotificationService: app is in the background")
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let _ = startChat() {
let userInfo = request.content.userInfo
if let ntfData = userInfo["notificationData"] as? [AnyHashable : Any],
let nonce = ntfData["nonce"] as? String,
let encNtfInfo = ntfData["message"] as? String,
let _ = startChat() {
apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo)
let content = receiveMessages()
contentHandler (content)
machMessenger.stop()
return
}
@@ -44,7 +43,6 @@ class NotificationService: UNNotificationServiceExtension {
contentHandler(bestAttemptContent)
}
machMessenger.stop()
}
override func serviceExtensionTimeWillExpire() {
@@ -145,3 +143,12 @@ func apiSetFilesFolder(filesFolder: String) throws {
throw r
}
func apiGetNtfMessage(nonce: String, encNtfInfo: String) {
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfMessages(connEntity, msgTs, ntfMessages) = r {
print(connEntity)
print(msgTs)
print(ntfMessages)
return
}
}

View File

@@ -15,6 +15,8 @@ public enum ChatCommand {
case showActiveUser
case createActiveUser(profile: Profile)
case startChat
case apiStopChat
case apiSetAppPhase(appPhase: AgentPhase)
case setFilesFolder(filesFolder: String)
case apiGetChats
case apiGetChat(type: ChatType, id: Int64)
@@ -25,6 +27,7 @@ public enum ChatCommand {
case apiVerifyToken(token: String, code: String, nonce: String)
case apiIntervalNofication(token: String, interval: Int)
case apiDeleteToken(token: String)
case apiGetNtfMessage(nonce: String, encNtfInfo: String)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case addContact
@@ -56,6 +59,8 @@ public enum ChatCommand {
case .showActiveUser: return "/u"
case let .createActiveUser(profile): return "/u \(profile.displayName) \(profile.fullName)"
case .startChat: return "/_start"
case .apiStopChat: return "/_stop"
case let .apiSetAppPhase(appPhase): return "/_app phase \(appPhase)"
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
case .apiGetChats: return "/_get chats pcc=on"
case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100"
@@ -68,6 +73,7 @@ public enum ChatCommand {
case let .apiVerifyToken(token, code, nonce): return "/_ntf verify apns \(token) \(code) \(nonce)"
case let .apiIntervalNofication(token, interval): return "/_ntf interval apns \(token) \(interval)"
case let .apiDeleteToken(token): return "/_ntf delete apns \(token)"
case let .apiGetNtfMessage(nonce, encNtfInfo): return "/_ntf message \(nonce) \(encNtfInfo)"
case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case .addContact: return "/connect"
@@ -101,6 +107,8 @@ public enum ChatCommand {
case .showActiveUser: return "showActiveUser"
case .createActiveUser: return "createActiveUser"
case .startChat: return "startChat"
case .apiStopChat: return "apiStopChat"
case .apiSetAppPhase: return "apiSetAppPhase"
case .setFilesFolder: return "setFilesFolder"
case .apiGetChats: return "apiGetChats"
case .apiGetChat: return "apiGetChat"
@@ -111,6 +119,7 @@ public enum ChatCommand {
case .apiVerifyToken: return "apiVerifyToken"
case .apiIntervalNofication: return "apiIntervalNofication"
case .apiDeleteToken: return "apiDeleteToken"
case .apiGetNtfMessage: return "apiGetNtfMessage"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .addContact: return "addContact"
@@ -156,6 +165,8 @@ public enum ChatResponse: Decodable, Error {
case activeUser(user: User)
case chatStarted
case chatRunning
case chatStopped
case appPhase(appPhase: AgentPhase)
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [String])
@@ -205,6 +216,7 @@ public enum ChatResponse: Decodable, Error {
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(contact: Contact)
case ntfTokenStatus(status: NtfTknStatus)
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
case newContactConnection(connection: PendingContactConnection)
case contactConnectionDeleted(connection: PendingContactConnection)
case cmdOk
@@ -218,6 +230,8 @@ public enum ChatResponse: Decodable, Error {
case .activeUser: return "activeUser"
case .chatStarted: return "chatStarted"
case .chatRunning: return "chatRunning"
case .chatStopped: return "chatStopped"
case .appPhase: return "appPhase"
case .apiChats: return "apiChats"
case .apiChat: return "apiChat"
case .userSMPServers: return "userSMPServers"
@@ -265,6 +279,7 @@ public enum ChatResponse: Decodable, Error {
case .callExtraInfo: return "callExtraInfo"
case .callEnded: return "callEnded"
case .ntfTokenStatus: return "ntfTokenStatus"
case .ntfMessages: return "ntfMessages"
case .newContactConnection: return "newContactConnection"
case .contactConnectionDeleted: return "contactConnectionDeleted"
case .cmdOk: return "cmdOk"
@@ -281,6 +296,8 @@ public enum ChatResponse: Decodable, Error {
case let .activeUser(user): return String(describing: user)
case .chatStarted: return noDetails
case .chatRunning: return noDetails
case .chatStopped: return noDetails
case let .appPhase(appPhase): return appPhase.rawValue
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers): return String(describing: smpServers)
@@ -328,6 +345,7 @@ public enum ChatResponse: Decodable, Error {
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
case let .callEnded(contact): return "contact: \(contact.id)"
case let .ntfTokenStatus(status): return String(describing: status)
case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"
case let .newContactConnection(connection): return String(describing: connection)
case let .contactConnectionDeleted(connection): return String(describing: connection)
case .cmdOk: return noDetails
@@ -346,6 +364,12 @@ struct ComposedMessage: Encodable {
var msgContent: MsgContent
}
public enum AgentPhase: String, Codable {
case active = "ACTIVE"
case paused = "PAUSED"
case suspended = "SUSPENDED"
}
public func decodeJSON<T: Decodable>(_ json: String) -> T? {
if let data = json.data(using: .utf8) {
return try? jsonDecoder.decode(T.self, from: data)

View File

@@ -9,169 +9,51 @@
import Foundation
import SwiftUI
let GROUP_DEFAULT_APP_IN_BACKGROUND = "appInBackground"
let GROUP_DEFAULT_APP_STATE = "appState"
let APP_GROUP_NAME = "group.chat.simplex.app"
public let NSE_MACH_PORT = "\(APP_GROUP_NAME).nse" as CFString
public let APP_MACH_PORT = "\(APP_GROUP_NAME).app" as CFString
func getGroupDefaults() -> UserDefaults? {
UserDefaults(suiteName: APP_GROUP_NAME)
}
public func setAppState(_ phase: ScenePhase) {
public enum AppState: String {
case active
case pausing
case paused
case suspending
case suspended
public init(appPhase: AgentPhase) {
switch appPhase {
case .active: self = .active
case .paused: self = .paused
case .suspended: self = .suspended
}
}
public var running: Bool {
switch self {
case .paused: return false
case .suspending: return false
case .suspended: return false
default: return true
}
}
}
public func setAppState(_ state: AppState) {
if let defaults = getGroupDefaults() {
defaults.set(phase == .background, forKey: GROUP_DEFAULT_APP_IN_BACKGROUND)
defaults.set(state.rawValue, forKey: GROUP_DEFAULT_APP_STATE)
defaults.synchronize()
}
}
public func getAppState() -> ScenePhase {
if let defaults = getGroupDefaults() {
if defaults.bool(forKey: GROUP_DEFAULT_APP_IN_BACKGROUND) {
return .background
}
public func getAppState() -> AppState {
if let defaults = getGroupDefaults(),
let rawValue = defaults.string(forKey: GROUP_DEFAULT_APP_STATE),
let state = AppState(rawValue: rawValue) {
return state
}
return .active
}
let MACH_SEND_TIMEOUT: CFTimeInterval = 1.0
let MACH_REPLY_TIMEOUT: CFTimeInterval = 1.0
public class MachMessenger {
public init(_ localPortName: CFString, callback: @escaping Callback) {
self.localPortName = localPortName
self.callback = callback
self.localPort = nil
}
var localPortName: CFString
var callback: Callback
var localPort: CFMessagePort?
public enum SendError: Error {
case sndTimeout
case rcvTimeout
case portInvalid
case sendError(Int32)
case msgError
}
public typealias Callback = (_ msgId: Int32, _ msg: String) -> String?
class CallbackInfo {
internal init(callback: @escaping MachMessenger.Callback) {
self.callback = callback
}
var callback: Callback
}
public static func sendError(_ code: Int32) -> SendError? {
switch code {
case kCFMessagePortSuccess: return nil
case kCFMessagePortSendTimeout: return .sndTimeout
case kCFMessagePortReceiveTimeout: return .rcvTimeout
case kCFMessagePortIsInvalid: return .portInvalid
case kCFMessagePortBecameInvalidError: return .portInvalid
default: return .sendError(code)
}
}
public func start() {
logger.debug("MachMessenger.start")
localPort = createLocalPort(localPortName, callback: callback)
}
public func stop() {
if let port = localPort {
logger.debug("MachMessenger.stop")
CFMessagePortInvalidate(port)
localPort = nil
}
}
public func sendMessage(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> SendError? {
logger.debug("MachMessenger.sendMessage")
if let port = createRemotePort(remotePortName) {
logger.debug("MachMessenger.sendMessage: sending...")
return sendMessage(port, msgId: msgId, msg: msg)
} else {
logger.debug("MachMessenger.sendMessage: no remote port")
return .portInvalid
}
}
public func sendMessageWithReply(_ remotePortName: CFString, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> {
logger.debug("MachMessenger.sendMessageWithReply")
if let port = createRemotePort(remotePortName) {
logger.debug("MachMessenger.sendMessageWithReply: sending...")
return sendMessageWithReply(port, msgId: msgId, msg: msg)
} else {
logger.debug("MachMessenger.sendMessageWithReply: no remote port")
return .failure(.portInvalid)
}
}
private func createLocalPort(_ portName: CFString, callback: @escaping Callback) -> CFMessagePort? {
logger.debug("MachMessenger.createLocalPort")
if let port = localPort { return port }
logger.debug("MachMessenger.createLocalPort: creating...")
var context = CFMessagePortContext()
context.version = 0
context.info = Unmanaged.passRetained(CallbackInfo(callback: callback)).toOpaque()
let callout: CFMessagePortCallBack = { port, msgId, msgData, info in
logger.debug("MachMessenger CFMessagePortCallBack called")
if let data = msgData,
let msg = String(data: data as Data, encoding: .utf8),
let info = info,
let resp = Unmanaged<CallbackInfo>.fromOpaque(info).takeUnretainedValue().callback(msgId, msg),
let respData = resp.data(using: .utf8) {
return Unmanaged.passRetained(respData as CFData)
}
return nil
}
return withUnsafeMutablePointer(to: &context) { cxt in
let port = CFMessagePortCreateLocal(kCFAllocatorDefault, portName, callout, cxt, nil)
CFMessagePortSetDispatchQueue(port, DispatchQueue.main);
localPort = port
logger.debug("MachMessenger.createLocalPort created: \(portName)")
return port
}
}
private func createRemotePort(_ portName: CFString) -> CFMessagePort? {
CFMessagePortCreateRemote(kCFAllocatorDefault, portName)
}
private func sendMessage(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> SendError? {
if let data = msg.data(using: .utf8) {
logger.debug("MachMessenger sendMessage")
let msgData = data as CFData
let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, 0, nil, nil)
logger.debug("MachMessenger sendMessage \(code)")
return MachMessenger.sendError(code)
}
return .msgError
}
private func sendMessageWithReply(_ remotePort: CFMessagePort, msgId: Int32 = 0, msg: String) -> Result<String?, SendError> {
if let data = msg.data(using: .utf8) {
let msgData = data as CFData
var respData: Unmanaged<CFData>? = nil
let code = CFMessagePortSendRequest(remotePort, msgId, msgData, MACH_SEND_TIMEOUT, MACH_REPLY_TIMEOUT, CFRunLoopMode.defaultMode.rawValue, &respData)
if let err = MachMessenger.sendError(code) {
return .failure(err)
} else if let data = respData?.takeUnretainedValue(),
let resp = String(data: data as Data, encoding: .utf8) {
return .success(resp)
} else {
return .success(nil)
}
}
return .failure(.msgError)
}
}

View File

@@ -264,6 +264,9 @@ public struct Connection: Decodable {
)
}
public struct UserContact: Decodable {
}
public struct UserContactRequest: Decodable, NamedChat {
var contactRequestId: Int64
var localDisplayName: ContactName
@@ -437,6 +440,18 @@ public struct MemberSubError: Decodable {
var memberError: ChatError
}
public enum ConnectionEntity: Decodable {
case rcvDirectMsgConnection(entityConnection: Connection, contact: Contact?)
case rcvGroupMsgConnection(entityConnection: Connection, groupInfo: GroupInfo, groupMember: GroupMember)
case sndFileConnection(entityConnection: Connection, sndFileTransfer: SndFileTransfer)
case rcvFileConnection(entityConnection: Connection, rcvFileTransfer: RcvFileTransfer)
case userContactConnection(entityConnection: Connection, userContact: UserContact)
}
public struct NtfMsgInfo: Decodable {
}
public struct AChatItem: Decodable {
public var chatInfo: ChatInfo
public var chatItem: ChatItem
@@ -901,6 +916,10 @@ public struct SndFileTransfer: Decodable {
}
public struct RcvFileTransfer: Decodable {
}
public struct FileTransferMeta: Decodable {
}

View File

@@ -1096,6 +1096,7 @@ processAgentMessage Nothing _ _ = throwChatError CENoActiveUser
processAgentMessage (Just User {userId}) "" agentMessage = case agentMessage of
DOWN srv conns -> serverEvent srv conns CRContactsDisconnected "disconnected"
UP srv conns -> serverEvent srv conns CRContactsSubscribed "connected"
PHASE phase -> toView $ CRAppPhase phase
_ -> pure ()
where
serverEvent srv@SMP.ProtocolServer {host, port} conns event str = do

View File

@@ -33,7 +33,7 @@ import Simplex.Chat.Call
import Simplex.Chat.Types
import Simplex.Chat.Util (safeDecodeUtf8)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (fromTextField_, sumTypeJSON)
import Simplex.Messaging.Parsers (fromTextField_, fstToLower, sumTypeJSON)
import Simplex.Messaging.Util (eitherToMaybe, (<$?>))
data ConnectionEntity
@@ -45,8 +45,8 @@ data ConnectionEntity
deriving (Eq, Show, Generic)
instance ToJSON ConnectionEntity where
toJSON = J.genericToJSON $ sumTypeJSON id
toEncoding = J.genericToEncoding $ sumTypeJSON id
toJSON = J.genericToJSON $ sumTypeJSON fstToLower
toEncoding = J.genericToEncoding $ sumTypeJSON fstToLower
updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity
updateEntityConnStatus connEntity connStatus = case connEntity of