ios: refresh call invitations and report call on start and activation; core: restore calls on activation (#776)

This commit is contained in:
JRoberts
2022-07-05 15:15:15 +04:00
committed by GitHub
parent 8c307c4675
commit ab848e8c13
13 changed files with 89 additions and 57 deletions

View File

@@ -35,7 +35,7 @@ final class ChatModel: ObservableObject {
@Published var notificationMode = NotificationsMode.off
@Published var notificationPreview: NotificationPreviewMode? = .message
// current WebRTC call
@Published var callInvitations: Dictionary<ChatId, CallInvitation> = [:]
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
@Published var activeCall: Call?
@Published var callCommand: WCallCommand?
@Published var showCallView = false

View File

@@ -184,7 +184,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
addNotification(createMessageReceivedNtf(cInfo, cItem))
}
func notifyCallInvitation(_ invitation: CallInvitation) {
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
logger.debug("NtfManager.notifyCallInvitation")
addNotification(createCallInvitationNtf(invitation))
}

View File

@@ -476,6 +476,12 @@ func apiEndCall(_ contact: Contact) async throws {
try await sendCommandOkResp(.apiEndCall(contact: contact))
}
func apiGetCallInvitations() throws -> [RcvCallInvitation] {
let r = chatSendCmdSync(.apiGetCallInvitations)
if case let .callInvitations(invs) = r { return invs }
throw r
}
func apiCallStatus(_ contact: Contact, _ status: String) async throws {
if let callStatus = WebRTCCallStatus.init(rawValue: status) {
try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus))
@@ -538,6 +544,7 @@ func startChat() throws {
m.userSMPServers = try getUserSMPServers()
let chats = try apiGetChats()
m.chats = chats.map { Chat.init($0) }
try refreshCallInvitations()
(m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken()
if let token = m.deviceToken {
registerToken(token: token)
@@ -694,19 +701,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
case let .callInvitation(contact, callType, sharedKey, callTs):
let uuid = UUID()
var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey, callTs: callTs)
m.callInvitations[contact.id] = invitation
CallController.shared.reportNewIncomingCall(invitation: invitation) { error in
if let error = error {
invitation.callkitUUID = nil
m.callInvitations[contact.id] = invitation
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
} else {
logger.debug("reportNewIncomingCall success")
}
}
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)
// This will be called from notification service extension
// CXProvider.reportNewIncomingVoIPPushPayload([
@@ -790,6 +787,27 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
m.updateNetworkStatus(contact.id, .error(err))
}
func refreshCallInvitations() throws {
let m = ChatModel.shared
let callInvitations = try apiGetCallInvitations()
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
if let inv = callInvitations.last {
activateCall(inv)
}
}
func activateCall(_ callInvitation: RcvCallInvitation) {
let m = ChatModel.shared
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
if let error = error {
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
} else {
logger.debug("reportNewIncomingCall success")
}
}
}
private struct UserResponse: Decodable {
var user: User?
var error: String?

View File

@@ -63,8 +63,8 @@ struct SimpleXApp: App {
let appState = appStateGroupDefault.get()
activateChat()
if appState.inactive && chatModel.chatRunning == true {
// TODO refresh call invitation
updateChats()
updateCallInvitations()
}
doAuthenticate = authenticationExpired()
default:
@@ -123,4 +123,13 @@ struct SimpleXApp: App {
logger.error("apiGetChats: cannot update chats \(responseError(error))")
}
}
private func updateCallInvitations() {
do {
try refreshCallInvitations()
}
catch let error {
logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))")
}
}
}

View File

