Skip to content

Commit bb6dca5

Browse files
authored
[Enhancement]CallKit will report ring timeout as reason when appropriate (#820)
1 parent 9a731a3 commit bb6dca5

File tree

7 files changed

+77
-23
lines changed

7 files changed

+77
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010

1111
### 🔄 Changed
1212
- When joining a Call, if the user has an external audio device connected, we will ignore the remote `CallSettings.speakerOn = true`. [#819](https://github.com/GetStream/stream-video-swift/pull/819)
13+
- CallKit will report correctly the rejection reason when ringing timeout is reached. [#820](https://github.com/GetStream/stream-video-swift/pull/820)
1314

1415
### 🐞 Fixed
1516
- Fix a retain cycle that was causing StreamVideo to leak in projects using NoiseCancellation. [#814](https://github.com/GetStream/stream-video-swift/pull/814)

Sources/StreamVideo/CallKit/CallKitService.swift

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
2020
var callUUID: UUID
2121
var createdBy: User?
2222
var isActive: Bool = false
23+
var ringingTimedOut: Bool = false
2324

2425
init(
2526
call: Call,
@@ -100,7 +101,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
100101
.default
101102
.publisher(for: Notification.Name(CallNotification.callEnded))
102103
.compactMap { $0.object as? Call }
103-
.sink { [weak self] in self?.callEnded($0.cId) }
104+
.sink { [weak self] in self?.callEnded($0.cId, ringingTimedOut: false) }
104105
}
105106

106107
/// Reports an incoming call to the CallKit framework.
@@ -151,7 +152,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
151152
""",
152153
subsystems: .callKit
153154
)
154-
callEnded(cid)
155+
callEnded(cid, ringingTimedOut: false)
155156
return
156157
}
157158

@@ -191,7 +192,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
191192
""",
192193
subsystems: .callKit
193194
)
194-
callEnded(cid)
195+
callEnded(cid, ringingTimedOut: false)
195196
}
196197
} catch {
197198
log.error(
@@ -202,7 +203,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
202203
""",
203204
subsystems: .callKit
204205
)
205-
callEnded(cid)
206+
callEnded(cid, ringingTimedOut: false)
206207
}
207208
}
208209
}
@@ -263,10 +264,15 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
263264
}
264265

265266
/// Handles the event when a call ends.
266-
open func callEnded(_ cId: String) {
267+
open func callEnded(
268+
_ cId: String,
269+
ringingTimedOut: Bool
270+
) {
267271
guard let callEndedEntry = callEntry(for: cId) else {
268272
return
269273
}
274+
callEndedEntry.ringingTimedOut = ringingTimedOut
275+
set(callEndedEntry, for: callEndedEntry.callUUID)
270276
Task {
271277
do {
272278
// End the call.
@@ -292,7 +298,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
292298
Task { @MainActor in
293299
if let call = callEntry(for: response.callCid)?.call,
294300
call.state.participants.count == 1 {
295-
callEnded(response.callCid)
301+
callEnded(response.callCid, ringingTimedOut: false)
296302
}
297303
}
298304
}
@@ -449,7 +455,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
449455
.rejectionReasonProvider
450456
.reason(
451457
for: stackEntry.call.cId,
452-
ringTimeout: false
458+
ringTimeout: stackEntry.ringingTimedOut
453459
)
454460
log.debug(
455461
"""
@@ -546,7 +552,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
546552
"Detected ringing timeout, hanging up...",
547553
subsystems: .callKit
548554
)
549-
self?.callEnded(callState.call.cid)
555+
self?.callEnded(callState.call.cid, ringingTimedOut: true)
550556
self?.ringingTimerCancellable = nil
551557
}
552558
}
@@ -582,7 +588,7 @@ open class CallKitService: NSObject, CXProviderDelegate, @unchecked Sendable {
582588
for await event in streamVideo.subscribe() {
583589
switch event {
584590
case let .typeCallEndedEvent(response):
585-
callEnded(response.callCid)
591+
callEnded(response.callCid, ringingTimedOut: false)
586592
case let .typeCallAcceptedEvent(response):
587593
callAccepted(response)
588594
case let .typeCallRejectedEvent(response):

Sources/StreamVideo/Utils/RejectionReasonProvider/RejectionReasonProvider.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ final class StreamRejectionReasonProvider: RejectionReasonProviding, @unchecked
5959
if isUserBusy {
6060
return RejectCallRequest.Reason.busy
6161
} else if isUserRejectingOutgoingCall {
62+
return RejectCallRequest.Reason.cancel
63+
} else {
6264
return ringTimeout
6365
? RejectCallRequest.Reason.timeout
64-
: RejectCallRequest.Reason.cancel
65-
} else {
66-
return RejectCallRequest.Reason.decline
66+
: RejectCallRequest.Reason.decline
6767
}
6868
}
6969
}

StreamVideoSwiftUITests/CallingViews/MicrophoneChecker_Tests.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ import XCTest
1111

1212
final class MicrophoneChecker_Tests: XCTestCase, @unchecked Sendable {
1313

14-
private nonisolated(unsafe) static var originalCallAudioRecorder: StreamCallAudioRecorder! = StreamCallAudioRecorderKey.currentValue
1514
private lazy var mockStreamVideo: MockStreamVideo! = .init()
1615
private lazy var subject: MicrophoneChecker! = .init(valueLimit: 3)
17-
private lazy var mockAudioRecorder: MockStreamCallAudioRecorder! = MockStreamCallAudioRecorder(filename: "test.wav")
16+
private lazy var mockAudioRecorder: MockStreamCallAudioRecorder! = .init(filename: "test.wav")
1817

1918
override func setUp() {
2019
super.setUp()
@@ -24,18 +23,13 @@ final class MicrophoneChecker_Tests: XCTestCase, @unchecked Sendable {
2423

2524
override func tearDown() async throws {
2625
await subject.stopListening()
27-
InjectedValues[\.callAudioRecorder] = Self.originalCallAudioRecorder
26+
InjectedValues[\.callAudioRecorder] = StreamCallAudioRecorder(filename: "test.wav")
2827
mockAudioRecorder = nil
2928
mockStreamVideo = nil
3029
subject = nil
3130
try await super.tearDown()
3231
}
3332

34-
override class func tearDown() {
35-
Self.originalCallAudioRecorder = nil
36-
super.tearDown()
37-
}
38-
3933
// MARK: - init
4034

4135
func test_startListening_startListeningWasCalledOnAudioRecorder() async {

StreamVideoTests/CallKit/CallKitServiceTests.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
352352
XCTFail()
353353
case .callKitActivated:
354354
XCTFail()
355+
case .reject:
356+
XCTFail()
355357
}
356358
}
357359

@@ -418,6 +420,8 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
418420
XCTFail()
419421
case .callKitActivated:
420422
XCTFail()
423+
case .reject:
424+
XCTFail()
421425
}
422426
XCTAssertEqual(call.microphone.status, .enabled)
423427

@@ -606,10 +610,32 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
606610
) { _ in }
607611

608612
try await assertRequestTransaction(CXEndCallAction.self) {
609-
subject.callEnded(cid)
613+
subject.callEnded(cid, ringingTimedOut: false)
610614
}
611615
}
612616

