From fd2dffb7be589962cf758868702e2b9acf111fcc Mon Sep 17 00:00:00 2001 From: Kanat Kiialbaev Date: Fri, 6 Oct 2023 16:56:35 -0700 Subject: [PATCH] Do not join active call (#499) --- dogfooding/lib/app/app_content.dart | 14 ++-- packages/stream_video/lib/src/call/call.dart | 66 ++++++++++++------- .../lib/src/core/client_state.dart | 3 + .../push_notification/call_kit_events.dart | 63 +++++++++++++++++- .../push_notification_manager.dart | 1 + .../stream_video/lib/src/stream_video.dart | 10 ++- .../src/stream_video_push_notification.dart | 6 +- 7 files changed, 127 insertions(+), 36 deletions(-) diff --git a/dogfooding/lib/app/app_content.dart b/dogfooding/lib/app/app_content.dart index f922e4976..448f1b00b 100644 --- a/dogfooding/lib/app/app_content.dart +++ b/dogfooding/lib/app/app_content.dart @@ -1,20 +1,15 @@ -// 🎯 Dart imports: import 'dart:async'; -// 🐦 Flutter imports: import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; - -// 📦 Package imports: -import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_dogfooding/router/routes.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart' hide User; import 'package:stream_video_flutter/stream_video_flutter.dart'; import 'package:uni_links/uni_links.dart'; -// 🌎 Project imports: -import 'package:flutter_dogfooding/router/routes.dart'; import '../core/repos/app_preferences.dart'; import '../di/injector.dart'; import '../firebase_options.dart'; @@ -67,6 +62,7 @@ class _StreamDogFoodingAppContentState extends State { late final _userAuthController = locator.get(); + late final _logger = taggedLogger(tag: 'StreamDogFoodingAppContent'); late final _router = initRouter(_userAuthController); @override @@ -150,6 +146,7 @@ class _StreamDogFoodingAppContentState } void _onCallAccept(ActionCallAccept event) async { + _logger.d(() => '[onCallAccept] event: $event'); final streamVideo = locator.get(); final uuid = event.data.uuid; @@ -163,7 +160,7 @@ class _StreamDogFoodingAppContentState var acceptResult = await callToJoin.accept(); // Return if cannot accept call - if(acceptResult.isFailure) { + if (acceptResult.isFailure) { debugPrint('Error accepting call: $call'); return; } @@ -177,6 +174,7 @@ class _StreamDogFoodingAppContentState } void _onCallDecline(ActionCallDecline event) async { + _logger.d(() => '[onCallDecline] event: $event'); final streamVideo = locator.get(); final uuid = event.data.uuid; diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index 69bcdc931..ecdde9dc7 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -30,6 +30,7 @@ typedef OnCallPermissionRequest = void Function( typedef GetCurrentUserId = String? Function(); typedef SetActiveCall = Future Function(Call?); +typedef GetActiveCallCid = StreamCallCid? Function(); const _idState = 1; const _idUserId = 2; @@ -53,6 +54,7 @@ class Call { required CoordinatorClient coordinatorClient, required StateEmitter currentUser, required SetActiveCall setActiveCall, + required GetActiveCallCid getActiveCallCid, RetryPolicy? retryPolicy, SdpPolicy? sdpPolicy, CallPreferences? preferences, @@ -63,6 +65,7 @@ class Call { coordinatorClient: coordinatorClient, currentUser: currentUser, setActiveCall: setActiveCall, + getActiveCallCid: getActiveCallCid, retryPolicy: retryPolicy, sdpPolicy: sdpPolicy, preferences: preferences, @@ -77,6 +80,7 @@ class Call { required CoordinatorClient coordinatorClient, required StateEmitter currentUser, required SetActiveCall setActiveCall, + required GetActiveCallCid getActiveCallCid, RetryPolicy? retryPolicy, SdpPolicy? sdpPolicy, CallPreferences? preferences, @@ -87,6 +91,7 @@ class Call { coordinatorClient: coordinatorClient, currentUser: currentUser, setActiveCall: setActiveCall, + getActiveCallCid: getActiveCallCid, retryPolicy: retryPolicy, sdpPolicy: sdpPolicy, preferences: preferences, @@ -101,6 +106,7 @@ class Call { required CoordinatorClient coordinatorClient, required StateEmitter currentUser, required SetActiveCall setActiveCall, + required GetActiveCallCid getActiveCallCid, RetryPolicy? retryPolicy, SdpPolicy? sdpPolicy, CallPreferences? preferences, @@ -111,6 +117,7 @@ class Call { coordinatorClient: coordinatorClient, currentUser: currentUser, setActiveCall: setActiveCall, + getActiveCallCid: getActiveCallCid, retryPolicy: retryPolicy, sdpPolicy: sdpPolicy, preferences: preferences, @@ -122,6 +129,7 @@ class Call { required CoordinatorClient coordinatorClient, required StateEmitter currentUser, required SetActiveCall setActiveCall, + required GetActiveCallCid getActiveCallCid, RetryPolicy? retryPolicy, SdpPolicy? sdpPolicy, CallPreferences? preferences, @@ -145,6 +153,7 @@ class Call { coordinatorClient: coordinatorClient, currentUser: currentUser, setActiveCall: setActiveCall, + getActiveCallCid: getActiveCallCid, preferences: finalCallPreferences, stateManager: stateManager, credentials: credentials, @@ -157,6 +166,7 @@ class Call { Call._({ required StateEmitter currentUser, required SetActiveCall setActiveCall, + required GetActiveCallCid getActiveCallCid, required CoordinatorClient coordinatorClient, required CallPreferences preferences, required CallStateNotifier stateManager, @@ -177,6 +187,7 @@ class Call { .whereNotNull() .distinct(), _setActiveCall = setActiveCall, + _getActiveCallCid = getActiveCallCid, _coordinatorClient = coordinatorClient, _preferences = preferences, _retryPolicy = retryPolicy, @@ -196,6 +207,7 @@ class Call { final GetCurrentUserId _getCurrentUserId; final Stream _currentUserIdUpdates; final SetActiveCall _setActiveCall; + final GetActiveCallCid _getActiveCallCid; final CoordinatorClient _coordinatorClient; final RetryPolicy _retryPolicy; final CallPreferences _preferences; @@ -383,15 +395,15 @@ class Call { @Deprecated('Lobby view no longer needs joining to coordinator') Future> joinLobby() async { - _logger.d(() => '[join] no args'); + _logger.d(() => '[joinLobby] no args'); _stateManager.lifecycleCallJoining(const CallJoining()); final joinedResult = await _joinIfNeeded(); if (joinedResult is Success) { - _logger.v(() => '[join] completed'); + _logger.v(() => '[joinLobby] completed'); return const Result.success(none); } else { final failedResult = joinedResult as Failure; - _logger.e(() => '[join] failed: $failedResult'); + _logger.e(() => '[joinLobby] failed: $failedResult'); final error = failedResult.error; _stateManager.lifecycleCallConnectFailed(ConnectFailed(error)); return failedResult; @@ -399,13 +411,19 @@ class Call { } Future> join() async { - _logger.i(() => '[connect] status: ${_status.value}'); + _logger.i(() => '[join] status: ${_status.value}'); if (_status.value == _ConnectionStatus.connected) { - _logger.w(() => '[connect] rejected (connected)'); + _logger.w(() => '[join] rejected (connected)'); return const Result.success(none); } + if (_getActiveCallCid() == callCid) { + _logger.w( + () => '[join] rejected (a call with the same cid is in progress)', + ); + return Result.error('a call with the same cid is in progress'); + } if (_status.value == _ConnectionStatus.connecting) { - _logger.v(() => '[connect] await "connecting" change'); + _logger.v(() => '[join] await "connecting" change'); final status = await _status.firstWhere( (it) => it != _ConnectionStatus.connecting, timeLimit: _preferences.connectTimeout, @@ -423,10 +441,10 @@ class Call { .storeIn(_idConnect, _cancelables) .valueOrDefault(Result.error('connect cancelled')); if (result.isSuccess) { - _logger.v(() => '[connect] finished: $result'); + _logger.v(() => '[join] finished: $result'); _status.value = _ConnectionStatus.connected; } else { - _logger.e(() => '[connect] failed: $result'); + _logger.e(() => '[join] failed: $result'); await leave(); } return result; @@ -458,18 +476,18 @@ class Call { } Future> _connect() async { - _logger.d(() => '[connect] options: $_connectOptions'); + _logger.d(() => '[join] options: $_connectOptions'); final validation = await _stateManager.validateUserId(_getCurrentUserId); if (validation.isFailure) { - _logger.w(() => '[connect] rejected (validation): $validation'); + _logger.w(() => '[join] rejected (validation): $validation'); return validation; } - _logger.v(() => '[connect] validated'); + _logger.v(() => '[join] validated'); final state = this.state.value; final status = state.status; if (!status.isConnectable) { - _logger.w(() => '[connect] rejected (not Connectable): $status'); + _logger.w(() => '[join] rejected (not Connectable): $status'); return Result.error('invalid status: $status'); } _observeState(); @@ -477,7 +495,7 @@ class Call { _observeUserId(); final result = await _awaitIfNeeded(); if (result.isFailure) { - _logger.e(() => '[connect] waiting failed: $result'); + _logger.e(() => '[join] waiting failed: $result'); _stateManager.lifecycleCallTimeout(const CallTimeout()); @@ -486,28 +504,28 @@ class Call { _stateManager .lifecycleCallConnectingAction(CallConnecting(_reconnectAttempt)); - _logger.v(() => '[connect] joining to coordinator'); + _logger.v(() => '[join] joining to coordinator'); final joinedResult = await _joinIfNeeded(); if (joinedResult is! Success) { - _logger.e(() => '[connect] joining failed: $joinedResult'); + _logger.e(() => '[join] coordinator joining failed: $joinedResult'); final error = (joinedResult as Failure).error; _stateManager.lifecycleCallConnectFailed(ConnectFailed(error)); return result; } - _logger.v(() => '[connect] starting sfu session'); + _logger.v(() => '[join] starting sfu session'); final sessionResult = await _startSession(joinedResult.data); if (sessionResult is! Success) { - _logger.w(() => '[connect] sfu session start failed: $sessionResult'); + _logger.w(() => '[join] sfu session start failed: $sessionResult'); final error = (sessionResult as Failure).error; _stateManager.lifecycleCallConnectFailed(ConnectFailed(error)); return sessionResult; } - _logger.v(() => '[connect] started session'); + _logger.v(() => '[join] started session'); _stateManager.lifecycleCallConnected(const CallConnected()); await _applyConnectOptions(); - _logger.v(() => '[connect] completed'); + _logger.v(() => '[join] completed'); return const Result.success(none); } @@ -662,19 +680,19 @@ class Call { Future> leave() async { final state = this.state.value; - _logger.i(() => '[disconnect] ${_status.value}; state: $state'); + _logger.i(() => '[leave] ${_status.value}; state: $state'); if (state.status.isDisconnected) { - _logger.w(() => '[disconnect] rejected (state.status is disconnected)'); + _logger.w(() => '[leave] rejected (state.status is disconnected)'); return const Result.success(none); } if (_status.value == _ConnectionStatus.disconnected) { - _logger.w(() => '[disconnect] rejected (status is disconnected)'); + _logger.w(() => '[leave] rejected (status is disconnected)'); return const Result.success(none); } _status.value = _ConnectionStatus.disconnected; - await _clear('disconnect'); + await _clear('leave'); _stateManager.lifecycleCallDisconnected(const CallDisconnected()); - _logger.v(() => '[disconnect] finished'); + _logger.v(() => '[leave] finished'); return const Result.success(none); } diff --git a/packages/stream_video/lib/src/core/client_state.dart b/packages/stream_video/lib/src/core/client_state.dart index 774940067..4dc36ef64 100644 --- a/packages/stream_video/lib/src/core/client_state.dart +++ b/packages/stream_video/lib/src/core/client_state.dart @@ -1,4 +1,5 @@ import '../call/call.dart'; +import '../models/call_cid.dart'; import '../models/user.dart'; import '../shared_emitter.dart'; import '../state_emitter.dart'; @@ -53,6 +54,8 @@ class MutableClientState implements ClientState { connection.value = ConnectionState.disconnected(user.value.id); } + StreamCallCid? getActiveCallCid() => activeCall.valueOrNull!.callCid; + Future setActiveCall(Call? call) async { final ongoingCall = activeCall.valueOrNull; if (ongoingCall != null && call != null) { diff --git a/packages/stream_video/lib/src/push_notification/call_kit_events.dart b/packages/stream_video/lib/src/push_notification/call_kit_events.dart index 99f7c962e..01edb39d0 100644 --- a/packages/stream_video/lib/src/push_notification/call_kit_events.dart +++ b/packages/stream_video/lib/src/push_notification/call_kit_events.dart @@ -4,8 +4,11 @@ part of 'push_notification_manager.dart'; /// /// Instances of this class are used to signify different call events that can be /// received from [PushNotificationManager]. -sealed class CallKitEvent { +sealed class CallKitEvent with EquatableMixin { const CallKitEvent(); + + @override + bool? get stringify => true; } /// Event for updating the VoIP push token on the device (iOS specific). @@ -16,6 +19,9 @@ class ActionDidUpdateDevicePushTokenVoip extends CallKitEvent { /// The updated device push token for VoIP. final String token; + + @override + List get props => [token]; } /// Represents an incoming call event. @@ -27,6 +33,9 @@ class ActionCallIncoming extends CallKitEvent { /// The call data associated with the incoming call. final CallData data; + + @override + List get props => [data]; } /// Represents a call start event. @@ -38,6 +47,9 @@ class ActionCallStart extends CallKitEvent { /// The call data associated with the outgoing call. final CallData data; + + @override + List get props => [data]; } /// Represents a call accept event. @@ -51,6 +63,9 @@ class ActionCallAccept extends CallKitEvent { /// The call data associated with the call that was accepted. final CallData data; + + @override + List get props => [data]; } /// Represents a call decline event. @@ -64,6 +79,9 @@ class ActionCallDecline extends CallKitEvent { /// The call data associated with the call that was declined. final CallData data; + + @override + List get props => [data]; } /// Represents a call end event. @@ -76,6 +94,9 @@ class ActionCallEnded extends CallKitEvent { /// The call data associated with the call that ended. final CallData data; + + @override + List get props => [data]; } /// Represents a call timeout event. @@ -88,6 +109,9 @@ class ActionCallTimeout extends CallKitEvent { /// The call data associated with the call that timed out. final CallData data; + + @override + List get props => [data]; } /// Represents a call callback event. @@ -102,6 +126,9 @@ class ActionCallCallback extends CallKitEvent { /// The call data associated with the call that was called back. final CallData data; + + @override + List get props => [data]; } /// Represents a call toggle hold event. @@ -120,6 +147,9 @@ class ActionCallToggleHold extends CallKitEvent { /// Indicates whether the call is on hold. final bool isOnHold; + + @override + List get props => [uuid, isOnHold]; } /// Represents a call toggle mute event. @@ -138,6 +168,9 @@ class ActionCallToggleMute extends CallKitEvent { /// Indicates whether the call is muted. final bool isMuted; + + @override + List get props => [uuid, isMuted]; } /// Represents a call toggle DMTF event. @@ -156,6 +189,9 @@ class ActionCallToggleDmtf extends CallKitEvent { /// The digits to send. final String digits; + + @override + List get props => [uuid, digits]; } /// Represents a call toggle group event. @@ -174,6 +210,9 @@ class ActionCallToggleGroup extends CallKitEvent { /// The unique identifier for the call to group with. final String callUUIDToGroupWith; + + @override + List get props => [uuid, callUUIDToGroupWith]; } /// Represents a call toggle audio session event. @@ -186,6 +225,9 @@ class ActionCallToggleAudioSession extends CallKitEvent { /// Indicates whether the audio session is active. final bool isActivate; + + @override + List get props => [isActivate]; } /// Represents a custom call event. @@ -195,6 +237,9 @@ class ActionCallCustom extends CallKitEvent { /// The custom data associated with the call. final Map? body; + + @override + List get props => [body]; } /// Represents call data with various properties related to the call. @@ -202,7 +247,7 @@ class ActionCallCustom extends CallKitEvent { /// This class encapsulates information about an ongoing or past call, including /// unique identifiers, caller's avatar, handle, name, video availability, and /// additional extra data associated with the call. -class CallData { +class CallData with EquatableMixin { /// Creates a [CallData] instance with the provided details. const CallData({ this.uuid, @@ -234,4 +279,18 @@ class CallData { /// Extra data associated with the call. final Map? extraData; + + @override + bool? get stringify => true; + + @override + List get props => [ + uuid, + callCid, + avatar, + handle, + nameCaller, + hasVideo, + extraData, + ]; } diff --git a/packages/stream_video/lib/src/push_notification/push_notification_manager.dart b/packages/stream_video/lib/src/push_notification/push_notification_manager.dart index 53679c16c..35de015e8 100644 --- a/packages/stream_video/lib/src/push_notification/push_notification_manager.dart +++ b/packages/stream_video/lib/src/push_notification/push_notification_manager.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index d7782fbfe..1e677f641 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -431,6 +431,7 @@ class StreamVideo { coordinatorClient: _client, currentUser: _state.user, setActiveCall: _state.setActiveCall, + getActiveCallCid: _state.getActiveCallCid, retryPolicy: _options.retryPolicy, sdpPolicy: _options.sdpPolicy, preferences: preferences, @@ -446,6 +447,7 @@ class StreamVideo { coordinatorClient: _client, currentUser: _state.user, setActiveCall: _state.setActiveCall, + getActiveCallCid: _state.getActiveCallCid, retryPolicy: _options.retryPolicy, sdpPolicy: _options.sdpPolicy, preferences: preferences, @@ -476,6 +478,9 @@ class StreamVideo { String? pushProviderName, bool? voipToken, }) { + _logger.d(() => '[addDevice] pushProvider: $pushProvider' + ', pushToken: $pushToken, pushProviderName: $pushProviderName' + ', voipToken: $voipToken'); return _client.createDevice( id: pushToken, pushProvider: pushProvider, @@ -494,6 +499,7 @@ class StreamVideo { Future> removeDevice({ required String pushToken, }) { + _logger.d(() => '[removeDevice] pushToken: $pushToken'); return _client.deleteDevice(id: pushToken, userId: currentUser.id); } @@ -502,7 +508,7 @@ class StreamVideo { ) { final manager = pushNotificationManager; if (manager == null) { - _logger.e(() => '[on] rejected (no manager)'); + _logger.e(() => '[onCallKitEvent] rejected (no manager)'); return null; } @@ -513,6 +519,7 @@ class StreamVideo { /// /// Returns `true` if the notification was handled, `false` otherwise. Future handleVoipPushNotification(Map payload) async { + _logger.d(() => '[handleVoipPushNotification] payload: $payload'); final manager = pushNotificationManager; if (manager == null) { _logger.e(() => '[handleVoipPushNotification] rejected (no manager)'); @@ -552,6 +559,7 @@ class StreamVideo { required String uuid, required String cid, }) async { + _logger.d(() => '[consumeIncomingCall] uuid: $uuid, cid: $cid'); final manager = pushNotificationManager; if (manager == null) { return const Result.failure( diff --git a/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart b/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart index 534832f06..81450aea0 100644 --- a/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart +++ b/packages/stream_video_push_notification/lib/src/stream_video_push_notification.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:stream_video/stream_video.dart'; import 'stream_video_push_params.dart'; @@ -82,7 +83,10 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { @override Stream get onCallEvent { - return StreamCallKit().onEvent.map((event) => event.toCallKitEvent()); + return StreamCallKit() + .onEvent + .map((event) => event.toCallKitEvent()) + .doOnData((event) => _logger.v(() => '[onCallKitEvent] event: $event')); } @override