ios, core: better notifications processing to avoid contention for database (#3485)
* core: forward notifications about message processing (for iOS notifications) * simplexmq * the option to keep database key, to allow re-opening the database * export new init with keepKey and reopen DB api * stop remote ctrl when suspending chat * ios: close/re-open db on suspend/activate * allow activating chat without restoring (for NSE) * update NSE to suspend/activate (does not work) * simplexmq * suspend chat and close database when last notification in the process is processed * stop reading notifications on message markers * replace async stream with cancellable concurrent queue * better synchronization of app and NSE * remove outside of task * remove unused var * whitespace * more debug logging, handle cancelled read after dequeue * comments * more comments
This commit is contained in:
committed by
GitHub
parent
2f7632a70f
commit
d3059afc99
@@ -80,7 +80,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
}
|
||||
} else if let checkMessages = ntfData["checkMessages"] as? Bool, checkMessages {
|
||||
logger.debug("AppDelegate: didReceiveRemoteNotification: checkMessages")
|
||||
if appStateGroupDefault.get().inactive && m.ntfEnablePeriodic {
|
||||
if m.ntfEnablePeriodic && allowBackgroundRefresh() {
|
||||
receiveMessages(completionHandler)
|
||||
} else {
|
||||
completionHandler(.noData)
|
||||
|
||||
@@ -15,7 +15,7 @@ private let receiveTaskId = "chat.simplex.app.receive"
|
||||
// TCP timeout + 2 sec
|
||||
private let waitForMessages: TimeInterval = 6
|
||||
|
||||
private let bgRefreshInterval: TimeInterval = 450
|
||||
private let bgRefreshInterval: TimeInterval = 600
|
||||
|
||||
private let maxTimerCount = 9
|
||||
|
||||
@@ -55,7 +55,7 @@ class BGManager {
|
||||
}
|
||||
logger.debug("BGManager.handleRefresh")
|
||||
schedule()
|
||||
if appStateGroupDefault.get().inactive {
|
||||
if allowBackgroundRefresh() {
|
||||
let completeRefresh = completionHandler {
|
||||
task.setTaskCompleted(success: true)
|
||||
}
|
||||
@@ -92,18 +92,19 @@ class BGManager {
|
||||
DispatchQueue.main.async {
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
setAppState(.bgRefresh)
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
activateChat(appState: .bgRefresh)
|
||||
if m.currentUser == nil {
|
||||
completeReceiving("no current user")
|
||||
return
|
||||
}
|
||||
logger.debug("BGManager.receiveMessages: starting chat")
|
||||
activateChat(appState: .bgRefresh)
|
||||
let cr = ChatReceiver()
|
||||
self.chatReceiver = cr
|
||||
cr.start()
|
||||
|
||||
@@ -105,11 +105,13 @@ final class ChatModel: ObservableObject {
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
var ntfEnableLocal: Bool {
|
||||
notificationMode == .off || ntfEnableLocalGroupDefault.get()
|
||||
true
|
||||
// notificationMode == .off || ntfEnableLocalGroupDefault.get()
|
||||
}
|
||||
|
||||
var ntfEnablePeriodic: Bool {
|
||||
notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
|
||||
notificationMode != .off
|
||||
// notificationMode == .periodic || ntfEnablePeriodicGroupDefault.get()
|
||||
}
|
||||
|
||||
var activeRemoteCtrl: Bool {
|
||||
|
||||
@@ -228,7 +228,8 @@ func apiStopChat() async throws {
|
||||
}
|
||||
|
||||
func apiActivateChat() {
|
||||
let r = chatSendCmdSync(.apiActivateChat)
|
||||
chatReopenStore()
|
||||
let r = chatSendCmdSync(.apiActivateChat(restoreChat: true))
|
||||
if case .cmdOk = r { return }
|
||||
logger.error("apiActivateChat error: \(String(describing: r))")
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ let bgSuspendTimeout: Int = 5 // seconds
|
||||
|
||||
let terminationTimeout: Int = 3 // seconds
|
||||
|
||||
let activationDelay: Double = 1.5 // seconds
|
||||
|
||||
private func _suspendChat(timeout: Int) {
|
||||
// this is a redundant check to prevent logical errors, like the one fixed in this PR
|
||||
let state = appStateGroupDefault.get()
|
||||
@@ -47,8 +49,6 @@ func suspendBgRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
private var terminating = false
|
||||
|
||||
func terminateChat() {
|
||||
logger.debug("terminateChat")
|
||||
suspendLockQueue.sync {
|
||||
@@ -64,7 +64,6 @@ func terminateChat() {
|
||||
case .stopped:
|
||||
chatCloseStore()
|
||||
default:
|
||||
terminating = true
|
||||
// the store will be closed in _chatSuspended when event is received
|
||||
_suspendChat(timeout: terminationTimeout)
|
||||
}
|
||||
@@ -85,14 +84,17 @@ private func _chatSuspended() {
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.stop()
|
||||
}
|
||||
if terminating {
|
||||
chatCloseStore()
|
||||
chatCloseStore()
|
||||
}
|
||||
|
||||
func setAppState(_ appState: AppState) {
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
}
|
||||
}
|
||||
|
||||
func activateChat(appState: AppState = .active) {
|
||||
logger.debug("DEBUGGING: activateChat")
|
||||
terminating = false
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(appState)
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
@@ -101,7 +103,6 @@ func activateChat(appState: AppState = .active) {
|
||||
}
|
||||
|
||||
func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
terminating = false
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
do {
|
||||
@@ -113,16 +114,32 @@ func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
}
|
||||
}
|
||||
|
||||
func startChatAndActivate() {
|
||||
terminating = false
|
||||
func startChatAndActivate(dispatchQueue: DispatchQueue = DispatchQueue.main, _ completion: @escaping () -> Void) {
|
||||
logger.debug("DEBUGGING: startChatAndActivate")
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
logger.debug("DEBUGGING: startChatAndActivate: after ChatReceiver.shared.start")
|
||||
}
|
||||
if .active != appStateGroupDefault.get() {
|
||||
if .active == appStateGroupDefault.get() {
|
||||
completion()
|
||||
} else if nseStateGroupDefault.get().inactive {
|
||||
activate()
|
||||
} else {
|
||||
suspendLockQueue.sync {
|
||||
appStateGroupDefault.set(.activating)
|
||||
}
|
||||
// TODO can be replaced with Mach messenger to notify the NSE to terminate and continue after reply, with timeout
|
||||
dispatchQueue.asyncAfter(deadline: .now() + activationDelay) {
|
||||
if appStateGroupDefault.get() == .activating {
|
||||
activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activate() {
|
||||
logger.debug("DEBUGGING: startChatAndActivate: before activateChat")
|
||||
activateChat()
|
||||
completion()
|
||||
logger.debug("DEBUGGING: startChatAndActivate: after activateChat")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,15 +77,16 @@ struct SimpleXApp: App {
|
||||
case .active:
|
||||
CallController.shared.shouldSuspendChat = false
|
||||
let appState = appStateGroupDefault.get()
|
||||
startChatAndActivate()
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||
updateCallInvitations()
|
||||
startChatAndActivate {
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||
updateCallInvitations()
|
||||
}
|
||||
}
|
||||
doAuthenticate = authenticationExpired()
|
||||
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
|
||||
}
|
||||
doAuthenticate = authenticationExpired()
|
||||
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -155,31 +155,32 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
initChatAndMigrate(refreshInvitations: false)
|
||||
}
|
||||
startChatAndActivate()
|
||||
shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
_ = try? justRefreshCallInvitations()
|
||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||
// Extract the call information from the push notification payload
|
||||
let m = ChatModel.shared
|
||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
let invitation = m.callInvitations[contactId] {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
startChatAndActivate(dispatchQueue: DispatchQueue.global()) {
|
||||
self.shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
_ = try? justRefreshCallInvitations()
|
||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||
// Extract the call information from the push notification payload
|
||||
let m = ChatModel.shared
|
||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
let invitation = m.callInvitations[contactId] {
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = self.cxCallUpdate(invitation: invitation)
|
||||
self.provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
}
|
||||
// Tell PushKit that the notification is handled.
|
||||
completion()
|
||||
}
|
||||
// Tell PushKit that the notification is handled.
|
||||
completion()
|
||||
} else {
|
||||
self.reportExpiredCall(update: update, completion)
|
||||
}
|
||||
} else {
|
||||
reportExpiredCall(update: update, completion)
|
||||
self.reportExpiredCall(payload: payload, completion)
|
||||
}
|
||||
} else {
|
||||
reportExpiredCall(payload: payload, completion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
64
apps/ios/SimpleX NSE/ConcurrentQueue.swift
Normal file
64
apps/ios/SimpleX NSE/ConcurrentQueue.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// ConcurrentQueue.swift
|
||||
// SimpleX NSE
|
||||
//
|
||||
// Created by Evgeny on 08/12/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DequeueElement<T> {
|
||||
var elementId: UUID?
|
||||
var task: Task<T?, Never>
|
||||
}
|
||||
|
||||
class ConcurrentQueue<T> {
|
||||
private var queue: [T] = []
|
||||
private var queueLock = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.concurrent-queue.lock.\(UUID())")
|
||||
private var continuations = [(elementId: UUID, continuation: CheckedContinuation<T?, Never>)]()
|
||||
|
||||
func enqueue(_ el: T) {
|
||||
resumeContinuation(el) { self.queue.append(el) }
|
||||
}
|
||||
|
||||
func frontEnqueue(_ el: T) {
|
||||
resumeContinuation(el) { self.queue.insert(el, at: 0) }
|
||||
}
|
||||
|
||||
private func resumeContinuation(_ el: T, add: @escaping () -> Void) {
|
||||
queueLock.sync {
|
||||
if let (_, cont) = continuations.first {
|
||||
continuations.remove(at: 0)
|
||||
cont.resume(returning: el)
|
||||
} else {
|
||||
add()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dequeue() -> DequeueElement<T> {
|
||||
queueLock.sync {
|
||||
if queue.isEmpty {
|
||||
let elementId = UUID()
|
||||
let task = Task {
|
||||
await withCheckedContinuation { cont in
|
||||
continuations.append((elementId, cont))
|
||||
}
|
||||
}
|
||||
return DequeueElement(elementId: elementId, task: task)
|
||||
} else {
|
||||
let el = queue.remove(at: 0)
|
||||
return DequeueElement(task: Task { el })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cancelDequeue(_ elementId: UUID) {
|
||||
queueLock.sync {
|
||||
let cancelled = continuations.filter { $0.elementId == elementId }
|
||||
continuations.removeAll { $0.elementId == elementId }
|
||||
cancelled.forEach { $0.continuation.resume(returning: nil) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,68 +14,167 @@ import SimpleXChat
|
||||
|
||||
let logger = Logger()
|
||||
|
||||
let suspendingDelay: UInt64 = 2_000_000_000
|
||||
let suspendingDelay: UInt64 = 2_500_000_000
|
||||
|
||||
typealias NtfStream = AsyncStream<NSENotification>
|
||||
let nseSuspendTimeout: Int = 10
|
||||
|
||||
typealias NtfStream = ConcurrentQueue<NSENotification>
|
||||
|
||||
// Notifications are delivered via concurrent queues, as they are all received from chat controller in a single loop that
|
||||
// writes to ConcurrentQueue and when notification is processed, the instance of Notification service extension reads from the queue.
|
||||
// One queue per connection (entity) is used.
|
||||
// The concurrent queues allow for read cancellation, to ensure that notifications are not lost in case the next the current thread completes
|
||||
// before expected notification is read (multiple notifications can be expected, because one notification can be delivered for several messages.
|
||||
actor PendingNtfs {
|
||||
static let shared = PendingNtfs()
|
||||
private var ntfStreams: [String: NtfStream] = [:]
|
||||
private var ntfConts: [String: NtfStream.Continuation] = [:]
|
||||
|
||||
func createStream(_ id: String) {
|
||||
logger.debug("PendingNtfs.createStream: \(id, privacy: .public)")
|
||||
if ntfStreams.index(forKey: id) == nil {
|
||||
ntfStreams[id] = AsyncStream { cont in
|
||||
ntfConts[id] = cont
|
||||
logger.debug("PendingNtfs.createStream: store continuation")
|
||||
}
|
||||
func createStream(_ id: String) async {
|
||||
logger.debug("NotificationService PendingNtfs.createStream: \(id, privacy: .public)")
|
||||
if ntfStreams[id] == nil {
|
||||
ntfStreams[id] = ConcurrentQueue()
|
||||
logger.debug("NotificationService PendingNtfs.createStream: created ConcurrentQueue")
|
||||
}
|
||||
}
|
||||
|
||||
func readStream(_ id: String, for nse: NotificationService, msgCount: Int = 1, showNotifications: Bool) async {
|
||||
logger.debug("PendingNtfs.readStream: \(id, privacy: .public) \(msgCount, privacy: .public)")
|
||||
func readStream(_ id: String, for nse: NotificationService, ntfInfo: NtfMessages) async {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: \(id, privacy: .public) \(ntfInfo.ntfMessages.count, privacy: .public)")
|
||||
if !ntfInfo.user.showNotifications {
|
||||
nse.setBestAttemptNtf(.empty)
|
||||
}
|
||||
if let s = ntfStreams[id] {
|
||||
logger.debug("PendingNtfs.readStream: has stream")
|
||||
var rcvCount = max(1, msgCount)
|
||||
for await ntf in s {
|
||||
nse.setBestAttemptNtf(showNotifications ? ntf : .empty)
|
||||
rcvCount -= 1
|
||||
if rcvCount == 0 || ntf.categoryIdentifier == ntfCategoryCallInvitation { break }
|
||||
logger.debug("NotificationService PendingNtfs.readStream: has stream")
|
||||
var expected = Set(ntfInfo.ntfMessages.map { $0.msgId })
|
||||
logger.debug("NotificationService PendingNtfs.readStream: expecting: \(expected, privacy: .public)")
|
||||
var readCancelled = false
|
||||
var dequeued: DequeueElement<NSENotification>?
|
||||
nse.cancelRead = {
|
||||
readCancelled = true
|
||||
if let elementId = dequeued?.elementId {
|
||||
s.cancelDequeue(elementId)
|
||||
}
|
||||
}
|
||||
logger.debug("PendingNtfs.readStream: exiting")
|
||||
while !readCancelled {
|
||||
dequeued = s.dequeue()
|
||||
if let ntf = await dequeued?.task.value {
|
||||
if readCancelled {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: read cancelled, put ntf to queue front")
|
||||
s.frontEnqueue(ntf)
|
||||
break
|
||||
} else if case let .msgInfo(info) = ntf {
|
||||
let found = expected.remove(info.msgId)
|
||||
if found != nil {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: msgInfo, last: \(expected.isEmpty, privacy: .public)")
|
||||
if expected.isEmpty { break }
|
||||
} else if let msgTs = ntfInfo.msgTs, info.msgTs > msgTs {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: unexpected msgInfo")
|
||||
s.frontEnqueue(ntf)
|
||||
break
|
||||
}
|
||||
} else if ntfInfo.user.showNotifications {
|
||||
logger.debug("NotificationService PendingNtfs.readStream: setting best attempt")
|
||||
nse.setBestAttemptNtf(ntf)
|
||||
if ntf.isCallInvitation { break }
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
nse.cancelRead = nil
|
||||
logger.debug("NotificationService PendingNtfs.readStream: exiting")
|
||||
}
|
||||
}
|
||||
|
||||
func writeStream(_ id: String, _ ntf: NSENotification) {
|
||||
logger.debug("PendingNtfs.writeStream: \(id, privacy: .public)")
|
||||
if let cont = ntfConts[id] {
|
||||
logger.debug("PendingNtfs.writeStream: writing ntf")
|
||||
cont.yield(ntf)
|
||||
func writeStream(_ id: String, _ ntf: NSENotification) async {
|
||||
logger.debug("NotificationService PendingNtfs.writeStream: \(id, privacy: .public)")
|
||||
if let s = ntfStreams[id] {
|
||||
logger.debug("NotificationService PendingNtfs.writeStream: writing ntf")
|
||||
s.enqueue(ntf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current implementation assumes concurrent notification delivery and uses semaphores
|
||||
// to process only one notification per connection (entity) at a time.
|
||||
class NtfStreamSemaphores {
|
||||
static let shared = NtfStreamSemaphores()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-semaphores.lock")
|
||||
private var semaphores: [String: DispatchSemaphore] = [:]
|
||||
|
||||
func waitForStream(_ id: String) {
|
||||
streamSemaphore(id, value: 0)?.wait()
|
||||
}
|
||||
|
||||
func signalStreamReady(_ id: String) {
|
||||
streamSemaphore(id, value: 1)?.signal()
|
||||
}
|
||||
|
||||
// this function returns nil if semaphore is just created, so passed value shoud be coordinated with the desired end value of the semaphore
|
||||
private func streamSemaphore(_ id: String, value: Int) -> DispatchSemaphore? {
|
||||
NtfStreamSemaphores.queue.sync {
|
||||
if let s = semaphores[id] {
|
||||
return s
|
||||
} else {
|
||||
semaphores[id] = DispatchSemaphore(value: value)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NSENotification {
|
||||
case nse(notification: UNMutableNotificationContent)
|
||||
case callkit(invitation: RcvCallInvitation)
|
||||
case nse(UNMutableNotificationContent)
|
||||
case callkit(RcvCallInvitation)
|
||||
case empty
|
||||
case msgInfo(NtfMsgInfo)
|
||||
|
||||
var categoryIdentifier: String? {
|
||||
var isCallInvitation: Bool {
|
||||
switch self {
|
||||
case let .nse(ntf): return ntf.categoryIdentifier
|
||||
case .callkit: return ntfCategoryCallInvitation
|
||||
case .empty: return nil
|
||||
case let .nse(ntf): ntf.categoryIdentifier == ntfCategoryCallInvitation
|
||||
case .callkit: true
|
||||
case .empty: false
|
||||
case .msgInfo: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once the last thread in the process completes processing chat controller is suspended, and the database is closed, to avoid
|
||||
// background crashes and contention for database with the application (both UI and background fetch triggered either on schedule
|
||||
// or when background notification is received.
|
||||
class NSEThreads {
|
||||
static let shared = NSEThreads()
|
||||
private static let queue = DispatchQueue(label: "chat.simplex.app.SimpleX-NSE.notification-threads.lock")
|
||||
private var threads: Set<UUID> = []
|
||||
|
||||
func startThread() -> UUID {
|
||||
NSEThreads.queue.sync {
|
||||
let (_, t) = threads.insert(UUID())
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func endThread(_ t: UUID) -> Bool {
|
||||
NSEThreads.queue.sync {
|
||||
let t_ = threads.remove(t)
|
||||
return t_ != nil && threads.isEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notification service extension creates a new instance of the class and calls didReceive for each notification.
|
||||
// Each didReceive is called in its own thread, but multiple calls can be made in one process, and, empirically, there is never
|
||||
// more than one process for notification service extension.
|
||||
// Soon after notification service delivers the last notification it is either suspended or terminated.
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptNtf: NSENotification?
|
||||
var badgeCount: Int = 0
|
||||
var threadId: UUID?
|
||||
var receiveEntityId: String?
|
||||
var cancelRead: (() -> Void)?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
threadId = NSEThreads.shared.startThread()
|
||||
logger.debug("DEBUGGING: NotificationService.didReceive")
|
||||
if let ntf = request.content.mutableCopy() as? UNMutableNotificationContent {
|
||||
setBestAttemptNtf(ntf)
|
||||
@@ -93,7 +192,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
setBadgeCount()
|
||||
Task {
|
||||
var state = appState
|
||||
for _ in 1...5 {
|
||||
for _ in 1...6 {
|
||||
_ = try await Task.sleep(nanoseconds: suspendingDelay)
|
||||
state = appStateGroupDefault.get()
|
||||
if state == .suspended || state != .suspending { break }
|
||||
@@ -123,24 +222,28 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let encNtfInfo = ntfData["message"] as? String,
|
||||
let dbStatus = startChat() {
|
||||
if case .ok = dbStatus,
|
||||
let ntfMsgInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)")
|
||||
if let connEntity = ntfMsgInfo.connEntity {
|
||||
let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) {
|
||||
logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo), privacy: .public)")
|
||||
if let connEntity = ntfInfo.connEntity_ {
|
||||
setBestAttemptNtf(
|
||||
ntfMsgInfo.ntfsEnabled
|
||||
? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity))
|
||||
ntfInfo.ntfsEnabled
|
||||
? .nse(createConnectionEventNtf(ntfInfo.user, connEntity))
|
||||
: .empty
|
||||
)
|
||||
if let id = connEntity.id {
|
||||
Task {
|
||||
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
|
||||
await PendingNtfs.shared.createStream(id)
|
||||
await PendingNtfs.shared.readStream(id, for: self, msgCount: ntfMsgInfo.ntfMessages.count, showNotifications: ntfMsgInfo.user.showNotifications)
|
||||
deliverBestAttemptNtf()
|
||||
receiveEntityId = id
|
||||
NtfStreamSemaphores.shared.waitForStream(id)
|
||||
if receiveEntityId != nil {
|
||||
Task {
|
||||
logger.debug("NotificationService: receiveNtfMessages: in Task, connEntity id \(id, privacy: .public)")
|
||||
await PendingNtfs.shared.createStream(id)
|
||||
await PendingNtfs.shared.readStream(id, for: self, ntfInfo: ntfInfo)
|
||||
deliverBestAttemptNtf()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
setBestAttemptNtf(createErrorNtf(dbStatus))
|
||||
}
|
||||
@@ -159,14 +262,14 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
func setBestAttemptNtf(_ ntf: UNMutableNotificationContent) {
|
||||
setBestAttemptNtf(.nse(notification: ntf))
|
||||
setBestAttemptNtf(.nse(ntf))
|
||||
}
|
||||
|
||||
func setBestAttemptNtf(_ ntf: NSENotification) {
|
||||
logger.debug("NotificationService.setBestAttemptNtf")
|
||||
if case let .nse(notification) = ntf {
|
||||
notification.badge = badgeCount as NSNumber
|
||||
bestAttemptNtf = .nse(notification: notification)
|
||||
bestAttemptNtf = .nse(notification)
|
||||
} else {
|
||||
bestAttemptNtf = ntf
|
||||
}
|
||||
@@ -174,9 +277,33 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private func deliverBestAttemptNtf() {
|
||||
logger.debug("NotificationService.deliverBestAttemptNtf")
|
||||
if let cancel = cancelRead {
|
||||
cancelRead = nil
|
||||
cancel()
|
||||
}
|
||||
if let id = receiveEntityId {
|
||||
receiveEntityId = nil
|
||||
NtfStreamSemaphores.shared.signalStreamReady(id)
|
||||
}
|
||||
if let t = threadId {
|
||||
threadId = nil
|
||||
if NSEThreads.shared.endThread(t) {
|
||||
suspendChat(nseSuspendTimeout)
|
||||
}
|
||||
}
|
||||
if let handler = contentHandler, let ntf = bestAttemptNtf {
|
||||
contentHandler = nil
|
||||
bestAttemptNtf = nil
|
||||
let deliver: (UNMutableNotificationContent?) -> Void = { ntf in
|
||||
let useNtf = if let ntf = ntf {
|
||||
appStateGroupDefault.get().running ? UNMutableNotificationContent() : ntf
|
||||
} else {
|
||||
UNMutableNotificationContent()
|
||||
}
|
||||
handler(useNtf)
|
||||
}
|
||||
switch ntf {
|
||||
case let .nse(content): handler(content)
|
||||
case let .nse(content): deliver(content)
|
||||
case let .callkit(invitation):
|
||||
CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
"displayName": invitation.contact.displayName,
|
||||
@@ -184,33 +311,71 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
"media": invitation.callType.media.rawValue
|
||||
]) { error in
|
||||
if error == nil {
|
||||
handler(UNMutableNotificationContent())
|
||||
deliver(nil)
|
||||
} else {
|
||||
logger.debug("reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
|
||||
handler(createCallInvitationNtf(invitation))
|
||||
logger.debug("NotificationService reportNewIncomingVoIPPushPayload success to CallController for \(invitation.contact.id)")
|
||||
deliver(createCallInvitationNtf(invitation))
|
||||
}
|
||||
}
|
||||
case .empty: handler(UNMutableNotificationContent())
|
||||
case .empty: deliver(nil) // used to mute notifications that did not unsubscribe yet
|
||||
case .msgInfo: deliver(nil) // unreachable, the best attempt is never set to msgInfo
|
||||
}
|
||||
bestAttemptNtf = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var chatStarted = false
|
||||
var networkConfig: NetCfg = getNetCfg()
|
||||
var xftpConfig: XFTPFileConfig? = getXFTPCfg()
|
||||
class NSEChatState {
|
||||
static let shared = NSEChatState()
|
||||
private var value_ = NSEState.created
|
||||
|
||||
var value: NSEState {
|
||||
value_
|
||||
}
|
||||
|
||||
func set(_ state: NSEState) {
|
||||
nseStateGroupDefault.set(state)
|
||||
value_ = state
|
||||
}
|
||||
|
||||
init() {
|
||||
set(.created)
|
||||
}
|
||||
}
|
||||
|
||||
var receiverStarted = false
|
||||
let startLock = DispatchSemaphore(value: 1)
|
||||
let suspendLock = DispatchSemaphore(value: 1)
|
||||
var networkConfig: NetCfg = getNetCfg()
|
||||
let xftpConfig: XFTPFileConfig? = getXFTPCfg()
|
||||
|
||||
// startChat uses semaphore startLock to ensure that only one didReceive thread can start chat controller
|
||||
// Subsequent calls to didReceive will be waiting on semaphore and won't start chat again, as it will be .active
|
||||
func startChat() -> DBMigrationResult? {
|
||||
logger.debug("NotificationService: startChat")
|
||||
if case .active = NSEChatState.shared.value { return .ok }
|
||||
|
||||
startLock.wait()
|
||||
defer { startLock.signal() }
|
||||
|
||||
return switch NSEChatState.shared.value {
|
||||
case .created: doStartChat()
|
||||
case .active: .ok
|
||||
case .suspending: activateChat()
|
||||
case .suspended: activateChat()
|
||||
}
|
||||
}
|
||||
|
||||
func doStartChat() -> DBMigrationResult? {
|
||||
logger.debug("NotificationService: doStartChat")
|
||||
hs_init(0, nil)
|
||||
if chatStarted { return .ok }
|
||||
let (_, dbStatus) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation())
|
||||
if dbStatus != .ok {
|
||||
resetChatCtrl()
|
||||
NSEChatState.shared.set(.created)
|
||||
return dbStatus
|
||||
}
|
||||
if let user = apiGetActiveUser() {
|
||||
logger.debug("active user \(String(describing: user))")
|
||||
logger.debug("NotificationService active user \(String(describing: user))")
|
||||
do {
|
||||
try setNetworkConfig(networkConfig)
|
||||
try apiSetTempFolder(tempFolder: getTempFilesDirectory().path)
|
||||
@@ -218,32 +383,102 @@ func startChat() -> DBMigrationResult? {
|
||||
try setXFTPConfig(xftpConfig)
|
||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||
let justStarted = try apiStartChat()
|
||||
chatStarted = true
|
||||
NSEChatState.shared.set(.active)
|
||||
if justStarted {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
Task { await receiveMessages() }
|
||||
Task {
|
||||
if !receiverStarted {
|
||||
receiverStarted = true
|
||||
await receiveMessages()
|
||||
}
|
||||
}
|
||||
}
|
||||
return .ok
|
||||
} catch {
|
||||
logger.error("NotificationService startChat error: \(responseError(error), privacy: .public)")
|
||||
}
|
||||
} else {
|
||||
logger.debug("no active user")
|
||||
logger.debug("NotificationService: no active user")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func activateChat() -> DBMigrationResult? {
|
||||
logger.debug("NotificationService: activateChat")
|
||||
let state = NSEChatState.shared.value
|
||||
NSEChatState.shared.set(.active)
|
||||
if apiActivateChat() {
|
||||
logger.debug("NotificationService: activateChat: after apiActivateChat")
|
||||
return .ok
|
||||
} else {
|
||||
NSEChatState.shared.set(state)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// suspendChat uses semaphore suspendLock to ensure that only one suspension can happen.
|
||||
func suspendChat(_ timeout: Int) {
|
||||
logger.debug("NotificationService: suspendChat")
|
||||
let state = NSEChatState.shared.value
|
||||
if !state.canSuspend {
|
||||
logger.error("NotificationService suspendChat called, current state: \(state.rawValue, privacy: .public)")
|
||||
} else {
|
||||
suspendLock.wait()
|
||||
defer { suspendLock.signal() }
|
||||
|
||||
NSEChatState.shared.set(.suspending)
|
||||
if apiSuspendChat(timeoutMicroseconds: timeout * 1000000) {
|
||||
logger.debug("NotificationService: activateChat: after apiActivateChat")
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + Double(timeout) + 1, execute: chatSuspended)
|
||||
} else {
|
||||
NSEChatState.shared.set(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func chatSuspended() {
|
||||
logger.debug("NotificationService chatSuspended")
|
||||
if case .suspending = NSEChatState.shared.value {
|
||||
NSEChatState.shared.set(.suspended)
|
||||
chatCloseStore()
|
||||
}
|
||||
}
|
||||
|
||||
// A single loop is used per Notification service extension process to receive and process all messages depending on the NSE state
|
||||
// If the extension is not active yet, or suspended/suspending, or the app is running, the notifications will no be received.
|
||||
func receiveMessages() async {
|
||||
logger.debug("NotificationService receiveMessages")
|
||||
while true {
|
||||
updateNetCfg()
|
||||
switch NSEChatState.shared.value {
|
||||
case .created: await delayWhenInactive()
|
||||
case .active:
|
||||
if appStateGroupDefault.get().running {
|
||||
suspendChat(nseSuspendTimeout)
|
||||
await delayWhenInactive()
|
||||
} else {
|
||||
updateNetCfg()
|
||||
await receiveMsg()
|
||||
}
|
||||
case .suspending: await receiveMsg()
|
||||
case .suspended: await delayWhenInactive()
|
||||
}
|
||||
}
|
||||
|
||||
func receiveMsg() async {
|
||||
if let msg = await chatRecvMsg() {
|
||||
logger.debug("NotificationService receiveMsg: message")
|
||||
if let (id, ntf) = await receivedMsgNtf(msg) {
|
||||
logger.debug("NotificationService receiveMsg: notification")
|
||||
await PendingNtfs.shared.createStream(id)
|
||||
await PendingNtfs.shared.writeStream(id, ntf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func delayWhenInactive() async {
|
||||
logger.debug("NotificationService delayWhenInactive")
|
||||
_ = try? await Task.sleep(nanoseconds: 1000_000000)
|
||||
}
|
||||
}
|
||||
|
||||
func chatRecvMsg() async -> ChatResponse? {
|
||||
@@ -257,14 +492,14 @@ private let isInChina = SKStorefront().countryCode == "CHN"
|
||||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType, privacy: .public)")
|
||||
switch res {
|
||||
case let .contactConnected(user, contact, _):
|
||||
return (contact.id, .nse(notification: createContactConnectedNtf(user, contact)))
|
||||
return (contact.id, .nse(createContactConnectedNtf(user, contact)))
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
return (UserContact(contactRequest: contactRequest).id, .nse(notification: createContactRequestNtf(user, contactRequest)))
|
||||
return (UserContact(contactRequest: contactRequest).id, .nse(createContactRequestNtf(user, contactRequest)))
|
||||
case let .newChatItem(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
var cItem = aChatItem.chatItem
|
||||
@@ -274,7 +509,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
if let file = cItem.autoReceiveFile() {
|
||||
cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem
|
||||
}
|
||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||
let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(createMessageReceivedNtf(user, cInfo, cItem)) : .empty
|
||||
return cItem.showNotification ? (aChatItem.chatId, ntf) : nil
|
||||
case let .rcvFileSndCancelled(_, aChatItem, _):
|
||||
cleanupFile(aChatItem)
|
||||
@@ -292,10 +527,15 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit
|
||||
return (
|
||||
invitation.contact.id,
|
||||
useCallKit() ? .callkit(invitation: invitation) : .nse(notification: createCallInvitationNtf(invitation))
|
||||
useCallKit() ? .callkit(invitation) : .nse(createCallInvitationNtf(invitation))
|
||||
)
|
||||
case let .ntfMessage(_, connEntity, ntfMessage):
|
||||
return if let id = connEntity.id { (id, .msgInfo(ntfMessage)) } else { nil }
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
return nil
|
||||
default:
|
||||
logger.debug("NotificationService processReceivedMsg ignored event: \(res.responseType)")
|
||||
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -334,6 +574,21 @@ func apiStartChat() throws -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func apiActivateChat() -> Bool {
|
||||
chatReopenStore()
|
||||
let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("NotificationService apiActivateChat error: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSuspendChat(timeoutMicroseconds: Int) -> Bool {
|
||||
let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
|
||||
if case .cmdOk = r { return true }
|
||||
logger.error("NotificationService apiSuspendChat error: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSetTempFolder(tempFolder: String) throws {
|
||||
let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder))
|
||||
if case .cmdOk = r { return }
|
||||
@@ -364,8 +619,8 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
return nil
|
||||
}
|
||||
let r = sendSimpleXCmd(.apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfMessages(user, connEntity, msgTs, ntfMessages) = r, let user = user {
|
||||
return NtfMessages(user: user, connEntity: connEntity, msgTs: msgTs, ntfMessages: ntfMessages)
|
||||
if case let .ntfMessages(user, connEntity_, msgTs, ntfMessages) = r, let user = user {
|
||||
return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages)
|
||||
} else if case let .chatCmdError(_, error) = r {
|
||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||
} else {
|
||||
@@ -405,11 +660,11 @@ func setNetworkConfig(_ cfg: NetCfg) throws {
|
||||
|
||||
struct NtfMessages {
|
||||
var user: User
|
||||
var connEntity: ConnectionEntity?
|
||||
var connEntity_: ConnectionEntity?
|
||||
var msgTs: Date?
|
||||
var ntfMessages: [NtfMsgInfo]
|
||||
|
||||
var ntfsEnabled: Bool {
|
||||
user.showNotifications && (connEntity?.ntfsEnabled ?? false)
|
||||
user.showNotifications && (connEntity_?.ntfsEnabled ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */; };
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */; };
|
||||
5C4B3B0A285FB130003915F2 /* DatabaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4B3B09285FB130003915F2 /* DatabaseView.swift */; };
|
||||
5C4BB4CC2B20E177007981AA /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C72B20E176007981AA /* libffi.a */; };
|
||||
5C4BB4CD2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */; };
|
||||
5C4BB4CE2B20E177007981AA /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4C92B20E177007981AA /* libgmpxx.a */; };
|
||||
5C4BB4CF2B20E177007981AA /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4CA2B20E177007981AA /* libgmp.a */; };
|
||||
5C4BB4D02B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */; };
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; };
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A91E283AD0E400C4E99E /* CallManager.swift */; };
|
||||
5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; };
|
||||
@@ -145,11 +150,7 @@
|
||||
5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; };
|
||||
5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; };
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; };
|
||||
5CF937182B22552700E1D781 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF937132B22552700E1D781 /* libffi.a */; };
|
||||
5CF937192B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF937142B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */; };
|
||||
5CF9371A2B22552700E1D781 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF937152B22552700E1D781 /* libgmp.a */; };
|
||||
5CF9371B2B22552700E1D781 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF937162B22552700E1D781 /* libgmpxx.a */; };
|
||||
5CF9371C2B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF937172B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */; };
|
||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
@@ -290,6 +291,11 @@
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettings.swift; sourceTree = "<group>"; };
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = "<group>"; };
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseView.swift; sourceTree = "<group>"; };
|
||||
5C4BB4C72B20E176007981AA /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a"; sourceTree = "<group>"; };
|
||||
5C4BB4C92B20E177007981AA /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C4BB4CA2B20E177007981AA /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = "<group>"; };
|
||||
5C55A91E283AD0E400C4E99E /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = "<group>"; };
|
||||
5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; };
|
||||
@@ -429,11 +435,7 @@
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
|
||||
5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; };
|
||||
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = "<group>"; };
|
||||
5CF937132B22552700E1D781 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CF937142B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CF937152B22552700E1D781 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CF937162B22552700E1D781 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CF937172B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a"; sourceTree = "<group>"; };
|
||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentQueue.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
@@ -511,12 +513,12 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CF9371C2B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a in Frameworks */,
|
||||
5CF9371B2B22552700E1D781 /* libgmpxx.a in Frameworks */,
|
||||
5C4BB4D02B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CF9371A2B22552700E1D781 /* libgmp.a in Frameworks */,
|
||||
5CF937182B22552700E1D781 /* libffi.a in Frameworks */,
|
||||
5CF937192B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a in Frameworks */,
|
||||
5C4BB4CE2B20E177007981AA /* libgmpxx.a in Frameworks */,
|
||||
5C4BB4CC2B20E177007981AA /* libffi.a in Frameworks */,
|
||||
5C4BB4CD2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a in Frameworks */,
|
||||
5C4BB4CF2B20E177007981AA /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -579,11 +581,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CF937132B22552700E1D781 /* libffi.a */,
|
||||
5CF937152B22552700E1D781 /* libgmp.a */,
|
||||
5CF937162B22552700E1D781 /* libgmpxx.a */,
|
||||
5CF937142B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4-ghc8.10.7.a */,
|
||||
5CF937172B22552700E1D781 /* libHSsimplex-chat-5.4.0.7-EoJ0xKOyE47DlSpHXf0V4.a */,
|
||||
5C4BB4C72B20E176007981AA /* libffi.a */,
|
||||
5C4BB4CA2B20E177007981AA /* libgmp.a */,
|
||||
5C4BB4C92B20E177007981AA /* libgmpxx.a */,
|
||||
5C4BB4CB2B20E177007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy-ghc9.6.3.a */,
|
||||
5C4BB4C82B20E176007981AA /* libHSsimplex-chat-5.4.0.6-CwRTIkaIEVTLXC76NTPbOy.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -788,6 +790,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */,
|
||||
5CF9371D2B23429500E1D781 /* ConcurrentQueue.swift */,
|
||||
5CDCAD472818589900503DA2 /* NotificationService.swift */,
|
||||
5CDCAD492818589900503DA2 /* Info.plist */,
|
||||
5CB0BA862826CB3A00B3292C /* InfoPlist.strings */,
|
||||
@@ -1259,6 +1262,7 @@
|
||||
files = (
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */,
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */,
|
||||
5CF9371E2B23429500E1D781 /* ConcurrentQueue.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
||||
var cKey = dbKey.cString(using: .utf8)!
|
||||
var cConfirm = confirm.rawValue.cString(using: .utf8)!
|
||||
// the last parameter of chat_migrate_init is used to return the pointer to chat controller
|
||||
let cjson = chat_migrate_init(&cPath, &cKey, &cConfirm, &chatController)!
|
||||
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, &chatController)!
|
||||
let dbRes = dbMigrationResult(fromCString(cjson))
|
||||
let encrypted = dbKey != ""
|
||||
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
|
||||
@@ -57,6 +57,13 @@ public func chatCloseStore() {
|
||||
}
|
||||
}
|
||||
|
||||
public func chatReopenStore() {
|
||||
let err = fromCString(chat_reopen_store(getChatCtrl()))
|
||||
if err != "" {
|
||||
logger.error("chatReopenStore error: \(err)")
|
||||
}
|
||||
}
|
||||
|
||||
public func resetChatCtrl() {
|
||||
chatController = nil
|
||||
migrationResult = nil
|
||||
|
||||
@@ -27,7 +27,7 @@ public enum ChatCommand {
|
||||
case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?)
|
||||
case startChat(subscribe: Bool, expire: Bool, xftp: Bool)
|
||||
case apiStopChat
|
||||
case apiActivateChat
|
||||
case apiActivateChat(restoreChat: Bool)
|
||||
case apiSuspendChat(timeoutMicroseconds: Int)
|
||||
case setTempFolder(tempFolder: String)
|
||||
case setFilesFolder(filesFolder: String)
|
||||
@@ -156,7 +156,7 @@ public enum ChatCommand {
|
||||
case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))"
|
||||
case let .startChat(subscribe, expire, xftp): return "/_start subscribe=\(onOff(subscribe)) expire=\(onOff(expire)) xftp=\(onOff(xftp))"
|
||||
case .apiStopChat: return "/_stop"
|
||||
case .apiActivateChat: return "/_app activate"
|
||||
case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))"
|
||||
case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)"
|
||||
case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)"
|
||||
case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)"
|
||||
@@ -604,7 +604,8 @@ public enum ChatResponse: Decodable, Error {
|
||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
case ntfTokenStatus(status: NtfTknStatus)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||
case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
// remote desktop responses/events
|
||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||
@@ -751,6 +752,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||
case .ntfToken: return "ntfToken"
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
case .ntfMessage: return "ntfMessage"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .remoteCtrlList: return "remoteCtrlList"
|
||||
case .remoteCtrlFound: return "remoteCtrlFound"
|
||||
@@ -898,6 +900,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||
case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))")
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)"
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
let GROUP_DEFAULT_APP_STATE = "appState"
|
||||
let GROUP_DEFAULT_NSE_STATE = "nseState"
|
||||
let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
|
||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||
@@ -68,11 +69,21 @@ public func registerGroupDefaults() {
|
||||
|
||||
public enum AppState: String {
|
||||
case active
|
||||
case activating
|
||||
case bgRefresh
|
||||
case suspending
|
||||
case suspended
|
||||
case stopped
|
||||
|
||||
public var running: Bool {
|
||||
switch self {
|
||||
case .active: return true
|
||||
case .activating: return true
|
||||
case .bgRefresh: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
public var inactive: Bool {
|
||||
switch self {
|
||||
case .suspending: return true
|
||||
@@ -84,12 +95,32 @@ public enum AppState: String {
|
||||
public var canSuspend: Bool {
|
||||
switch self {
|
||||
case .active: return true
|
||||
case .activating: return true
|
||||
case .bgRefresh: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum NSEState: String {
|
||||
case created
|
||||
case active
|
||||
case suspending
|
||||
case suspended
|
||||
|
||||
public var inactive: Bool {
|
||||
switch self {
|
||||
case .created: true
|
||||
case .suspended: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
public var canSuspend: Bool {
|
||||
if case .active = self { true } else { false }
|
||||
}
|
||||
}
|
||||
|
||||
public enum DBContainer: String {
|
||||
case documents
|
||||
case group
|
||||
@@ -101,6 +132,16 @@ public let appStateGroupDefault = EnumDefault<AppState>(
|
||||
withDefault: .active
|
||||
)
|
||||
|
||||
public let nseStateGroupDefault = EnumDefault<NSEState>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_NSE_STATE,
|
||||
withDefault: .created
|
||||
)
|
||||
|
||||
public func allowBackgroundRefresh() -> Bool {
|
||||
appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive
|
||||
}
|
||||
|
||||
public let dbContainerGroupDefault = EnumDefault<DBContainer>(
|
||||
defaults: groupDefaults,
|
||||
forKey: GROUP_DEFAULT_DB_CONTAINER,
|
||||
|
||||
@@ -2016,7 +2016,8 @@ public enum ConnectionEntity: Decodable {
|
||||
}
|
||||
|
||||
public struct NtfMsgInfo: Decodable {
|
||||
|
||||
public var msgId: String
|
||||
public var msgTs: Date
|
||||
}
|
||||
|
||||
public struct AChatItem: Decodable {
|
||||
|
||||
@@ -16,10 +16,10 @@ extern void hs_init(int argc, char **argv[]);
|
||||
typedef void* chat_ctrl;
|
||||
|
||||
// the last parameter is used to return the pointer to chat controller
|
||||
extern char *chat_migrate_init(char *path, char *key, char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_close_store(chat_ctrl ctl);
|
||||
extern char *chat_reopen_store(chat_ctrl ctl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctl, char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctl);
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait);
|
||||
extern char *chat_parse_markdown(char *str);
|
||||
extern char *chat_parse_server(char *str);
|
||||
|
||||
Reference in New Issue
Block a user