diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 803786ab..bb34f583 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -322,6 +322,21 @@ import flutter_foreground_task return nil } + private func extraMarksEndedPush(_ extra: [String: Any]) -> Bool { + let value = extra["endedPush"] + if let bool = value as? Bool { + return bool + } + if let number = value as? NSNumber { + return number.intValue != 0 + } + if let text = value as? String { + let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized == "true" || normalized == "1" + } + return false + } + private func voipBool(_ value: Any?) -> Bool? { if let bool = value as? Bool { return bool @@ -1106,6 +1121,22 @@ import flutter_foreground_task resumeAudioAfterCallKitEnds = false audioRestartGeneration += 1 callKitOwnsAudioSession = false + // Accept reports the CallKit payload on this channel, and that is what + // stops ringing on the other devices. The plugin decline event does not. + let extra = call.data.extra as? [String: Any] ?? [:] + if !extraMarksEndedPush(extra) { + recordVoipDiagnostic( + "callkit_decline_native", + data: [ + "callKitId": call.data.uuid, + "conversationUuid": conversationUuid(fromCall: call) ?? "", + ] + ) + voipDiagnosticsChannel?.invokeMethod( + "declinedVoipCall", + arguments: call.data.toJSON() + ) + } action.fulfill() } diff --git a/lib/features/video_calls/services/video_call_callkit_coordinator.dart b/lib/features/video_calls/services/video_call_callkit_coordinator.dart index f53930f3..fb0157ec 100644 --- a/lib/features/video_calls/services/video_call_callkit_coordinator.dart +++ b/lib/features/video_calls/services/video_call_callkit_coordinator.dart @@ -79,6 +79,8 @@ class VideoCallCallKitCoordinator { _scheduleNativeVoipDiagnosticsDrain(); case 'acceptedVoipCall': await _handleNativeAcceptedVoipCall(call.arguments); + case 'declinedVoipCall': + await _handleNativeDeclinedVoipCall(call.arguments); case 'tombstoneCallId': _handleNativeTombstoneCallId(call.arguments); } @@ -390,7 +392,11 @@ class VideoCallCallKitCoordinator { ); return; } - await _handleDecline(id, eventExtra: _callKitEventParams(event)?.extra); + await _handleDecline( + id, + eventExtra: _callKitEventParams(event)?.extra, + skipBackendNotify: Platform.isIOS && event is CallEventActionCallDecline, + ); } Future _handleEndedEvent(CallEvent event) async { @@ -615,6 +621,39 @@ class VideoCallCallKitCoordinator { ); } + /// Same payload accept uses. The plugin decline event is not what stops + /// ringing on the other devices. + Future _handleNativeDeclinedVoipCall(Object? arguments) async { + if (!Platform.isIOS) return; + + final data = _stringKeyedMap(arguments); + final extra = _stringKeyedMap(data['extra']); + if (callKitExtraMarksEndedPush(Map.from(extra))) return; + + final callKitId = _parseString(data, const ['id', 'uuid']); + if (callKitId != null && + (_suppressedEndCallKitIds.contains(callKitId) || + VideoCallRemoteEndRegistry.contains(callKitId))) { + return; + } + + final conversationUuid = _parseConversationUuid( + extra, + handle: _parseString(data, const ['handle']), + ); + final callId = backendVideoCallId( + callId: + _parseCallId(extra) ?? + (callKitId != null && callKitId.contains('-') ? callKitId : null), + conversationUuid: conversationUuid, + ); + log( + 'VideoCallCallKitCoordinator: native declinedVoipCall conversation=$conversationUuid callId=$callId', + name: 'VideoCallCallKit', + ); + await _manager.declineIncomingCall(conversationUuid, callId: callId); + } + Future _handleAccept(String callKitId) async { if (_acceptNavigationCompletedIds.contains(callKitId)) return; @@ -976,6 +1015,7 @@ class VideoCallCallKitCoordinator { Future _handleDecline( String callKitId, { Map? eventExtra, + bool skipBackendNotify = false, }) async { if (_suppressedEndCallKitIds.contains(callKitId)) { log( @@ -1015,6 +1055,13 @@ class VideoCallCallKitCoordinator { return; } + if (skipBackendNotify) { + // iOS decline is reported from AppDelegate with the CallKit payload, + // the same way accept is. This plugin event must not send /ended instead. + _forgetCallKitId(callKitId); + return; + } + _suppressedEndCallKitIds.add(callKitId); try { await FlutterCallkitIncoming.endCall(callKitId); diff --git a/pubspec.yaml b/pubspec.yaml index d737573a..aea7a6ae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.3.65+154 +version: 1.3.66+155 publish_to: 'none' environment: