From d81d0ed3a7544848dca5c3aeceebf50869a2ce3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Bra=C5=BCewicz?= Date: Fri, 9 Feb 2024 10:53:41 +0100 Subject: [PATCH] feat: push notification enhancements (#580) * push notification enhancements * added changelog * changelog tweak * fix for accepting call * changed name to id --- packages/stream_video/lib/src/call/call.dart | 6 +- .../lib/src/call/call_ringing_state.dart | 6 ++ .../push_notification_manager.dart | 1 + .../stream_video/lib/src/stream_video.dart | 94 +++++++++++++++++-- packages/stream_video/lib/stream_video.dart | 1 + packages/stream_video_flutter/CHANGELOG.md | 14 +++ .../src/stream_video_push_notification.dart | 90 ++++++++++++------ 7 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 packages/stream_video/lib/src/call/call_ringing_state.dart diff --git a/packages/stream_video/lib/src/call/call.dart b/packages/stream_video/lib/src/call/call.dart index 23ea8ba38..1365a3234 100644 --- a/packages/stream_video/lib/src/call/call.dart +++ b/packages/stream_video/lib/src/call/call.dart @@ -371,7 +371,8 @@ class Call { Future> reject() async { final state = this.state.value; final status = state.status; - if (status is! CallStatusIncoming || status.acceptedByMe) { + if ((status is! CallStatusIncoming || status.acceptedByMe) && + status is! CallStatusOutgoing) { _logger.w(() => '[rejectCall] rejected (invalid status): $status'); return Result.error('invalid status: $status'); } @@ -502,6 +503,7 @@ class Call { if (result.isFailure) { _logger.e(() => '[join] waiting failed: $result'); + await reject(); _stateManager.lifecycleCallTimeout(const CallTimeout()); return result; @@ -1050,7 +1052,7 @@ class Call { final response = await _coordinatorClient.getOrCreateCall( callCid: callCid, ringing: ringing, - members: memberIds.map((id) { + members: {...memberIds, currentUserId}.map((id) { return MemberRequest( userId: id, role: 'admin', diff --git a/packages/stream_video/lib/src/call/call_ringing_state.dart b/packages/stream_video/lib/src/call/call_ringing_state.dart new file mode 100644 index 000000000..0025b06fd --- /dev/null +++ b/packages/stream_video/lib/src/call/call_ringing_state.dart @@ -0,0 +1,6 @@ +enum CallRingingState { + ended, + rejected, + accepted, + ringing, +} 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 35de015e8..1d1a7404e 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 @@ -12,6 +12,7 @@ part 'call_kit_events.dart'; /// [PushNotificationManager]. typedef PNManagerProvider = PushNotificationManager Function( CoordinatorClient client, + StreamVideo streamVideo, ); /// Interface for managing push notifications related to call events. diff --git a/packages/stream_video/lib/src/stream_video.dart b/packages/stream_video/lib/src/stream_video.dart index 759cac954..ae59c0382 100644 --- a/packages/stream_video/lib/src/stream_video.dart +++ b/packages/stream_video/lib/src/stream_video.dart @@ -5,6 +5,7 @@ import 'package:uuid/uuid.dart'; import '../open_api/video/coordinator/api.dart' as open; import 'call/call.dart'; +import 'call/call_ringing_state.dart'; import 'coordinator/coordinator_client.dart'; import 'coordinator/models/coordinator_events.dart'; import 'coordinator/open_api/coordinator_client_open_api.dart'; @@ -141,7 +142,8 @@ class StreamVideo { ); // Initialize the push notification manager if the provider is provided. - pushNotificationManager = pushNotificationManagerProvider?.call(_client); + pushNotificationManager = + pushNotificationManagerProvider?.call(_client, this); _state.user.value = user; final tokenProvider = switch (user.type) { @@ -574,24 +576,96 @@ class StreamVideo { if (callCid == null) return false; var callId = const Uuid().v4(); + var callType = 'default'; + final splitCid = callCid.split(':'); if (splitCid.length == 2) { + callType = splitCid.first; callId = splitCid.last; } final createdById = payload['created_by_id'] as String?; final createdByName = payload['created_by_display_name'] as String?; - unawaited( - manager.showIncomingCall( - uuid: callId, - handle: createdById, - nameCaller: createdByName, - callCid: callCid, - ), - ); + final callRingingState = + await getCallRingingState(type: callType, id: callId); + + switch (callRingingState) { + case CallRingingState.ringing: + unawaited( + manager.showIncomingCall( + uuid: callId, + handle: createdById, + nameCaller: createdByName, + callCid: callCid, + ), + ); + return true; + case CallRingingState.accepted: + return false; + case CallRingingState.rejected: + return false; + case CallRingingState.ended: + unawaited( + manager.showMissedCall( + uuid: callId, + handle: createdById, + nameCaller: createdByName, + callCid: callCid, + ), + ); + return false; + } + } + + Future getCallRingingState({ + required String type, + required String id, + }) async { + final call = makeCall(type: type, id: id); + final callResult = await call.get(); - return true; + return callResult.fold( + failure: (failure) { + _logger.e(() => '[getCallRingingState] failed: $failure'); + return CallRingingState.ended; + }, + success: (success) { + final callData = success.data; + + if (callData.metadata.details.endedAt != null) { + _logger.e(() => '[getCallRingingState] call already ended'); + + return CallRingingState.ended; + } + + if (callData.metadata.session.acceptedBy + .containsKey(_state.currentUser.id)) { + _logger.e(() => '[getCallRingingState] call already accepted'); + return CallRingingState.accepted; + } + + if (callData.metadata.session.rejectedBy + .containsKey(_state.currentUser.id)) { + _logger.e(() => '[getCallRingingState] call already rejected'); + return CallRingingState.rejected; + } + + final otherMembers = callData.metadata.members.keys.toList() + ..remove(_state.currentUser.id); + if (callData.metadata.session.rejectedBy.keys + .toSet() + .containsAll(otherMembers)) { + _logger.e( + () => + '[getCallRingingState] call already rejected by all other members', + ); + return CallRingingState.rejected; + } + + return CallRingingState.ringing; + }, + ); } /// Consumes incoming voIP call and returns the [Call] object. diff --git a/packages/stream_video/lib/stream_video.dart b/packages/stream_video/lib/stream_video.dart index f92f3f1a9..120bc16e2 100644 --- a/packages/stream_video/lib/stream_video.dart +++ b/packages/stream_video/lib/stream_video.dart @@ -4,6 +4,7 @@ export 'open_api/video/coordinator/api.dart'; export 'src/action/participant_action.dart'; export 'src/call/call.dart'; export 'src/call/call_connect_options.dart'; +export 'src/call/call_ringing_state.dart'; export 'src/call_state.dart'; export 'src/coordinator/coordinator_client.dart'; export 'src/coordinator/models/coordinator_events.dart'; diff --git a/packages/stream_video_flutter/CHANGELOG.md b/packages/stream_video_flutter/CHANGELOG.md index 21e808541..489265283 100644 --- a/packages/stream_video_flutter/CHANGELOG.md +++ b/packages/stream_video_flutter/CHANGELOG.md @@ -1,5 +1,19 @@ ## Upcoming +🐞 Fixed + +* Various fixes to call ringing and push notifications + - fixes call ringing cancelation when app is terminated on iOS (requires additional setup - check Step 6 of the [APNS integration](https://getstream.io/video/docs/flutter/advanced/adding_ringing_and_callkit/#integrating-apns-for-ios)) in our documentations + - fixes late push notification handling on Android, where already ended call was ringing if the device was offline and the push was delivered with a delay + - fixes call ringing cancelation when caller timed out while calling +* Fixed background image for incoming/outgoing call screens when `participant.image` is invalid +* Fixed action tap callback on Android call notification +* Fixes possible crashes for Android SDKs versions <26 +* Fixed screen sharing on iOS when screen sharing mode was switched between `in-app` and `broadcast` +* Changed the version range of `intl` package to >=0.18.1 <=0.19.0 because it was causing isses with other packages + +✅ Added + * Added `custom` field to `CallParticipantState` with custom user data. ## 0.3.1 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 d36226392..fe90eab1c 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 @@ -17,6 +17,7 @@ const _idCallKitIncoming = 2; const _idCallEnded = 3; const _idCallAccepted = 4; const _idCallKitAcceptDecline = 5; +const _idCallRejected = 6; /// Implementation of [PushNotificationManager] for Stream Video. class StreamVideoPushNotificationManager implements PushNotificationManager { @@ -30,7 +31,7 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { BackgroundVoipCallHandler? backgroundVoipCallHandler, StreamVideoPushParams? pushParams, }) { - return (CoordinatorClient client) { + return (CoordinatorClient client, StreamVideo streamVideo) { final params = _defaultPushParams.merge(pushParams); if (CurrentPlatform.isIos) { @@ -43,6 +44,7 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { return StreamVideoPushNotificationManager._( client: client, + streamVideo: streamVideo, iosPushProvider: iosPushProvider, androidPushProvider: androidPushProvider, pushParams: params, @@ -53,22 +55,68 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { StreamVideoPushNotificationManager._({ required CoordinatorClient client, + required StreamVideo streamVideo, required this.iosPushProvider, required this.androidPushProvider, required this.pushParams, this.callerCustomizationCallback, }) : _client = client { - //if there are active calls (for iOS) when connecting, subscribe to end call event + subscribeToEvents() { + _subscriptions.add( + _idCallEnded, + client.events.on( + (event) { + FlutterCallkitIncoming.endCall(event.callCid.id); + }, + ), + ); + + _subscriptions.add( + _idCallRejected, + client.events.on( + (event) async { + final callRingingState = await streamVideo.getCallRingingState( + type: event.callCid.type, id: event.callCid.id); + + switch (callRingingState) { + case CallRingingState.accepted: + case CallRingingState.rejected: + case CallRingingState.ended: + FlutterCallkitIncoming.endCall(event.callCid.id); + case CallRingingState.ringing: + break; + } + }, + ), + ); + + _subscriptions.add( + _idCallAccepted, + client.events.on( + (event) async { + final callRingingState = await streamVideo.getCallRingingState( + type: event.callCid.type, id: event.callCid.id); + + switch (callRingingState) { + case CallRingingState.accepted: + case CallRingingState.rejected: + case CallRingingState.ended: + await FlutterCallkitIncoming.silenceEvents(); + await FlutterCallkitIncoming.endCall(event.callCid.id); + await Future.delayed(const Duration(milliseconds: 300)); + await FlutterCallkitIncoming.unsilenceEvents(); + case CallRingingState.ringing: + break; + } + }, + ), + ); + } + + //if there are active calls (for iOS) when connecting, subscribe to events as if the call was incoming FlutterCallkitIncoming.activeCalls().then((value) { if (value is List && value.isNotEmpty) { - _subscriptions.add( - _idCallEnded, - client.events.on( - (event) { - FlutterCallkitIncoming.endCall(event.callCid.id); - }, - ), - ); + subscribeToEvents(); } }); @@ -81,26 +129,7 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { client.openConnection(); } - _subscriptions.add( - _idCallEnded, - client.events.on( - (event) { - FlutterCallkitIncoming.endCall(event.callCid.id); - }, - ), - ); - - _subscriptions.add( - _idCallAccepted, - client.events.on( - (event) async { - await FlutterCallkitIncoming.silenceEvents(); - await FlutterCallkitIncoming.endCall(event.callCid.id); - await Future.delayed(const Duration(milliseconds: 300)); - await FlutterCallkitIncoming.unsilenceEvents(); - }, - ), - ); + subscribeToEvents(); }, ), ); @@ -119,6 +148,7 @@ class StreamVideoPushNotificationManager implements PushNotificationManager { _subscriptions.cancel(_idCallAccepted); _subscriptions.cancel(_idCallEnded); + _subscriptions.cancel(_idCallRejected); }, ), );