617+
@MainActor
618+
func test_callEnded_ringingTimedOutTrue_expectedTransactionWasRequested() async throws {
619+
let call = stubCall(response: defaultGetCallResponse)
620+
subject.streamVideo = mockedStreamVideo
621+
622+
subject.reportIncomingCall(
623+
cid,
624+
localizedCallerName: localizedCallerName,
625+
callerId: callerId,
626+
hasVideo: false
627+
) { _ in }
628+
629+
try await assertRequestTransaction(CXEndCallAction.self) {
630+
subject.callEnded(cid, ringingTimedOut: true)
631+
}
632+
633+
await fulfillment { call.timesCalled(.reject) == 1 }
634+
635+
let reason = try XCTUnwrap(call.recordedInputPayload(String.self, for: .reject)?.first)
636+
XCTAssertEqual(reason, "timeout")
637+
}
638+
613639
// MARK: - callParticipantLeft
614640

615641
@MainActor
@@ -924,6 +950,7 @@ final class CallKitServiceTests: XCTestCase, @unchecked Sendable {
924950
)
925951
)
926952
call.stub(for: .accept, with: AcceptCallResponse(duration: "0"))
953+
call.stub(for: .reject, with: RejectCallResponse(duration: "0"))
927954
call.stub(for: \.state, with: .init())
928955
mockedStreamVideo.stub(for: .call, with: call)
929956
return call