@@ -18,7 +18,7 @@ class CallController: NSObject, ObservableObject {
// private let provider = CXProvider(configuration: CallController.configuration)
// private let controller = CXCallController()
private let callManager = CallManager()
@Published var activeCallInvitation: CallInvitation?
@Published var activeCallInvitation: RcvCallInvitation?
// PKPushRegistry will be used from notification service extension
// let registry = PKPushRegistry(queue: nil)
@@ -120,7 +120,7 @@ class CallController: NSObject, ObservableObject {
// }
// }
func reportNewIncomingCall(invitation: CallInvitation, completion: @escaping (Error?) -> Void) {
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
logger.debug("CallController.reportNewIncomingCall")
if !UserDefaults.standard.bool(forKey: DEFAULT_EXPERIMENTAL_CALLS) { return }
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
@@ -142,7 +142,7 @@ class CallController: NSObject, ObservableObject {
// }
// }
func reportCallRemoteEnded(invitation: CallInvitation) {
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
@@ -172,7 +172,7 @@ class CallController: NSObject, ObservableObject {
}
}
func answerCall(invitation: CallInvitation) {
func answerCall(invitation: RcvCallInvitation) {
callManager.answerIncomingCall(invitation: invitation)
if invitation.contact.id == self.activeCallInvitation?.contact.id {
self.activeCallInvitation = nil
@@ -193,7 +193,7 @@ class CallController: NSObject, ObservableObject {
// }
}
func endCall(invitation: CallInvitation) {
func endCall(invitation: RcvCallInvitation) {
callManager.endCall(invitation: invitation) {
if invitation.contact.id == self.activeCallInvitation?.contact.id {
DispatchQueue.main.async {

View File

@@ -34,7 +34,7 @@ class CallManager {
return false
}
func answerIncomingCall(invitation: CallInvitation) {
func answerIncomingCall(invitation: RcvCallInvitation) {
let m = ChatModel.shared
m.callInvitations.removeValue(forKey: invitation.contact.id)
m.activeCall = Call(
@@ -42,13 +42,13 @@ class CallManager {
contact: invitation.contact,
callkitUUID: invitation.callkitUUID,
callState: .invitationAccepted,
localMedia: invitation.peerMedia,
localMedia: invitation.callType.media,
sharedKey: invitation.sharedKey
)
m.showCallView = true
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
logger.debug("answerIncomingCall useRelay \(useRelay)")
m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay)
m.callCommand = .start(media: invitation.callType.media, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay)
}
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
@@ -86,7 +86,7 @@ class CallManager {
}
}
func endCall(invitation: CallInvitation, completed: @escaping () -> Void) {
func endCall(invitation: RcvCallInvitation, completed: @escaping () -> Void) {
ChatModel.shared.callInvitations.removeValue(forKey: invitation.contact.id)
Task {
do {
@@ -98,7 +98,7 @@ class CallManager {
}
}
private func getCallInvitation(_ callUUID: UUID) -> CallInvitation? {
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
return invitation
}

View File

@@ -26,10 +26,10 @@ struct IncomingCallView: View {
}
}
private func incomingCall(_ invitation: CallInvitation) -> some View {
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
Image(systemName: invitation.peerMedia == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
Text(invitation.callTypeText)
}
HStack {
@@ -81,7 +81,7 @@ struct IncomingCallView: View {
struct IncomingCallView_Previews: PreviewProvider {
static var previews: some View {
CallController.shared.activeCallInvitation = CallInvitation.sampleData
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
return IncomingCallView()
}
}

View File

@@ -52,6 +52,7 @@ public enum ChatCommand {
case apiSendCallAnswer(contact: Contact, answer: WebRTCSession)
case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case apiEndCall(contact: Contact)
case apiGetCallInvitations
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
case receiveFile(fileId: Int64)
@@ -100,6 +101,7 @@ public enum ChatCommand {
case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))"
case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))"
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
case .apiGetCallInvitations: return "/_call get"
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
case let .receiveFile(fileId): return "/freceive \(fileId)"
@@ -149,6 +151,7 @@ public enum ChatCommand {
case .apiSendCallAnswer: return "apiSendCallAnswer"
case .apiSendCallExtraInfo: return "apiSendCallExtraInfo"
case .apiEndCall: return "apiEndCall"
case .apiGetCallInvitations: return "apiGetCallInvitations"
case .apiCallStatus: return "apiCallStatus"
case .apiChatRead: return "apiChatRead"
case .receiveFile: return "receiveFile"
@@ -219,11 +222,12 @@ public enum ChatResponse: Decodable, Error {
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date)
case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
case callAnswer(contact: Contact, answer: WebRTCSession)
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
case callEnded(contact: Contact)
case callInvitations(callInvitations: [RcvCallInvitation])
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
@@ -287,6 +291,7 @@ public enum ChatResponse: Decodable, Error {
case .callAnswer: return "callAnswer"
case .callExtraInfo: return "callExtraInfo"
case .callEnded: return "callEnded"
case .callInvitations: return "callInvitations"
case .ntfTokenStatus: return "ntfTokenStatus"
case .ntfToken: return "ntfToken"
case .ntfMessages: return "ntfMessages"
@@ -348,11 +353,12 @@ public enum ChatResponse: Decodable, Error {
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")"
case let .callInvitation(inv): return String(describing: inv)
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
case let .callEnded(contact): return "contact: \(contact.id)"
case let .callInvitations(invs): return String(describing: invs)
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(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"

View File

@@ -37,32 +37,24 @@ public struct WebRTCExtraInfo: Codable {
public var rtcIceCandidates: String
}
public struct CallInvitation {
public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) {
self.contact = contact
self.callkitUUID = callkitUUID
self.peerMedia = peerMedia
self.sharedKey = sharedKey
self.callTs = callTs
}
public struct RcvCallInvitation: Decodable {
public var contact: Contact
public var callkitUUID: UUID?
public var peerMedia: CallMediaType
public var callkitUUID: UUID? = UUID()
public var callType: CallType
public var sharedKey: String?
public var callTs: Date
public var callTypeText: LocalizedStringKey {
get {
switch peerMedia {
switch callType.media {
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
}
}
}
public static let sampleData = CallInvitation(
public static let sampleData = RcvCallInvitation(
contact: Contact.sampleData,
peerMedia: .audio,
callType: CallType(media: .audio, capabilities: CallCapabilities(encryption: false)),
callTs: .now
)
}

View File

@@ -50,8 +50,8 @@ public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UN
)
}
public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
let text = invitation.peerMedia == .video
public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent {
let text = invitation.callType.media == .video
? NSLocalizedString("Incoming video call", comment: "notification")
: NSLocalizedString("Incoming audio call", comment: "notification")
return createNotification(

View File

@@ -150,7 +150,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ())
startChatController user subConns = do
asks smpAgent >>= resumeAgentClient
restoreCalls
restoreCalls user
s <- asks agentAsync
readTVarIO s >>= maybe (start s) (pure . fst)
where
@@ -162,11 +162,13 @@ startChatController user subConns = do
else pure Nothing
atomically . writeTVar s $ Just (a1, a2)
pure a1
restoreCalls = do
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user)
let callsMap = M.fromList $ map (\(call@Call {contactId}) -> (contactId, call)) savedCalls
calls <- asks currentCalls
atomically $ writeTVar calls callsMap
restoreCalls :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m ()
restoreCalls user = do
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user)
let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls
calls <- asks currentCalls
atomically $ writeTVar calls callsMap
stopChatController :: MonadUnliftIO m => ChatController -> m ()
stopChatController ChatController {smpAgent, agentAsync = s} = do
@@ -213,7 +215,9 @@ processChatCommand = \case
APIStopChat -> do
ask >>= stopChatController
pure CRChatStopped
APIActivateChat -> withAgent activateAgent $> CRCmdOk
APIActivateChat -> do
withUser $ \user -> restoreCalls user
withAgent activateAgent $> CRCmdOk
APISuspendChat t -> withAgent (`suspendAgent` t) $> CRCmdOk
ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk
SetFilesFolder filesFolder' -> do
@@ -544,8 +548,9 @@ processChatCommand = \case
SndMessage {msgId} <- sendDirectContactMessage ct (XCallEnd callId)
updateCallItemStatus userId ct call WCSDisconnected $ Just msgId
pure Nothing
APIGetCallInvitations -> withUser $ \user@User {userId} -> do
invs <- mapMaybe callInvitation <$> withStore' (`getCalls` user)
APIGetCallInvitations -> withUser $ \User {userId} -> do
calls <- asks currentCalls >>= readTVarIO
let invs = mapMaybe callInvitation $ M.elems calls
CRCallInvitations <$> mapM (rcvCallInvitation userId) invs
where
callInvitation Call {contactId, callState, callTs} = case callState of

View File

@@ -33,6 +33,7 @@ data Call = Call
callState :: CallState,
callTs :: UTCTime
}
deriving (Show)
isRcvInvitation :: Call -> Bool
isRcvInvitation Call {callState} = case callState of
@@ -88,7 +89,7 @@ data CallState
peerCallSession :: WebRTCSession,
sharedKey :: Maybe C.Key
}
deriving (Generic)
deriving (Show, Generic)
-- database representation
instance FromJSON CallState where

View File

@@ -3693,6 +3693,7 @@ getCalls db User {userId} = do
contact_id, shared_call_id, chat_item_id, call_state, call_ts
FROM calls
WHERE user_id = ?
ORDER BY call_ts ASC
|]
(Only userId)
where