Skip to content

Commit

Permalink
[Fix]CallKitService storage access (#566)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis authored Oct 8, 2024
1 parent 5ebe8fb commit d2c4432
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 22 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### 🔄 Changed
### 🐞 Fixed

- Improved performance on lower end devices [#557](https://github.com/GetStream/stream-video-swift/pull/557)
- CallKitService access issue when ending calls [#566](https://github.com/GetStream/stream-video-swift/pull/566)

# [1.12.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.12.0)
_September 27, 2024_
Expand Down
72 changes: 55 additions & 17 deletions Sources/StreamVideo/CallKit/CallKitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,23 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
}

/// The unique identifier for the call.
open var callId: String { active.map { storage[$0]?.call.callId ?? "" } ?? "" }
open var callId: String {
if let active, let callEntry = callEntry(for: active) {
return callEntry.call.callId
} else {
return ""
}
}

/// The type of call.
open var callType: String { active.map { storage[$0]?.call.callType ?? "" } ?? "" }
open var callType: String {
if let active, let callEntry = callEntry(for: active) {
return callEntry.call.callType
} else {
return ""
}
}

/// The icon data for the call template.
open var iconTemplateImageData: Data?
/// Whether the call can be held on its own or swapped with another call.
Expand All @@ -65,8 +79,10 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// The call provider responsible for handling call-related actions.
open internal(set) lazy var callProvider = buildProvider()

private(set) var storage: [UUID: CallEntry] = [:]
private var _storage: [UUID: CallEntry] = [:]
private let storageAccessQueue: UnfairQueue = .init()
private var active: UUID?
var callCount: Int { storageAccessQueue.sync { _storage.count } }

private var callEventsSubscription: Task<Void, Error>?
private var callEndedNotificationCancellable: AnyCancellable?
Expand Down Expand Up @@ -120,7 +136,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
"""
)

guard let streamVideo, let callEntry = storage[callUUID] else {
guard let streamVideo, let callEntry = callEntry(for: callUUID) else {
log.warning(
"""
CallKit operation:reportIncomingCall cannot be fulfilled because
Expand Down Expand Up @@ -188,7 +204,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// The call was accepted somewhere else (e.g the incoming call on the same device or another
/// device). No action is required.
guard
let newCallEntry = storage.first(where: { $0.value.call.cId == response.callCid })?.value,
let newCallEntry = callEntry(for: response.callCid),
newCallEntry.callUUID != active // Ensure that the new call isn't the currently active one.
else {
return
Expand All @@ -200,7 +216,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
)
ringingTimerCancellable?.cancel()
ringingTimerCancellable = nil
storage[newCallEntry.callUUID] = nil
set(nil, for: newCallEntry.callUUID)
callCache.remove(for: newCallEntry.call.cId)
}

Expand All @@ -209,7 +225,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// - Parameter response: The call rejected event.
open func callRejected(_ response: CallRejectedEvent) {
guard
let newCallEntry = storage.first(where: { $0.value.call.cId == response.callCid })?.value,
let newCallEntry = callEntry(for: response.callCid),
newCallEntry.callUUID != active // Ensure that the new call isn't the currently active one.
else {
return
Expand All @@ -230,13 +246,13 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
)
ringingTimerCancellable?.cancel()
ringingTimerCancellable = nil
storage[newCallEntry.callUUID] = nil
set(nil, for: newCallEntry.callUUID)
callCache.remove(for: newCallEntry.call.cId)
}

/// Handles the event when a call ends.
open func callEnded(_ cId: String) {
guard let callEndedEntry = storage.first(where: { $0.value.call.cId == cId })?.value else {
guard let callEndedEntry = callEntry(for: cId) else {
return
}
Task {
Expand All @@ -262,7 +278,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// We listen for the event so in the case we are the only ones remaining
/// in the call, we leave.
Task { @MainActor in
if let call = storage.first(where: { $0.value.call.cId == response.callCid })?.value.call,
if let call = callEntry(for: response.callCid)?.call,
call.state.participants.count == 1 {
callEnded(response.callCid)
}
Expand All @@ -276,8 +292,10 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
/// This callback can be treated as a request to end all calls without the need to respond to any actions
open func providerDidReset(_ provider: CXProvider) {
log.debug("CXProvider didReset.")
for (_, entry) in storage {
entry.call.leave()
storageAccessQueue.sync {
for (_, entry) in _storage {
entry.call.leave()
}
}
}

Expand All @@ -287,7 +305,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
) {
guard
action.callUUID != active,
let callToJoinEntry = storage[action.callUUID]
let callToJoinEntry = callEntry(for: action.callUUID)
else {
return action.fail()
}
Expand All @@ -313,7 +331,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
action.fulfill()
} catch {
callToJoinEntry.call.leave()
storage[action.callUUID] = nil
set(nil, for: action.callUUID)
log.error(error)
action.fail()
}
Expand All @@ -328,7 +346,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
ringingTimerCancellable = nil
let currentCallWasEnded = action.callUUID == active

guard let stackEntry = storage[action.callUUID] else {
guard let stackEntry = callEntry(for: action.callUUID) else {
action.fail()
return
}
Expand Down Expand Up @@ -363,7 +381,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
if currentCallWasEnded {
stackEntry.call.leave()
}
storage[action.callUUID] = nil
set(nil, for: action.callUUID)
action.fulfill()
}
}
Expand Down Expand Up @@ -503,7 +521,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
callType: idComponents[0],
callId: idComponents[1]
) {
storage[uuid] = .init(call: call, callUUID: uuid)
set(.init(call: call, callUUID: uuid), for: uuid)
}

update.localizedCallerName = localizedCallerName
Expand All @@ -524,6 +542,26 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {

return (uuid, update)
}

// MARK: - Storage Access

private func set(_ value: CallEntry?, for key: UUID) {
storageAccessQueue.sync {
_storage[key] = value
}
}

private func callEntry(for cId: String) -> CallEntry? {
storageAccessQueue.sync {
_storage
.first { $0.value.call.cId == cId }?
.value
}
}

private func callEntry(for uuid: UUID) -> CallEntry? {
storageAccessQueue.sync { _storage[uuid] }
}
}

extension CallKitService: InjectionKey {
Expand Down
8 changes: 4 additions & 4 deletions StreamVideoTests/CallKit/CallKitServiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {

await waitExpectation()

XCTAssertEqual(subject.storage.count, 1)
XCTAssertEqual(subject.callCount, 1)

// Stub with the new call
let secondCallUUID = UUID()
Expand All @@ -408,7 +408,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
callerId: callerId
) { _ in }

XCTAssertEqual(subject.storage.count, 2)
XCTAssertEqual(subject.callCount, 2)

subject.provider(
callProvider,
Expand All @@ -417,9 +417,9 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
)
)

await fulfillment { [weak subject] in subject?.storage.count == 1 }
await fulfillment { [weak subject] in subject?.callCount == 1 }

XCTAssertEqual(subject.storage.count, 1)
XCTAssertEqual(subject.callCount, 1)
}

// MARK: - callEnded
Expand Down

0 comments on commit d2c4432

Please sign in to comment.