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,