Skip to content

Commit

Permalink
Cleanup how we setup the CallKit provider and have it be used for out…
Browse files Browse the repository at this point in the history
…going calls as well

- tear down ElementCall screens when ending the call from the CXCallController
- make the call UI available in the task manager and lock screen
  • Loading branch information
stefanceriu committed Jun 25, 2024
1 parent fa22b98 commit 57761ba
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 101 deletions.
6 changes: 3 additions & 3 deletions ElementX/Sources/Application/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
switch action {
case .answerCall(let roomID):
case .startCall(let roomID):
self?.handleAppRoute(.call(roomID: roomID))
case .declineCall:
break
case .endCall:
self?.handleAppRoute(.call(roomID: nil))
}
}
.store(in: &cancellables)
Expand Down
4 changes: 2 additions & 2 deletions ElementX/Sources/Application/Navigation/AppRoutes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ enum AppRoute: Equatable {
case childEventOnRoomAlias(eventID: String, alias: String)
/// The profile of a matrix user (outside of a room).
case userProfile(userID: String)
/// An Element Call running in a particular room
case call(roomID: String)
/// An Element Call running in a particular room. Nil if having to end the call
case call(roomID: String?)
/// An Element Call link generated outside of a chat room.
case genericCallLink(url: URL)
/// The settings screen.
Expand Down
16 changes: 14 additions & 2 deletions ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,12 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
case .userProfile(let userID):
stateMachine.processEvent(.showUserProfileScreen(userID: userID), userInfo: .init(animated: animated))
case .call(let roomID):
Task {
await presentCallScreen(roomID: roomID)
if let roomID {
Task {
await presentCallScreen(roomID: roomID)
}
} else {
dismissCallScreen()
}
case .genericCallLink(let url):
navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
Expand Down Expand Up @@ -569,6 +573,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentCallScreen(roomProxy: roomProxy)
}

private func dismissCallScreen() {
guard navigationSplitCoordinator.sheetCoordinator is CallScreenCoordinator else {
return
}

navigationSplitCoordinator.setSheetCoordinator(nil)
}

// MARK: Secure backup confirmation

private func presentSecureBackupLogoutConfirmationScreen() {
Expand Down
32 changes: 16 additions & 16 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4697,44 +4697,44 @@ class ElementCallServiceMock: ElementCallServiceProtocol {

//MARK: - setupCallSession

var setupCallSessionTitleUnderlyingCallsCount = 0
var setupCallSessionTitleCallsCount: Int {
var setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = 0
var setupCallSessionRoomIDRoomDisplayNameCallsCount: Int {
get {
if Thread.isMainThread {
return setupCallSessionTitleUnderlyingCallsCount
return setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = setupCallSessionTitleUnderlyingCallsCount
returnValue = setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount
}

return returnValue!
}
}
set {
if Thread.isMainThread {
setupCallSessionTitleUnderlyingCallsCount = newValue
setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
setupCallSessionTitleUnderlyingCallsCount = newValue
setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = newValue
}
}
}
}
var setupCallSessionTitleCalled: Bool {
return setupCallSessionTitleCallsCount > 0
var setupCallSessionRoomIDRoomDisplayNameCalled: Bool {
return setupCallSessionRoomIDRoomDisplayNameCallsCount > 0
}
var setupCallSessionTitleReceivedTitle: String?
var setupCallSessionTitleReceivedInvocations: [String] = []
var setupCallSessionTitleClosure: ((String) async -> Void)?
var setupCallSessionRoomIDRoomDisplayNameReceivedArguments: (roomID: String, roomDisplayName: String)?
var setupCallSessionRoomIDRoomDisplayNameReceivedInvocations: [(roomID: String, roomDisplayName: String)] = []
var setupCallSessionRoomIDRoomDisplayNameClosure: ((String, String) async -> Void)?

func setupCallSession(title: String) async {
setupCallSessionTitleCallsCount += 1
setupCallSessionTitleReceivedTitle = title
func setupCallSession(roomID: String, roomDisplayName: String) async {
setupCallSessionRoomIDRoomDisplayNameCallsCount += 1
setupCallSessionRoomIDRoomDisplayNameReceivedArguments = (roomID: roomID, roomDisplayName: roomDisplayName)
DispatchQueue.main.async {
self.setupCallSessionTitleReceivedInvocations.append(title)
self.setupCallSessionRoomIDRoomDisplayNameReceivedInvocations.append((roomID: roomID, roomDisplayName: roomDisplayName))
}
await setupCallSessionTitleClosure?(title)
await setupCallSessionRoomIDRoomDisplayNameClosure?(roomID, roomDisplayName)
}
//MARK: - tearDownCallSession

Expand Down
4 changes: 2 additions & 2 deletions ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
return
}

await elementCallService.setupCallSession(title: roomProxy.roomTitle)
await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle)

let _ = await roomProxy.sendCallNotificationIfNeeeded()
}
Expand Down Expand Up @@ -136,7 +136,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
"""

let result = await widgetDriver.sendMessage(hangUpMessage)
MXLog.error("Result yo: \(result)")
MXLog.info("Sent hangUp message with result: \(result)")
}

private static let eventHandlerName = "elementx"
Expand Down
170 changes: 97 additions & 73 deletions ElementX/Sources/Services/ElementCall/ElementCallService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,27 @@ import Foundation
import PushKit

class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
private let pushRegistry: PKPushRegistry
private struct CallID: Equatable {
let callKitID: UUID
let roomID: String
}

private let pushRegistry: PKPushRegistry
private let callController = CXCallController()

private var callProvider: CXProvider?
private var ongoingCallID: UUID?

private var incomingCallRoomID: String?
private lazy var callProvider: CXProvider = {
let configuration = CXProviderConfiguration()
configuration.supportsVideo = true
configuration.includesCallsInRecents = true
// Provide image icon if available
configuration.iconTemplateImageData = nil

// https://stackoverflow.com/a/46077628/730924
configuration.supportedHandleTypes = [.generic]

return CXProvider(configuration: configuration)
}()

private var ongoingCallID: CallID?
private var endUnansweredCallTask: Task<Void, Never>?

private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
Expand All @@ -44,47 +56,30 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe

pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]

callProvider.setDelegate(self, queue: DispatchQueue.main)
}

func setupCallSession(title: String) async {
guard ongoingCallID == nil else {
return
func setupCallSession(roomID: String, roomDisplayName: String) async {
let callID = if let ongoingCallID {
ongoingCallID
} else {
CallID(callKitID: UUID(), roomID: roomID)
}

let callID = UUID()

ongoingCallID = callID

let handle = CXHandle(type: .generic, value: title)
let startCallAction = CXStartCallAction(call: callID, handle: handle)
let handle = CXHandle(type: .generic, value: roomDisplayName)
let startCallAction = CXStartCallAction(call: callID.callKitID, handle: handle)
startCallAction.isVideo = true

let transaction = CXTransaction(action: startCallAction)

do {
try await callController.request(transaction)
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoChat, options: [])
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch {
MXLog.error("Failed setting up VoIP session with error: \(error)")
tearDownCallSession()
}
try? await callController.request(CXTransaction(action: startCallAction))
try? AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoChat, options: [])
try? AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
}

func tearDownCallSession() {
guard let ongoingCallID else {
return
}

try? AVAudioSession.sharedInstance().setActive(false)

let endCallAction = CXEndCallAction(call: ongoingCallID)
let transaction = CXTransaction(action: endCallAction)

callController.request(transaction) { error in
if let error {
MXLog.error("Failed transaction with error: \(error)")
}
}
tearDownCallSession(sendEndCallAction: true)
}

// MARK: - PKPushRegistryDelegate
Expand All @@ -97,46 +92,34 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return
}

let callID = UUID()
let callID = CallID(callKitID: UUID(), roomID: roomID)
ongoingCallID = callID

incomingCallRoomID = roomID

let configuration = CXProviderConfiguration()
configuration.supportsVideo = true
configuration.includesCallsInRecents = true
// Provide image icon if available
configuration.iconTemplateImageData = nil

// https://stackoverflow.com/a/46077628/730924
configuration.supportedHandleTypes = [.generic]
let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String

let update = CXCallUpdate()
update.hasVideo = true

update.localizedCallerName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String

update.localizedCallerName = roomDisplayName
// https://stackoverflow.com/a/41230020/730924
update.remoteHandle = .init(type: .generic, value: roomID)

let callProvider = CXProvider(configuration: configuration)
callProvider.setDelegate(self, queue: nil)
callProvider.reportNewIncomingCall(with: callID, update: update) { error in
callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { error in
if let error {
MXLog.error("Failed reporting new incoming call with error: \(error)")
}

completion()
}

endUnansweredCallTask = Task { [weak self, callProvider, callID] in
endUnansweredCallTask = Task { [weak self] in
try? await Task.sleep(for: .seconds(15))
guard !Task.isCancelled else {

guard let self, !Task.isCancelled else {
return
}

if let ongoingCallID = self?.ongoingCallID, ongoingCallID == callID {
callProvider.reportCall(with: callID, endedAt: .now, reason: .unanswered)
if let ongoingCallID, ongoingCallID.callKitID == callID.callKitID {
callProvider.reportCall(with: ongoingCallID.callKitID, endedAt: .now, reason: .unanswered)
}
}
}
Expand All @@ -147,29 +130,70 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
MXLog.info("Call provider did reset: \(provider)")
}

func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
guard let ongoingCallID else {
MXLog.error("Failed starting call, missing ongoingCallID")
tearDownCallSession()
return
}

provider.reportOutgoingCall(with: ongoingCallID.callKitID, connectedAt: .now)

action.fulfill()
}

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
if let incomingCallRoomID {
Task {
// Dispatch to next run loop so it doesn't conflict with `setupCallSession`
actionsSubject.send(.answerCall(roomID: incomingCallRoomID))
}
self.incomingCallRoomID = nil
endUnansweredCallTask?.cancel()
} else {
MXLog.error("Failed answering incoming call, missing room ID")
guard let ongoingCallID else {
MXLog.error("Failed answering incoming call, missing ongoingCallID")
tearDownCallSession()
return
}

// Dispatch to next run loop so it doesn't conflict with `setupCallSession`
actionsSubject.send(.startCall(roomID: ongoingCallID.roomID))

endUnansweredCallTask?.cancel()

action.fulfill()
}

func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
// Forward this to the widget somehow
}

func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
if let incomingCallRoomID {
actionsSubject.send(.declineCall(roomID: incomingCallRoomID))
self.incomingCallRoomID = nil
} else {
MXLog.error("Failed declining incoming call, missing room ID")
defer {
tearDownCallSession(sendEndCallAction: false)
}


guard let ongoingCallID else {
MXLog.error("Failed declining incoming call, missing ongoingCallID")
return
}

actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))

action.fulfill()
}

// MARK: - Private

func tearDownCallSession(sendEndCallAction: Bool = true) {
guard let ongoingCallID else {
return
}

try? AVAudioSession.sharedInstance().setActive(false)

if sendEndCallAction {
let transaction = CXTransaction(action: CXEndCallAction(call: ongoingCallID.callKitID))
callController.request(transaction) { error in
if let error {
MXLog.error("Failed transaction with error: \(error)")
}
}
}

self.ongoingCallID = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import Combine

enum ElementCallServiceAction {
case answerCall(roomID: String)
case declineCall(roomID: String)
case startCall(roomID: String)
case endCall(roomID: String)
}

enum ElementCallServiceNotificationKey: String {
Expand All @@ -32,7 +32,7 @@ let ElementCallServiceNotificationDiscardDelta = 10.0
protocol ElementCallServiceProtocol {
var actions: AnyPublisher<ElementCallServiceAction, Never> { get }

func setupCallSession(title: String) async
func setupCallSession(roomID: String, roomDisplayName: String) async

func tearDownCallSession()
}

0 comments on commit 57761ba

Please sign in to comment.