Skip to content

Commit d2c4432

Browse files
authored
[Fix]CallKitService storage access (#566)
1 parent 5ebe8fb commit d2c4432

File tree

3 files changed

+63
-22
lines changed

3 files changed

+63
-22
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

55
# Upcoming
66

7-
### 🔄 Changed
7+
### 🐞 Fixed
8+
9+
- Improved performance on lower end devices [#557](https://github.com/GetStream/stream-video-swift/pull/557)
10+
- CallKitService access issue when ending calls [#566](https://github.com/GetStream/stream-video-swift/pull/566)
811

912
# [1.12.0](https://github.com/GetStream/stream-video-swift/releases/tag/1.12.0)
1013
_September 27, 2024_

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,23 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
4444
}
4545

4646
/// The unique identifier for the call.
47-
open var callId: String { active.map { storage[$0]?.call.callId ?? "" } ?? "" }
47+
open var callId: String {
48+
if let active, let callEntry = callEntry(for: active) {
49+
return callEntry.call.callId
50+
} else {
51+
return ""
52+
}
53+
}
54+
4855
/// The type of call.
49-
open var callType: String { active.map { storage[$0]?.call.callType ?? "" } ?? "" }
56+
open var callType: String {
57+
if let active, let callEntry = callEntry(for: active) {
58+
return callEntry.call.callType
59+
} else {
60+
return ""
61+
}
62+
}
63+
5064
/// The icon data for the call template.
5165
open var iconTemplateImageData: Data?
5266
/// Whether the call can be held on its own or swapped with another call.
@@ -65,8 +79,10 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
6579
/// The call provider responsible for handling call-related actions.
6680
open internal(set) lazy var callProvider = buildProvider()
6781

68-
private(set) var storage: [UUID: CallEntry] = [:]
82+
private var _storage: [UUID: CallEntry] = [:]
83+
private let storageAccessQueue: UnfairQueue = .init()
6984
private var active: UUID?
85+
var callCount: Int { storageAccessQueue.sync { _storage.count } }
7086

7187
private var callEventsSubscription: Task<Void, Error>?
7288
private var callEndedNotificationCancellable: AnyCancellable?
@@ -120,7 +136,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
120136
"""
121137
)
122138

123-
guard let streamVideo, let callEntry = storage[callUUID] else {
139+
guard let streamVideo, let callEntry = callEntry(for: callUUID) else {
124140
log.warning(
125141
"""
126142
CallKit operation:reportIncomingCall cannot be fulfilled because
@@ -188,7 +204,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
188204
/// The call was accepted somewhere else (e.g the incoming call on the same device or another
189205
/// device). No action is required.
190206
guard
191-
let newCallEntry = storage.first(where: { $0.value.call.cId == response.callCid })?.value,
207+
let newCallEntry = callEntry(for: response.callCid),
192208
newCallEntry.callUUID != active // Ensure that the new call isn't the currently active one.
193209
else {
194210
return
@@ -200,7 +216,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
200216
)
201217
ringingTimerCancellable?.cancel()
202218
ringingTimerCancellable = nil
203-
storage[newCallEntry.callUUID] = nil
219+
set(nil, for: newCallEntry.callUUID)
204220
callCache.remove(for: newCallEntry.call.cId)
205221
}
206222

@@ -209,7 +225,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
209225
/// - Parameter response: The call rejected event.
210226
open func callRejected(_ response: CallRejectedEvent) {
211227
guard
212-
let newCallEntry = storage.first(where: { $0.value.call.cId == response.callCid })?.value,
228+
let newCallEntry = callEntry(for: response.callCid),
213229
newCallEntry.callUUID != active // Ensure that the new call isn't the currently active one.
214230
else {
215231
return
@@ -230,13 +246,13 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
230246
)
231247
ringingTimerCancellable?.cancel()
232248
ringingTimerCancellable = nil
233-
storage[newCallEntry.callUUID] = nil
249+
set(nil, for: newCallEntry.callUUID)
234250
callCache.remove(for: newCallEntry.call.cId)
235251
}
236252

237253
/// Handles the event when a call ends.
238254
open func callEnded(_ cId: String) {
239-
guard let callEndedEntry = storage.first(where: { $0.value.call.cId == cId })?.value else {
255+
guard let callEndedEntry = callEntry(for: cId) else {
240256
return
241257
}
242258
Task {
@@ -262,7 +278,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
262278
/// We listen for the event so in the case we are the only ones remaining
263279
/// in the call, we leave.
264280
Task { @MainActor in
265-
if let call = storage.first(where: { $0.value.call.cId == response.callCid })?.value.call,
281+
if let call = callEntry(for: response.callCid)?.call,
266282
call.state.participants.count == 1 {
267283
callEnded(response.callCid)
268284
}
@@ -276,8 +292,10 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
276292
/// This callback can be treated as a request to end all calls without the need to respond to any actions
277293
open func providerDidReset(_ provider: CXProvider) {
278294
log.debug("CXProvider didReset.")
279-
for (_, entry) in storage {
280-
entry.call.leave()
295+
storageAccessQueue.sync {
296+
for (_, entry) in _storage {
297+
entry.call.leave()
298+
}
281299
}
282300
}
283301

@@ -287,7 +305,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
287305
) {
288306
guard
289307
action.callUUID != active,
290-
let callToJoinEntry = storage[action.callUUID]
308+
let callToJoinEntry = callEntry(for: action.callUUID)
291309
else {
292310
return action.fail()
293311
}
@@ -313,7 +331,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
313331
action.fulfill()
314332
} catch {
315333
callToJoinEntry.call.leave()
316-
storage[action.callUUID] = nil
334+
set(nil, for: action.callUUID)
317335
log.error(error)
318336
action.fail()
319337
}
@@ -328,7 +346,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
328346
ringingTimerCancellable = nil
329347
let currentCallWasEnded = action.callUUID == active
330348

331-
guard let stackEntry = storage[action.callUUID] else {
349+
guard let stackEntry = callEntry(for: action.callUUID) else {
332350
action.fail()
333351
return
334352
}
@@ -363,7 +381,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
363381
if currentCallWasEnded {
364382
stackEntry.call.leave()
365383
}
366-
storage[action.callUUID] = nil
384+
set(nil, for: action.callUUID)
367385
action.fulfill()
368386
}
369387
}
@@ -503,7 +521,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
503521
callType: idComponents[0],
504522
callId: idComponents[1]
505523
) {
506-
storage[uuid] = .init(call: call, callUUID: uuid)
524+
set(.init(call: call, callUUID: uuid), for: uuid)
507525
}
508526

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

525543
return (uuid, update)
526544
}
545+
546+
// MARK: - Storage Access
547+
548+
private func set(_ value: CallEntry?, for key: UUID) {
549+
storageAccessQueue.sync {
550+
_storage[key] = value
551+
}
552+
}
553+
554+
private func callEntry(for cId: String) -> CallEntry? {
555+
storageAccessQueue.sync {
556+
_storage
557+
.first { $0.value.call.cId == cId }?
558+
.value
559+
}
560+
}
561+
562+
private func callEntry(for uuid: UUID) -> CallEntry? {
563+
storageAccessQueue.sync { _storage[uuid] }
564+
}
527565
}
528566

529567
extension CallKitService: InjectionKey {

StreamVideoTests/CallKit/CallKitServiceTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
387387

388388
await waitExpectation()
389389

390-
XCTAssertEqual(subject.storage.count, 1)
390+
XCTAssertEqual(subject.callCount, 1)
391391

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

411-
XCTAssertEqual(subject.storage.count, 2)
411+
XCTAssertEqual(subject.callCount, 2)
412412

413413
subject.provider(
414414
callProvider,
@@ -417,9 +417,9 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
417417
)
418418
)
419419

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

422-
XCTAssertEqual(subject.storage.count, 1)
422+
XCTAssertEqual(subject.callCount, 1)
423423
}
424424

425425
// MARK: - callEnded

0 commit comments

Comments
 (0)