From cb22ad8409094931dd9520e0494511878ff06179 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:05:16 +0300 Subject: [PATCH 1/8] Collect remote participants in CallService and set callDisplayName --- .../internal/service/CallService.kt | 49 +++++++++++++++++++ .../src/main/res/values/strings.xml | 2 + 2 files changed, 51 insertions(+) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 9beb88a79f..404c90722d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -488,6 +488,55 @@ internal open class CallService : Service() { } } } + + // Remote participants + serviceScope.launch { + val call = streamVideo.call(callId.type, callId.id) + var latestRemoteParticipantCount = 0 + + call.state.remoteParticipants.collect { remoteParticipants -> + if (remoteParticipants.size != latestRemoteParticipantCount) { + // If number of remote participants increased or decreased + latestRemoteParticipantCount = remoteParticipants.size + + val callDisplayName = if (remoteParticipants.isEmpty()) { + // If no remote participants, get simple call notification title + applicationContext.getString( + R.string.stream_video_ongoing_call_notification_title, + ) + } else { + if (remoteParticipants.size > 1) { + // If more than 1 remote participant, get group call notification title + applicationContext.getString( + R.string.stream_video_ongoing_group_call_notification_title, + ) + } else { + // If 1 remote participant, get the name of the remote participant + val remoteParticipantName = remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" + + applicationContext.getString( + R.string.stream_video_ongoing_one_on_one_call_notification_title, + remoteParticipantName, + ) + } + } + + val notification = streamVideo.getOngoingCallNotification( + callDisplayName = callDisplayName, // Use title in notification + callId = callId, + ) + + notification?.let { + startForegroundWithServiceType( + callId.hashCode(), + it, + TRIGGER_ONGOING_CALL, + serviceType, + ) + } + } + } + } } private fun playCallSound(@RawRes sound: Int?) { diff --git a/stream-video-android-core/src/main/res/values/strings.xml b/stream-video-android-core/src/main/res/values/strings.xml index ef39925f05..57939a1c65 100644 --- a/stream-video-android-core/src/main/res/values/strings.xml +++ b/stream-video-android-core/src/main/res/values/strings.xml @@ -40,6 +40,8 @@ Ongoing Calls Ongoing call notifications Call in progress + Group call in progress + Call with %1$s in progress There is a call in progress, tap to go back to the call call_setup Call Setup From a74988209164eb0dc57e65550d6eef22ce9b27d7 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:17:44 +0300 Subject: [PATCH 2/8] Split observeCallState into several methods --- .../internal/service/CallService.kt | 119 +++++++++--------- 1 file changed, 63 insertions(+), 56 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 404c90722d..eb5f2f36a7 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -415,7 +415,12 @@ internal open class CallService : Service() { } private fun observeCallState(callId: StreamCallId, streamVideo: StreamVideoImpl) { - // Ringing state + observeRingingState(callId, streamVideo) + observeCallEvents(callId, streamVideo) + observeRemoteParticipants(callId, streamVideo) + } + + private fun observeRingingState(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) call.state.ringingState.collect { @@ -457,8 +462,43 @@ internal open class CallService : Service() { } } } + } + + private fun playCallSound(@RawRes sound: Int?) { + sound?.let { + try { + mediaPlayer?.let { + if (!it.isPlaying) { + setMediaPlayerDataSource(it, sound) + it.start() + } + } + } catch (e: IllegalStateException) { + logger.d { "Error playing call sound." } + } + } + } - // Call state + private fun setMediaPlayerDataSource(mediaPlayer: MediaPlayer, @RawRes resId: Int) { + mediaPlayer.reset() + val afd = resources.openRawResourceFd(resId) + if (afd != null) { + mediaPlayer.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) + afd.close() + } + mediaPlayer.isLooping = true + mediaPlayer.prepare() + } + + private fun stopCallSound() { + try { + if (mediaPlayer?.isPlaying == true) mediaPlayer?.stop() + } catch (e: IllegalStateException) { + logger.d { "Error stopping call sound. MediaPlayer might have already been released." } + } + } + + private fun observeCallEvents(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) call.subscribe { event -> @@ -488,8 +528,28 @@ internal open class CallService : Service() { } } } + } + + private fun handleIncomingCallAcceptedByMeOnAnotherDevice(acceptedByUserId: String, myUserId: String, callRingingState: RingingState) { + // If accepted event was received, with event user being me, but current device is still ringing, it means the call was accepted on another device + if (acceptedByUserId == myUserId && callRingingState is RingingState.Incoming) { + // So stop ringing on this device + stopService() + } + } + + private fun handleIncomingCallRejectedByMeOrCaller(rejectedByUserId: String, myUserId: String, createdByUserId: String?, activeCallExists: Boolean) { + // If rejected event was received (even from another device), with event user being me OR the caller, remove incoming call / stop service. + if (rejectedByUserId == myUserId || rejectedByUserId == createdByUserId) { + if (activeCallExists) { + removeIncomingCall(INCOMING_CALL_NOTIFICATION_ID) + } else { + stopService() + } + } + } - // Remote participants + private fun observeRemoteParticipants(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) var latestRemoteParticipantCount = 0 @@ -539,59 +599,6 @@ internal open class CallService : Service() { } } - private fun playCallSound(@RawRes sound: Int?) { - sound?.let { - try { - mediaPlayer?.let { - if (!it.isPlaying) { - setMediaPlayerDataSource(it, sound) - it.start() - } - } - } catch (e: IllegalStateException) { - logger.d { "Error playing call sound." } - } - } - } - - private fun setMediaPlayerDataSource(mediaPlayer: MediaPlayer, @RawRes resId: Int) { - mediaPlayer.reset() - val afd = resources.openRawResourceFd(resId) - if (afd != null) { - mediaPlayer.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length) - afd.close() - } - mediaPlayer.isLooping = true - mediaPlayer.prepare() - } - - private fun stopCallSound() { - try { - if (mediaPlayer?.isPlaying == true) mediaPlayer?.stop() - } catch (e: IllegalStateException) { - logger.d { "Error stopping call sound. MediaPlayer might have already been released." } - } - } - - private fun handleIncomingCallAcceptedByMeOnAnotherDevice(acceptedByUserId: String, myUserId: String, callRingingState: RingingState) { - // If accepted event was received, with event user being me, but current device is still ringing, it means the call was accepted on another device - if (acceptedByUserId == myUserId && callRingingState is RingingState.Incoming) { - // So stop ringing on this device - stopService() - } - } - - private fun handleIncomingCallRejectedByMeOrCaller(rejectedByUserId: String, myUserId: String, createdByUserId: String?, activeCallExists: Boolean) { - // If rejected event was received (even from another device), with event user being me OR the caller, remove incoming call / stop service. - if (rejectedByUserId == myUserId || rejectedByUserId == createdByUserId) { - if (activeCallExists) { - removeIncomingCall(INCOMING_CALL_NOTIFICATION_ID) - } else { - stopService() - } - } - } - private fun registerToggleCameraBroadcastReceiver() { serviceScope.launch { if (!isToggleCameraBroadcastReceiverRegistered) { From efa8c65ca2dc6c63a3a162df7f4622dd7b567cbe Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 19 Sep 2024 14:47:21 +0300 Subject: [PATCH 3/8] Refactor incoming and ongoing notifications --- .../api/stream-video-android-core.api | 5 +- .../DefaultNotificationHandler.kt | 58 ++++++----- .../core/notifications/NotificationHandler.kt | 6 +- .../internal/NoOpNotificationHandler.kt | 3 +- .../internal/service/CallService.kt | 96 +++++++++++-------- .../res/drawable/stream_video_ic_user.xml | 28 ++++++ .../drawable/stream_video_ic_user_group.xml | 34 +++++++ .../src/main/res/values/strings.xml | 9 +- 8 files changed, 164 insertions(+), 75 deletions(-) create mode 100644 stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml create mode 100644 stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 78171e6a4a..f62b200feb 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4139,7 +4139,7 @@ public class io/getstream/video/android/core/notifications/DefaultNotificationHa public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationIconRes ()I protected final fun getNotificationManager ()Landroidx/core/app/NotificationManagerCompat; - public fun getOngoingCallNotification (Ljava/lang/String;Lio/getstream/video/android/model/StreamCallId;)Landroid/app/Notification; + public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;I)Landroid/app/Notification; public fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public fun getSettingUpCallNotification ()Landroid/app/Notification; public fun onLiveCall (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;)V @@ -4189,7 +4189,8 @@ public abstract interface class io/getstream/video/android/core/notifications/No public static final field INTENT_EXTRA_CALL_CID Ljava/lang/String; public static final field INTENT_EXTRA_CALL_DISPLAY_NAME Ljava/lang/String; public static final field INTENT_EXTRA_NOTIFICATION_ID Ljava/lang/String; - public abstract fun getOngoingCallNotification (Ljava/lang/String;Lio/getstream/video/android/model/StreamCallId;)Landroid/app/Notification; + public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;I)Landroid/app/Notification; + public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IILjava/lang/Object;)Landroid/app/Notification; public abstract fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public static synthetic fun getRingingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZILjava/lang/Object;)Landroid/app/Notification; public abstract fun getSettingUpCallNotification ()Landroid/app/Notification; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt index c8db32af1a..9e17c4479d 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt @@ -32,6 +32,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.CallStyle import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person +import androidx.core.graphics.drawable.IconCompat import io.getstream.android.push.permissions.DefaultNotificationPermissionHandler import io.getstream.android.push.permissions.NotificationPermissionHandler import io.getstream.log.taggedLogger @@ -40,7 +41,6 @@ import io.getstream.video.android.core.RingingState import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_LIVE_CALL import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_MISSED_CALL import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_NOTIFICATION -import io.getstream.video.android.core.notifications.NotificationHandler.Companion.INCOMING_CALL_NOTIFICATION_ID import io.getstream.video.android.core.notifications.internal.DefaultStreamIntentResolver import io.getstream.video.android.core.notifications.internal.service.CallService import io.getstream.video.android.model.StreamCallId @@ -183,7 +183,7 @@ public open class DefaultNotificationHandler( fullScreenPendingIntent: PendingIntent, acceptCallPendingIntent: PendingIntent, rejectCallPendingIntent: PendingIntent, - callDisplayName: String, + callerName: String, shouldHaveContentIntent: Boolean, ): Notification { // if the app is in foreground then don't interrupt the user with a high priority @@ -231,10 +231,10 @@ public open class DefaultNotificationHandler( return getNotification { priority = NotificationCompat.PRIORITY_HIGH - setContentTitle( + setContentTitle(callerName) + setContentText( application.getString(R.string.stream_video_incoming_call_notification_title), ) - setContentText(callDisplayName) setChannelId(channelId) setOngoing(false) setCategory(NotificationCompat.CATEGORY_CALL) @@ -251,7 +251,7 @@ public open class DefaultNotificationHandler( setContentIntent(emptyIntent) setAutoCancel(false) } - addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callDisplayName) + addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callerName) } } @@ -322,8 +322,9 @@ public open class DefaultNotificationHandler( } override fun getOngoingCallNotification( - callDisplayName: String?, callId: StreamCallId, + callDisplayName: String?, + remoteParticipantCount: Int, ): Notification? { val notificationId = callId.hashCode() // Notification ID @@ -376,7 +377,11 @@ public open class DefaultNotificationHandler( ) .setAutoCancel(false) .setOngoing(true) - .addHangupAction(endCallIntent, callDisplayName ?: callId.toString()) + .addHangupAction( + endCallIntent, + callDisplayName ?: callId.toString(), + remoteParticipantCount, + ) .build() } @@ -436,25 +441,6 @@ public open class DefaultNotificationHandler( } } - private fun showIncomingCallNotification( - fullScreenPendingIntent: PendingIntent, - acceptCallPendingIntent: PendingIntent, - rejectCallPendingIntent: PendingIntent, - callDisplayName: String, - notificationId: Int = INCOMING_CALL_NOTIFICATION_ID, - ) { - showNotification(notificationId) { - priority = NotificationCompat.PRIORITY_HIGH - setContentTitle("Incoming call") - setContentText(callDisplayName) - setOngoing(false) - setContentIntent(fullScreenPendingIntent) - setFullScreenIntent(fullScreenPendingIntent, true) - setCategory(NotificationCompat.CATEGORY_CALL) - addCallActions(acceptCallPendingIntent, rejectCallPendingIntent, callDisplayName) - } - } - @SuppressLint("MissingPermission") private fun showNotification( notificationId: Int, @@ -477,12 +463,32 @@ public open class DefaultNotificationHandler( private fun NotificationCompat.Builder.addHangupAction( rejectCallPendingIntent: PendingIntent, callDisplayName: String, + remoteParticipantCount: Int, ): NotificationCompat.Builder = apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setStyle( CallStyle.forOngoingCall( Person.Builder() .setName(callDisplayName) + .apply { + if (remoteParticipantCount == 0) { + // Just one user in the call + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user, + ), + ) + } else if (remoteParticipantCount > 1) { + // More than one user in the call + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user_group, + ), + ) + } + } .build(), rejectCallPendingIntent, ), diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index b9b7a41148..6f3f941b3a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -26,7 +26,11 @@ public interface NotificationHandler : NotificationPermissionHandler { fun onMissedCall(callId: StreamCallId, callDisplayName: String) fun onNotification(callId: StreamCallId, callDisplayName: String) fun onLiveCall(callId: StreamCallId, callDisplayName: String) - fun getOngoingCallNotification(callDisplayName: String?, callId: StreamCallId): Notification? + fun getOngoingCallNotification( + callId: StreamCallId, + callDisplayName: String?, + remoteParticipantCount: Int = 0, + ): Notification? fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt index f4fa89e3ad..66f0ed30f6 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt @@ -27,8 +27,9 @@ internal object NoOpNotificationHandler : NotificationHandler { override fun onNotification(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } override fun onLiveCall(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } override fun getOngoingCallNotification( - callDisplayName: String?, callId: StreamCallId, + callDisplayName: String?, + remoteParticipantCount: Int, ): Notification? = null override fun getRingingCallNotification( ringingState: RingingState, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index eb5f2f36a7..db8f898d93 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -48,6 +48,10 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import org.openapitools.client.models.CallAcceptedEvent import org.openapitools.client.models.CallEndedEvent @@ -248,8 +252,8 @@ internal open class CallService : Service() { val notificationData: Pair = when (trigger) { TRIGGER_ONGOING_CALL -> Pair( first = streamVideo.getOngoingCallNotification( - callDisplayName = intentCallDisplayName, callId = intentCallId, + callDisplayName = intentCallDisplayName, ), second = intentCallId.hashCode(), ) @@ -552,49 +556,61 @@ internal open class CallService : Service() { private fun observeRemoteParticipants(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) - var latestRemoteParticipantCount = 0 - - call.state.remoteParticipants.collect { remoteParticipants -> - if (remoteParticipants.size != latestRemoteParticipantCount) { - // If number of remote participants increased or decreased - latestRemoteParticipantCount = remoteParticipants.size - - val callDisplayName = if (remoteParticipants.isEmpty()) { - // If no remote participants, get simple call notification title - applicationContext.getString( - R.string.stream_video_ongoing_call_notification_title, - ) - } else { - if (remoteParticipants.size > 1) { - // If more than 1 remote participant, get group call notification title - applicationContext.getString( - R.string.stream_video_ongoing_group_call_notification_title, + var latestRemoteParticipantCount = -1 + + // Monitor call state and remote participants + combine( + call.state.ringingState, + call.state.remoteParticipants, + ) { ringingState, remoteParticipants -> + Pair(ringingState, remoteParticipants) + } + .distinctUntilChanged() + .filter { it.first is RingingState.Active } + .collectLatest { + val ringingState = it.first + val remoteParticipants = it.second + + // If we have an active call (not incoming, not outgoing etc.) + if (ringingState is RingingState.Active) { + // If number of remote participants increased or decreased + if (remoteParticipants.size != latestRemoteParticipantCount) { + latestRemoteParticipantCount = remoteParticipants.size + + val callDisplayName = if (remoteParticipants.isEmpty()) { + // If no remote participants, get simple call notification title + applicationContext.getString( + R.string.stream_video_ongoing_call_notification_title, + ) + } else { + if (remoteParticipants.size > 1) { + // If more than 1 remote participant, get group call notification title + applicationContext.getString( + R.string.stream_video_ongoing_group_call_notification_title, + ) + } else { + // If 1 remote participant, get the name of the remote participant + remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" + } + } + + // Use latest call display name in notification + val notification = streamVideo.getOngoingCallNotification( + callId = callId, + callDisplayName = callDisplayName, + remoteParticipantCount = remoteParticipants.size, ) - } else { - // If 1 remote participant, get the name of the remote participant - val remoteParticipantName = remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" - applicationContext.getString( - R.string.stream_video_ongoing_one_on_one_call_notification_title, - remoteParticipantName, - ) + notification?.let { + startForegroundWithServiceType( + callId.hashCode(), + notification, + TRIGGER_ONGOING_CALL, + serviceType, + ) + } } } - - val notification = streamVideo.getOngoingCallNotification( - callDisplayName = callDisplayName, // Use title in notification - callId = callId, - ) - - notification?.let { - startForegroundWithServiceType( - callId.hashCode(), - it, - TRIGGER_ONGOING_CALL, - serviceType, - ) - } - } } } } diff --git a/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml new file mode 100644 index 0000000000..da19c18bca --- /dev/null +++ b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml new file mode 100644 index 0000000000..064403f90e --- /dev/null +++ b/stream-video-android-core/src/main/res/drawable/stream_video_ic_user_group.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/stream-video-android-core/src/main/res/values/strings.xml b/stream-video-android-core/src/main/res/values/strings.xml index 57939a1c65..a2abd9e451 100644 --- a/stream-video-android-core/src/main/res/values/strings.xml +++ b/stream-video-android-core/src/main/res/values/strings.xml @@ -35,14 +35,13 @@ Outgoing Calls Outgoing call notifications Calling... - There is a call in progress, tap to go back to the call + Tap to go back to the call ongoing_calls Ongoing Calls Ongoing call notifications - Call in progress - Group call in progress - Call with %1$s in progress - There is a call in progress, tap to go back to the call + Call in Progress + Group Call in Progress + Tap to go back to the call call_setup Call Setup Temporary notifications used while setting up calls From 913656731fdb7df6d257d128514fed9dc04f1f85 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Thu, 19 Sep 2024 18:51:56 +0300 Subject: [PATCH 4/8] Refactor outgoing notifications --- .../api/stream-video-android-core.api | 6 +- .../DefaultNotificationHandler.kt | 67 +++++-------------- .../core/notifications/NotificationHandler.kt | 1 + .../internal/NoOpNotificationHandler.kt | 1 + .../internal/service/CallService.kt | 53 +++++++++++---- 5 files changed, 63 insertions(+), 65 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index f62b200feb..8b296f82a4 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4139,7 +4139,7 @@ public class io/getstream/video/android/core/notifications/DefaultNotificationHa public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationIconRes ()I protected final fun getNotificationManager ()Landroidx/core/app/NotificationManagerCompat; - public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;I)Landroid/app/Notification; + public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZ)Landroid/app/Notification; public fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public fun getSettingUpCallNotification ()Landroid/app/Notification; public fun onLiveCall (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;)V @@ -4189,8 +4189,8 @@ public abstract interface class io/getstream/video/android/core/notifications/No public static final field INTENT_EXTRA_CALL_CID Ljava/lang/String; public static final field INTENT_EXTRA_CALL_DISPLAY_NAME Ljava/lang/String; public static final field INTENT_EXTRA_NOTIFICATION_ID Ljava/lang/String; - public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;I)Landroid/app/Notification; - public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IILjava/lang/Object;)Landroid/app/Notification; + public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZ)Landroid/app/Notification; + public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZILjava/lang/Object;)Landroid/app/Notification; public abstract fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public static synthetic fun getRingingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZILjava/lang/Object;)Landroid/app/Notification; public abstract fun getSettingUpCallNotification ()Landroid/app/Notification; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt index 9e17c4479d..6190e3b30b 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt @@ -132,10 +132,10 @@ public open class DefaultNotificationHandler( val endCallPendingIntent = intentResolver.searchEndCallPendingIntent(callId) if (outgoingCallPendingIntent != null && endCallPendingIntent != null) { - getOutgoingCallNotification( - outgoingCallPendingIntent, - endCallPendingIntent, + getOngoingCallNotification( + callId, callDisplayName, + isOutgoingCall = true, ) } else { logger.e { "Ringing call notification not shown, one of the intents is null." } @@ -236,7 +236,7 @@ public open class DefaultNotificationHandler( application.getString(R.string.stream_video_incoming_call_notification_title), ) setChannelId(channelId) - setOngoing(false) + setOngoing(true) setCategory(NotificationCompat.CATEGORY_CALL) setFullScreenIntent(fullScreenPendingIntent, true) if (shouldHaveContentIntent) { @@ -255,48 +255,6 @@ public open class DefaultNotificationHandler( } } - private fun getOutgoingCallNotification( - outgoingCallPendingIntent: PendingIntent, - endCallPendingIntent: PendingIntent, - callDisplayName: String, - ): Notification { - val channelId = application.getString( - R.string.stream_video_outgoing_call_notification_channel_id, - ) - maybeCreateChannel( - channelId = channelId, - context = application, - configure = { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - name = application.getString( - R.string.stream_video_outgoing_call_notification_channel_title, - ) - description = application.getString( - R.string.stream_video_outgoing_call_notification_channel_description, - ) - } - }, - ) - - return getNotification { - setContentTitle( - application.getString(R.string.stream_video_outgoing_call_notification_title), - ) - setContentText(callDisplayName) - setChannelId(channelId) - setOngoing(true) - setContentIntent(outgoingCallPendingIntent) - setCategory(NotificationCompat.CATEGORY_CALL) - addAction( - NotificationCompat.Action.Builder( - android.R.drawable.ic_menu_close_clear_cancel, - application.getString(R.string.stream_video_call_notification_action_cancel), - endCallPendingIntent, - ).build(), - ) - } - } - override fun onNotification(callId: StreamCallId, callDisplayName: String) { val notificationId = callId.hashCode() intentResolver.searchNotificationCallPendingIntent(callId, notificationId) @@ -325,6 +283,7 @@ public open class DefaultNotificationHandler( callId: StreamCallId, callDisplayName: String?, remoteParticipantCount: Int, + isOutgoingCall: Boolean, ): Notification? { val notificationId = callId.hashCode() // Notification ID @@ -333,7 +292,11 @@ public open class DefaultNotificationHandler( callId, notificationId, ) - val endCallIntent = intentResolver.searchEndCallPendingIntent(callId = callId) + val hangUpIntent = if (isOutgoingCall) { + intentResolver.searchRejectCallPendingIntent(callId) + } else { + intentResolver.searchEndCallPendingIntent(callId) + } // Channel preparation val ongoingCallsChannelId = application.getString( @@ -353,7 +316,7 @@ public open class DefaultNotificationHandler( }, ) - if (endCallIntent == null) { + if (hangUpIntent == null) { logger.e { "End call intent is null, not showing notification!" } return null } @@ -370,7 +333,11 @@ public open class DefaultNotificationHandler( } } .setContentTitle( - application.getString(R.string.stream_video_ongoing_call_notification_title), + if (isOutgoingCall) { + application.getString(R.string.stream_video_outgoing_call_notification_title) + } else { + application.getString(R.string.stream_video_ongoing_call_notification_title) + }, ) .setContentText( application.getString(R.string.stream_video_ongoing_call_notification_description), @@ -378,7 +345,7 @@ public open class DefaultNotificationHandler( .setAutoCancel(false) .setOngoing(true) .addHangupAction( - endCallIntent, + hangUpIntent, callDisplayName ?: callId.toString(), remoteParticipantCount, ) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index 6f3f941b3a..686f489e76 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -30,6 +30,7 @@ public interface NotificationHandler : NotificationPermissionHandler { callId: StreamCallId, callDisplayName: String?, remoteParticipantCount: Int = 0, + isOutgoingCall: Boolean = false, ): Notification? fun getRingingCallNotification( ringingState: RingingState, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt index 66f0ed30f6..a08a355e1a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt @@ -30,6 +30,7 @@ internal object NoOpNotificationHandler : NotificationHandler { callId: StreamCallId, callDisplayName: String?, remoteParticipantCount: Int, + isOutgoingCall: Boolean, ): Notification? = null override fun getRingingCallNotification( ringingState: RingingState, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index db8f898d93..9870a27c91 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -273,7 +273,7 @@ internal open class CallService : Service() { ringingState = RingingState.Outgoing(), callId = intentCallId, callDisplayName = getString( - R.string.stream_video_ongoing_call_notification_description, + R.string.stream_video_outgoing_call_notification_title, ), ), second = INCOMING_CALL_NOTIFICATION_ID, // Same for incoming and outgoing @@ -335,7 +335,7 @@ internal open class CallService : Service() { } else if (trigger == TRIGGER_OUTGOING_CALL) { if (mediaPlayer == null) mediaPlayer = MediaPlayer() } - observeCallState(intentCallId, streamVideo) + observeCall(intentCallId, streamVideo) registerToggleCameraBroadcastReceiver() return START_NOT_STICKY } @@ -418,10 +418,10 @@ internal open class CallService : Service() { } } - private fun observeCallState(callId: StreamCallId, streamVideo: StreamVideoImpl) { + private fun observeCall(callId: StreamCallId, streamVideo: StreamVideoImpl) { observeRingingState(callId, streamVideo) observeCallEvents(callId, streamVideo) - observeRemoteParticipants(callId, streamVideo) + observeParticipants(callId, streamVideo) } private fun observeRingingState(callId: StreamCallId, streamVideo: StreamVideoImpl) { @@ -553,7 +553,7 @@ internal open class CallService : Service() { } } - private fun observeRemoteParticipants(callId: StreamCallId, streamVideo: StreamVideoImpl) { + private fun observeParticipants(callId: StreamCallId, streamVideo: StreamVideoImpl) { serviceScope.launch { val call = streamVideo.call(callId.type, callId.id) var latestRemoteParticipantCount = -1 @@ -561,18 +561,47 @@ internal open class CallService : Service() { // Monitor call state and remote participants combine( call.state.ringingState, + call.state.members, call.state.remoteParticipants, - ) { ringingState, remoteParticipants -> - Pair(ringingState, remoteParticipants) + ) { ringingState, members, remoteParticipants -> + Triple(ringingState, members, remoteParticipants) } .distinctUntilChanged() - .filter { it.first is RingingState.Active } + .filter { it.first is RingingState.Active || it.first is RingingState.Outgoing } .collectLatest { val ringingState = it.first - val remoteParticipants = it.second + val members = it.second + val remoteParticipants = it.third - // If we have an active call (not incoming, not outgoing etc.) - if (ringingState is RingingState.Active) { + if (ringingState is RingingState.Outgoing) { + val remoteMembersCount = members.size - 1 + + val callDisplayName = if (remoteMembersCount != 1) { + applicationContext.getString( + R.string.stream_video_outgoing_call_notification_title, + ) + } else { + members.firstOrNull { member -> + member.user.id != streamVideo.userId + }?.user?.name ?: "Unknown" + } + + val notification = streamVideo.getOngoingCallNotification( + callId = callId, + callDisplayName = callDisplayName, + remoteParticipantCount = remoteMembersCount, + isOutgoingCall = true, + ) + + notification?.let { + startForegroundWithServiceType( + callId.hashCode(), + notification, + TRIGGER_ONGOING_CALL, + serviceType, + ) + } + } else if (ringingState is RingingState.Active) { // If number of remote participants increased or decreased if (remoteParticipants.size != latestRemoteParticipantCount) { latestRemoteParticipantCount = remoteParticipants.size @@ -611,7 +640,7 @@ internal open class CallService : Service() { } } } - } + } } } From f163c62410dc059a7807d2698750eca94f078d57 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:00:12 +0300 Subject: [PATCH 5/8] Add getNotificationUpdates() in NotificationHandler and default implementation --- .../api/stream-video-android-core.api | 8 +- .../DefaultNotificationHandler.kt | 119 ++++++++++++++++-- .../core/notifications/NotificationHandler.kt | 13 +- .../internal/NoOpNotificationHandler.kt | 13 +- .../internal/service/CallService.kt | 108 +++------------- .../video/android/model/StreamCallId.kt | 2 +- .../src/main/res/values/strings.xml | 2 +- 7 files changed, 153 insertions(+), 112 deletions(-) diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api index 8b296f82a4..833485348b 100644 --- a/stream-video-android-core/api/stream-video-android-core.api +++ b/stream-video-android-core/api/stream-video-android-core.api @@ -4139,7 +4139,8 @@ public class io/getstream/video/android/core/notifications/DefaultNotificationHa public final fun getHideRingingNotificationInForeground ()Z public final fun getNotificationIconRes ()I protected final fun getNotificationManager ()Landroidx/core/app/NotificationManagerCompat; - public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZ)Landroid/app/Notification; + public fun getNotificationUpdates (Lkotlinx/coroutines/CoroutineScope;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lkotlin/jvm/functions/Function1;)V + public fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZI)Landroid/app/Notification; public fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public fun getSettingUpCallNotification ()Landroid/app/Notification; public fun onLiveCall (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;)V @@ -4189,8 +4190,9 @@ public abstract interface class io/getstream/video/android/core/notifications/No public static final field INTENT_EXTRA_CALL_CID Ljava/lang/String; public static final field INTENT_EXTRA_CALL_DISPLAY_NAME Ljava/lang/String; public static final field INTENT_EXTRA_NOTIFICATION_ID Ljava/lang/String; - public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZ)Landroid/app/Notification; - public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;IZILjava/lang/Object;)Landroid/app/Notification; + public abstract fun getNotificationUpdates (Lkotlinx/coroutines/CoroutineScope;Lio/getstream/video/android/core/Call;Lio/getstream/video/android/model/User;Lkotlin/jvm/functions/Function1;)V + public abstract fun getOngoingCallNotification (Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZI)Landroid/app/Notification; + public static synthetic fun getOngoingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZIILjava/lang/Object;)Landroid/app/Notification; public abstract fun getRingingCallNotification (Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;Z)Landroid/app/Notification; public static synthetic fun getRingingCallNotification$default (Lio/getstream/video/android/core/notifications/NotificationHandler;Lio/getstream/video/android/core/RingingState;Lio/getstream/video/android/model/StreamCallId;Ljava/lang/String;ZILjava/lang/Object;)Landroid/app/Notification; public abstract fun getSettingUpCallNotification ()Landroid/app/Notification; diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt index 6190e3b30b..badb033f05 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt @@ -36,6 +36,7 @@ import androidx.core.graphics.drawable.IconCompat import io.getstream.android.push.permissions.DefaultNotificationPermissionHandler import io.getstream.android.push.permissions.NotificationPermissionHandler import io.getstream.log.taggedLogger +import io.getstream.video.android.core.Call import io.getstream.video.android.core.R import io.getstream.video.android.core.RingingState import io.getstream.video.android.core.notifications.NotificationHandler.Companion.ACTION_LIVE_CALL @@ -44,6 +45,13 @@ import io.getstream.video.android.core.notifications.NotificationHandler.Compani import io.getstream.video.android.core.notifications.internal.DefaultStreamIntentResolver import io.getstream.video.android.core.notifications.internal.service.CallService import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch public open class DefaultNotificationHandler( private val application: Application, @@ -107,7 +115,7 @@ public open class DefaultNotificationHandler( override fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String?, shouldHaveContentIntent: Boolean, ): Notification? { return if (ringingState is RingingState.Incoming) { @@ -183,7 +191,7 @@ public open class DefaultNotificationHandler( fullScreenPendingIntent: PendingIntent, acceptCallPendingIntent: PendingIntent, rejectCallPendingIntent: PendingIntent, - callerName: String, + callerName: String?, shouldHaveContentIntent: Boolean, ): Notification { // if the app is in foreground then don't interrupt the user with a high priority @@ -233,7 +241,7 @@ public open class DefaultNotificationHandler( priority = NotificationCompat.PRIORITY_HIGH setContentTitle(callerName) setContentText( - application.getString(R.string.stream_video_incoming_call_notification_title), + application.getString(R.string.stream_video_incoming_call_notification_description), ) setChannelId(channelId) setOngoing(true) @@ -282,8 +290,8 @@ public open class DefaultNotificationHandler( override fun getOngoingCallNotification( callId: StreamCallId, callDisplayName: String?, - remoteParticipantCount: Int, isOutgoingCall: Boolean, + remoteParticipantCount: Int, ): Notification? { val notificationId = callId.hashCode() // Notification ID @@ -344,14 +352,97 @@ public open class DefaultNotificationHandler( ) .setAutoCancel(false) .setOngoing(true) - .addHangupAction( + .addHangUpAction( hangUpIntent, - callDisplayName ?: callId.toString(), + callDisplayName ?: application.getString( + R.string.stream_video_ongoing_call_notification_title, + ), remoteParticipantCount, ) .build() } + override fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) { + coroutineScope.launch { + var latestRemoteParticipantCount = -1 + + // Monitor call state and remote participants + combine( + call.state.ringingState, + call.state.members, + call.state.remoteParticipants, + ) { ringingState, members, remoteParticipants -> + Triple(ringingState, members, remoteParticipants) + } + .distinctUntilChanged() + .filter { it.first is RingingState.Active || it.first is RingingState.Outgoing } + .collectLatest { state -> + val ringingState = state.first + val members = state.second + val remoteParticipants = state.third + + if (ringingState is RingingState.Outgoing) { + val remoteMembersCount = members.size - 1 + + val callDisplayName = if (remoteMembersCount != 1) { + application.getString( + R.string.stream_video_outgoing_call_notification_title, + ) + } else { + members.firstOrNull { member -> + member.user.id != localUser.id + }?.user?.name ?: "Unknown" + } + + getOngoingCallNotification( + callId = StreamCallId.fromCallCid(call.cid), + callDisplayName = callDisplayName, + isOutgoingCall = true, + remoteParticipantCount = remoteMembersCount, + )?.let { + onUpdate(it) + } + } else if (ringingState is RingingState.Active) { + // If number of remote participants increased or decreased + if (remoteParticipants.size != latestRemoteParticipantCount) { + latestRemoteParticipantCount = remoteParticipants.size + + val callDisplayName = if (remoteParticipants.isEmpty()) { + // If no remote participants, get simple call notification title + application.getString( + R.string.stream_video_ongoing_call_notification_title, + ) + } else { + if (remoteParticipants.size > 1) { + // If more than 1 remote participant, get group call notification title + application.getString( + R.string.stream_video_ongoing_group_call_notification_title, + ) + } else { + // If 1 remote participant, get the name of the remote participant + remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" + } + } + + // Use latest call display name in notification + getOngoingCallNotification( + callId = StreamCallId.fromCallCid(call.cid), + callDisplayName = callDisplayName, + remoteParticipantCount = remoteParticipants.size, + )?.let { + onUpdate(it) + } + } + } + } + } + } + private fun maybeCreateChannel( channelId: String, context: Context, @@ -427,7 +518,7 @@ public open class DefaultNotificationHandler( .build() } - private fun NotificationCompat.Builder.addHangupAction( + private fun NotificationCompat.Builder.addHangUpAction( rejectCallPendingIntent: PendingIntent, callDisplayName: String, remoteParticipantCount: Int, @@ -474,13 +565,23 @@ public open class DefaultNotificationHandler( private fun NotificationCompat.Builder.addCallActions( acceptCallPendingIntent: PendingIntent, rejectCallPendingIntent: PendingIntent, - callDisplayName: String, + callDisplayName: String?, ): NotificationCompat.Builder = apply { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { setStyle( CallStyle.forIncomingCall( Person.Builder() - .setName(callDisplayName) + .setName(callDisplayName ?: "Unknown") + .apply { + if (callDisplayName == null) { + setIcon( + IconCompat.createWithResource( + application, + R.drawable.stream_video_ic_user, + ), + ) + } + } .build(), rejectCallPendingIntent, acceptCallPendingIntent, diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index 686f489e76..5646bc2eb8 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -18,8 +18,11 @@ package io.getstream.video.android.core.notifications import android.app.Notification import io.getstream.android.push.permissions.NotificationPermissionHandler +import io.getstream.video.android.core.Call import io.getstream.video.android.core.RingingState import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope public interface NotificationHandler : NotificationPermissionHandler { fun onRingingCall(callId: StreamCallId, callDisplayName: String) @@ -29,16 +32,22 @@ public interface NotificationHandler : NotificationPermissionHandler { fun getOngoingCallNotification( callId: StreamCallId, callDisplayName: String?, - remoteParticipantCount: Int = 0, isOutgoingCall: Boolean = false, + remoteParticipantCount: Int = 0, ): Notification? fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String?, shouldHaveContentIntent: Boolean = true, ): Notification? fun getSettingUpCallNotification(): Notification? + fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) companion object { const val ACTION_NOTIFICATION = "io.getstream.video.android.action.NOTIFICATION" diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt index a08a355e1a..fb35929e08 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/NoOpNotificationHandler.kt @@ -17,9 +17,12 @@ package io.getstream.video.android.core.notifications.internal import android.app.Notification +import io.getstream.video.android.core.Call import io.getstream.video.android.core.RingingState import io.getstream.video.android.core.notifications.NotificationHandler import io.getstream.video.android.model.StreamCallId +import io.getstream.video.android.model.User +import kotlinx.coroutines.CoroutineScope internal object NoOpNotificationHandler : NotificationHandler { override fun onRingingCall(callId: StreamCallId, callDisplayName: String) { /* NoOp */ } @@ -29,16 +32,22 @@ internal object NoOpNotificationHandler : NotificationHandler { override fun getOngoingCallNotification( callId: StreamCallId, callDisplayName: String?, - remoteParticipantCount: Int, isOutgoingCall: Boolean, + remoteParticipantCount: Int, ): Notification? = null override fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String, + callDisplayName: String?, shouldHaveContentIntent: Boolean, ): Notification? = null override fun getSettingUpCallNotification(): Notification? = null + override fun getNotificationUpdates( + coroutineScope: CoroutineScope, + call: Call, + localUser: User, + onUpdate: (Notification) -> Unit, + ) { /* NoOp */ } override fun onPermissionDenied() { /* NoOp */ } override fun onPermissionGranted() { /* NoOp */ } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt index 9870a27c91..897e0868ab 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt @@ -48,10 +48,6 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import org.openapitools.client.models.CallAcceptedEvent import org.openapitools.client.models.CallEndedEvent @@ -262,7 +258,7 @@ internal open class CallService : Service() { first = streamVideo.getRingingCallNotification( ringingState = RingingState.Incoming(), callId = intentCallId, - callDisplayName = intentCallDisplayName!!, + callDisplayName = intentCallDisplayName, shouldHaveContentIntent = streamVideo.state.activeCall.value == null, ), second = INCOMING_CALL_NOTIFICATION_ID, @@ -421,7 +417,7 @@ internal open class CallService : Service() { private fun observeCall(callId: StreamCallId, streamVideo: StreamVideoImpl) { observeRingingState(callId, streamVideo) observeCallEvents(callId, streamVideo) - observeParticipants(callId, streamVideo) + observeNotificationUpdates(callId, streamVideo) } private fun observeRingingState(callId: StreamCallId, streamVideo: StreamVideoImpl) { @@ -553,94 +549,18 @@ internal open class CallService : Service() { } } - private fun observeParticipants(callId: StreamCallId, streamVideo: StreamVideoImpl) { - serviceScope.launch { - val call = streamVideo.call(callId.type, callId.id) - var latestRemoteParticipantCount = -1 - - // Monitor call state and remote participants - combine( - call.state.ringingState, - call.state.members, - call.state.remoteParticipants, - ) { ringingState, members, remoteParticipants -> - Triple(ringingState, members, remoteParticipants) - } - .distinctUntilChanged() - .filter { it.first is RingingState.Active || it.first is RingingState.Outgoing } - .collectLatest { - val ringingState = it.first - val members = it.second - val remoteParticipants = it.third - - if (ringingState is RingingState.Outgoing) { - val remoteMembersCount = members.size - 1 - - val callDisplayName = if (remoteMembersCount != 1) { - applicationContext.getString( - R.string.stream_video_outgoing_call_notification_title, - ) - } else { - members.firstOrNull { member -> - member.user.id != streamVideo.userId - }?.user?.name ?: "Unknown" - } - - val notification = streamVideo.getOngoingCallNotification( - callId = callId, - callDisplayName = callDisplayName, - remoteParticipantCount = remoteMembersCount, - isOutgoingCall = true, - ) - - notification?.let { - startForegroundWithServiceType( - callId.hashCode(), - notification, - TRIGGER_ONGOING_CALL, - serviceType, - ) - } - } else if (ringingState is RingingState.Active) { - // If number of remote participants increased or decreased - if (remoteParticipants.size != latestRemoteParticipantCount) { - latestRemoteParticipantCount = remoteParticipants.size - - val callDisplayName = if (remoteParticipants.isEmpty()) { - // If no remote participants, get simple call notification title - applicationContext.getString( - R.string.stream_video_ongoing_call_notification_title, - ) - } else { - if (remoteParticipants.size > 1) { - // If more than 1 remote participant, get group call notification title - applicationContext.getString( - R.string.stream_video_ongoing_group_call_notification_title, - ) - } else { - // If 1 remote participant, get the name of the remote participant - remoteParticipants.firstOrNull()?.name?.value ?: "Unknown" - } - } - - // Use latest call display name in notification - val notification = streamVideo.getOngoingCallNotification( - callId = callId, - callDisplayName = callDisplayName, - remoteParticipantCount = remoteParticipants.size, - ) - - notification?.let { - startForegroundWithServiceType( - callId.hashCode(), - notification, - TRIGGER_ONGOING_CALL, - serviceType, - ) - } - } - } - } + private fun observeNotificationUpdates(callId: StreamCallId, streamVideo: StreamVideoImpl) { + streamVideo.getNotificationUpdates( + serviceScope, + streamVideo.call(callId.type, callId.id), + streamVideo.user, + ) { notification -> + startForegroundWithServiceType( + callId.hashCode(), + notification, + TRIGGER_ONGOING_CALL, + serviceType, + ) } } diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt index 85db14bd41..cd805daa7f 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/model/StreamCallId.kt @@ -79,4 +79,4 @@ public data class StreamCallId constructor( public fun Intent.streamCallId(key: String): StreamCallId? = IntentCompat.getParcelableExtra(this, key, StreamCallId::class.java) -public fun Intent.streamCallDisplayName(key: String): String = this.getStringExtra(key) ?: "." +public fun Intent.streamCallDisplayName(key: String): String? = this.getStringExtra(key) diff --git a/stream-video-android-core/src/main/res/values/strings.xml b/stream-video-android-core/src/main/res/values/strings.xml index a2abd9e451..ad9ee8f7ab 100644 --- a/stream-video-android-core/src/main/res/values/strings.xml +++ b/stream-video-android-core/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ incoming_calls_low_priority Incoming audio and video call alerts Incoming audio and video call alerts - Incoming call + Incoming call outgoing_calls Outgoing Calls Outgoing call notifications From 260d269b92c2fdb27563e2e9a2a65d693afdc5a2 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:12:06 +0300 Subject: [PATCH 6/8] Fix hang up bug --- .../DefaultNotificationHandler.kt | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt index badb033f05..4027d2d355 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/DefaultNotificationHandler.kt @@ -296,10 +296,17 @@ public open class DefaultNotificationHandler( val notificationId = callId.hashCode() // Notification ID // Intents - val ongoingCallIntent = intentResolver.searchOngoingCallPendingIntent( - callId, - notificationId, - ) + val onClickIntent = if (isOutgoingCall) { + intentResolver.searchOutgoingCallPendingIntent( + callId, + notificationId, + ) + } else { + intentResolver.searchOngoingCallPendingIntent( + callId, + notificationId, + ) + } val hangUpIntent = if (isOutgoingCall) { intentResolver.searchRejectCallPendingIntent(callId) } else { @@ -334,8 +341,8 @@ public open class DefaultNotificationHandler( .setSmallIcon(notificationIconRes) .also { // If the intent is configured, clicking the notification will return to the call - if (ongoingCallIntent != null) { - it.setContentIntent(ongoingCallIntent) + if (onClickIntent != null) { + it.setContentIntent(onClickIntent) } else { logger.w { "Ongoing intent is null click on the ongoing call notification will not work." } } @@ -519,7 +526,7 @@ public open class DefaultNotificationHandler( } private fun NotificationCompat.Builder.addHangUpAction( - rejectCallPendingIntent: PendingIntent, + hangUpIntent: PendingIntent, callDisplayName: String, remoteParticipantCount: Int, ): NotificationCompat.Builder = apply { @@ -548,7 +555,7 @@ public open class DefaultNotificationHandler( } } .build(), - rejectCallPendingIntent, + hangUpIntent, ), ) } else { @@ -556,7 +563,7 @@ public open class DefaultNotificationHandler( NotificationCompat.Action.Builder( null, application.getString(R.string.stream_video_call_notification_action_leave), - rejectCallPendingIntent, + hangUpIntent, ).build(), ) } From fabb6dc517afa3ea4bed8c3513db78e66cb8e92a Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:33:52 +0300 Subject: [PATCH 7/8] Set null as callDisplayName default value in getOngoingCallNotification and getRingingCallNotification --- .../video/android/core/notifications/NotificationHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index 5646bc2eb8..5c6f9f792a 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -31,14 +31,14 @@ public interface NotificationHandler : NotificationPermissionHandler { fun onLiveCall(callId: StreamCallId, callDisplayName: String) fun getOngoingCallNotification( callId: StreamCallId, - callDisplayName: String?, + callDisplayName: String? = null, isOutgoingCall: Boolean = false, remoteParticipantCount: Int = 0, ): Notification? fun getRingingCallNotification( ringingState: RingingState, callId: StreamCallId, - callDisplayName: String?, + callDisplayName: String? = null, shouldHaveContentIntent: Boolean = true, ): Notification? fun getSettingUpCallNotification(): Notification? From 6e07c0fc265be827cde06875acb1f13fd0773207 Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Fri, 20 Sep 2024 15:40:29 +0300 Subject: [PATCH 8/8] Add KDocs to getNotificationUpdates --- .../android/core/notifications/NotificationHandler.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt index 5c6f9f792a..5926298082 100644 --- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt +++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/NotificationHandler.kt @@ -42,6 +42,16 @@ public interface NotificationHandler : NotificationPermissionHandler { shouldHaveContentIntent: Boolean = true, ): Notification? fun getSettingUpCallNotification(): Notification? + + /** + * Get subsequent updates to notifications. + * Initially, notifications are posted by one of the other methods, and then this method can be used to re-post them with updated content. + * + * @param coroutineScope Coroutine scope used for the updates. + * @param call The Stream call object. + * @param localUser The local Stream user. + * @param onUpdate Callback to be called when the notification is updated. + */ fun getNotificationUpdates( coroutineScope: CoroutineScope, call: Call,