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] 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