StreamVideoTests/Mock/MockCall.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ final class MockCall: Call, Mockable, @unchecked Sendable {
1212
enum MockCallFunctionKey: Hashable, CaseIterable {
1313
case get
1414
case accept
15+
case reject
1516
case join
1617
case updateTrackSize
1718
case callKitActivated
@@ -30,6 +31,8 @@ final class MockCall: Call, Mockable, @unchecked Sendable {
3031

3132
case callKitActivated(audioSession: AVAudioSessionProtocol)
3233

34+
case reject(reason: String?)
35+
3336
var payload: Any {
3437
switch self {
3538
case let .join(create, options, ring, notify, callSettings):
@@ -40,6 +43,9 @@ final class MockCall: Call, Mockable, @unchecked Sendable {
4043

4144
case let .callKitActivated(audioSession):
4245
return audioSession
46+
47+
case let .reject(reason):
48+
return reason ?? ""
4349
}
4450
}
4551
}
@@ -91,6 +97,11 @@ final class MockCall: Call, Mockable, @unchecked Sendable {
9197
stubbedFunction[.accept] as! AcceptCallResponse
9298
}
9399

100+
override func reject(reason: String? = nil) async throws -> RejectCallResponse {
101+
stubbedFunctionInput[.reject]?.append(.reject(reason: reason))
102+
return stubbedFunction[.reject] as! RejectCallResponse
103+
}
104+
94105
override func join(
95106
create: Bool = false,
96107
options: CreateCallOptions? = nil,

StreamVideoTests/Utils/RejectionReasonProvider/RejectionReasonProvider_Tests.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class StreamRejectionReasonProviderTests: XCTestCase, @unchecked Sendable
3232
}
3333

3434
@MainActor
35-
func test_rejectionReason_givenRingingCallWithMatchingCidAndRingTimeout_whenUserIsRejectingOutgoingCall_thenReturnsTimeoutReason(
35+
func test_rejectionReason_givenRingingCallWithMatchingCidAndRingTimeout_whenUserIsRejectingOutgoingCall_thenReturnsCancelReason(
3636
) async {
3737
let ringingCall = MockCall(.dummy())
3838
mockStreamVideo.state.ringingCall = ringingCall
@@ -43,7 +43,7 @@ final class StreamRejectionReasonProviderTests: XCTestCase, @unchecked Sendable
4343
ringTimeout: true
4444
)
4545

46-
XCTAssertEqual(reason, RejectCallRequest.Reason.timeout)
46+
XCTAssertEqual(reason, RejectCallRequest.Reason.cancel)
4747
}
4848

4949
@MainActor
@@ -76,6 +76,21 @@ final class StreamRejectionReasonProviderTests: XCTestCase, @unchecked Sendable
7676
XCTAssertEqual(reason, RejectCallRequest.Reason.decline)
7777
}
7878

79+
@MainActor
80+
func test_rejectionReason_givenRingingCallWithMatchingCidAndRingTimeout_whenUserIsNotBusyAndNotRejectingOutgoingCall_thenReturnsTimeoutReason(
81+
) async {
82+
let ringingCall = MockCall(.dummy())
83+
mockStreamVideo.state.ringingCall = ringingCall
84+
ringingCall.state.createdBy = .dummy()
85+
86+
let reason = await subject.reason(
87+
for: ringingCall.cId,
88+
ringTimeout: true
89+
)
90+
91+
XCTAssertEqual(reason, RejectCallRequest.Reason.timeout)
92+
}
93+
7994
@MainActor
8095
func test_rejectionReason_givenNoRingingCallMatchingCid_thenReturnsNil() async {
8196
let ringingCall = MockCall(.dummy())

0 commit comments

Comments
 (0)