@@ -21,10 +21,12 @@ package com.wire.android.notification
2121import android.annotation.SuppressLint
2222import android.app.Notification
2323import android.content.Context
24+ import android.service.notification.StatusBarNotification
2425import androidx.core.app.NotificationCompat
2526import androidx.core.app.NotificationManagerCompat
2627import com.wire.android.R
2728import com.wire.android.appLogger
29+ import com.wire.android.notification.NotificationConstants.INCOMING_CALL_ID_PREFIX
2830import com.wire.android.util.dispatchers.DispatcherProvider
2931import com.wire.kalium.logic.data.call.Call
3032import com.wire.kalium.logic.data.call.CallStatus
@@ -34,17 +36,16 @@ import com.wire.kalium.logic.data.id.QualifiedID
3436import com.wire.kalium.logic.data.user.UserId
3537import kotlinx.coroutines.CoroutineScope
3638import kotlinx.coroutines.SupervisorJob
37- import kotlinx.coroutines.flow.Flow
3839import kotlinx.coroutines.flow.MutableSharedFlow
3940import kotlinx.coroutines.flow.MutableStateFlow
4041import kotlinx.coroutines.flow.collectLatest
4142import kotlinx.coroutines.flow.debounce
4243import kotlinx.coroutines.flow.distinctUntilChanged
4344import kotlinx.coroutines.flow.filter
4445import kotlinx.coroutines.flow.flatMapLatest
45- import kotlinx.coroutines.flow.flowOf
4646import kotlinx.coroutines.flow.map
4747import kotlinx.coroutines.flow.onStart
48+ import kotlinx.coroutines.flow.scan
4849import kotlinx.coroutines.flow.update
4950import kotlinx.coroutines.launch
5051import org.jetbrains.annotations.VisibleForTesting
@@ -62,75 +63,108 @@ class CallNotificationManager @Inject constructor(
6263
6364 private val notificationManager = NotificationManagerCompat .from(context)
6465 private val scope = CoroutineScope (SupervisorJob () + dispatcherProvider.default())
65- private val incomingCallsForUsers = MutableStateFlow <Map <UserId , Call >>(mapOf ())
66+ private val incomingCallsForUsers = MutableStateFlow <Map <UserId , IncomingCallsForUser >>(mapOf ())
6667 private val reloadCallNotification = MutableSharedFlow <CallNotificationIds >()
6768
6869 init {
6970 scope.launch {
7071 incomingCallsForUsers
7172 .debounce { if (it.isEmpty()) 0L else DEBOUNCE_TIME } // debounce to avoid showing and hiding notification too fast
72- .map { it.entries.firstOrNull()?.toCallNotificationData() }
73+ .map {
74+ it.values.map { (userId, userName, calls) ->
75+ calls.map { call ->
76+ CallNotificationData (userId, call, userName)
77+ }
78+ }.flatten()
79+ }
80+ .scan(emptyList<CallNotificationData >() to emptyList<CallNotificationData >()) { (previousCalls, _), currentCalls ->
81+ currentCalls to (currentCalls - previousCalls.toSet())
82+ }
7383 .distinctUntilChanged()
74- .reloadIfNeeded()
75- .collectLatest { incomingCallData ->
76- if (incomingCallData == null ) {
77- hideIncomingCallNotification()
78- } else {
79- appLogger.i(" $TAG : showing incoming call" )
80- showIncomingCallNotification(incomingCallData)
84+ .flatMapLatest { (allCurrentCalls, newCalls) ->
85+ reloadCallNotification
86+ .map { (userIdString, conversationIdString) ->
87+ allCurrentCalls to allCurrentCalls.filter { // emit call that needs to be reloaded as newOrUpdated
88+ it.userId.toString() == userIdString && it.conversationId.toString() == conversationIdString
89+ }
90+ }
91+ .filter { (_, newCalls) -> newCalls.isNotEmpty() } // only emit if there is something to reload
92+ .onStart { emit(allCurrentCalls to newCalls) }
93+ }
94+ .collectLatest { (allCurrentCalls, newCalls) ->
95+ // remove outdated incoming call notifications
96+ hideOutdatedIncomingCallNotifications(allCurrentCalls)
97+ // show current incoming call notifications
98+ appLogger.i(" $TAG : showing ${newCalls.size} new incoming calls (all incoming calls: ${allCurrentCalls.size} )" )
99+ newCalls.forEach { data ->
100+ showIncomingCallNotification(data)
81101 }
82102 }
83103 }
84104 }
85105
86- fun reloadIfNeeded ( data : CallNotificationData ): Flow < CallNotificationData > = reloadCallNotification
87- .filter { reloadCallNotificationIds -> // check if the reload action is for the same call
88- reloadCallNotificationIds.userIdString == data.userId.toString()
89- && reloadCallNotificationIds.conversationIdString == data .conversationId.toString()
106+ @VisibleForTesting
107+ internal fun hideOutdatedIncomingCallNotifications ( currentIncomingCalls : List < CallNotificationData >) {
108+ val currentIncomingCallNotificationIds = currentIncomingCalls.map {
109+ NotificationConstants .getIncomingCallId(it.userId.toString(), it .conversationId.toString() )
90110 }
91- .map { data }
92- .onStart { emit(data) }
93-
94- private fun Flow<CallNotificationData?>.reloadIfNeeded (): Flow <CallNotificationData ?> = this .flatMapLatest { callEntry ->
95- callEntry?.let { reloadIfNeeded(it) } ? : flowOf(null )
111+ hideIncomingCallNotifications { _, id -> ! currentIncomingCallNotificationIds.contains(id) }
96112 }
97113
98114 fun reloadCallNotifications (reloadCallNotificationIds : CallNotificationIds ) = scope.launch {
99115 reloadCallNotification.emit(reloadCallNotificationIds)
100116 }
101117
102- fun handleIncomingCallNotifications (calls : List <Call >, userId : UserId ) {
118+ fun handleIncomingCalls (calls : List <Call >, userId : UserId , userName : String ) {
103119 if (calls.isEmpty()) {
104- incomingCallsForUsers.update { it.filter { it.key != userId } }
120+ incomingCallsForUsers.update {
121+ it.minus(userId)
122+ }
105123 } else {
106- incomingCallsForUsers.update { it.filter { it.key != userId } + (userId to calls.first()) }
124+ incomingCallsForUsers.update {
125+ it.plus(userId to IncomingCallsForUser (userId, userName, calls))
126+ }
107127 }
108128 }
109129
110- fun hideAllNotifications () {
111- hideIncomingCallNotification()
130+ private fun hideIncomingCallNotifications (predicate : (tag: String , id: Int ) -> Boolean ) {
131+ notificationManager.activeNotifications.filter {
132+ it.tag?.startsWith(INCOMING_CALL_ID_PREFIX ) == true && predicate(it.tag, it.id)
133+ }.forEach {
134+ it.hideIncomingCallNotification()
135+ }
112136 }
113137
114- private fun hideIncomingCallNotification () {
138+ fun hideAllIncomingCallNotifications () = hideIncomingCallNotifications { _, _ -> true }
139+
140+ fun hideAllIncomingCallNotificationsForUser (userId : UserId ) =
141+ hideIncomingCallNotifications { tag, _ -> tag == NotificationConstants .getIncomingCallTag(userId.toString()) }
142+
143+ fun hideIncomingCallNotification (userIdString : String , conversationIdString : String ) =
144+ hideIncomingCallNotifications { _, id -> id == NotificationConstants .getIncomingCallId(userIdString, conversationIdString) }
145+
146+ private fun StatusBarNotification.hideIncomingCallNotification () {
115147 appLogger.i(" $TAG : hiding incoming call" )
116148
117149 // This delay is just so when the user receives two calling signals one straight after the other [INCOMING -> CANCEL]
118- // Due to the signals being one after the other we are creating a notification when we are trying to cancel it, it wasn't properly
119- // cancelling vibration as probably when we were cancelling, the vibration object was still being created and started and thus
120- // never stopped.
150+ // Due to the signals being one after the other we are creating a notification when we are trying to cancel it, it wasn't
151+ // properly cancelling vibration as probably when we were cancelling, the vibration object was still being created and started
152+ // and thus never stopped.
121153 TimeUnit .MILLISECONDS .sleep(CANCEL_CALL_NOTIFICATION_DELAY )
122- notificationManager.cancel(NotificationIds . CALL_INCOMING_NOTIFICATION_ID .ordinal )
154+ notificationManager.cancel(tag, id )
123155 }
124156
125157 @SuppressLint(" MissingPermission" )
126158 @VisibleForTesting
127159 internal fun showIncomingCallNotification (data : CallNotificationData ) {
128- appLogger.i(" $TAG : showing incoming call notification for user ${data.userId.toLogString()} " )
129- val notification = builder.getIncomingCallNotification(data)
130- notificationManager.notify(
131- NotificationIds .CALL_INCOMING_NOTIFICATION_ID .ordinal,
132- notification
160+ appLogger.i(
161+ " $TAG : showing incoming call notification for user ${data.userId.toLogString()} " +
162+ " and conversation ${data.conversationId.toLogString()} "
133163 )
164+ val tag = NotificationConstants .getIncomingCallTag(data.userId.toString())
165+ val id = NotificationConstants .getIncomingCallId(data.userId.toString(), data.conversationId.toString())
166+ val notification = builder.getIncomingCallNotification(data)
167+ notificationManager.notify(tag, id, notification)
134168 }
135169
136170 // Notifications
@@ -141,10 +175,6 @@ class CallNotificationManager @Inject constructor(
141175
142176 @VisibleForTesting
143177 internal const val DEBOUNCE_TIME = 200L
144-
145- fun hideIncomingCallNotification (context : Context ) {
146- NotificationManagerCompat .from(context).cancel(NotificationIds .CALL_INCOMING_NOTIFICATION_ID .ordinal)
147- }
148178 }
149179}
150180
@@ -164,6 +194,7 @@ class CallNotificationBuilder @Inject constructor(
164194 .setSmallIcon(R .drawable.notification_icon_small)
165195 .setContentTitle(data.conversationName)
166196 .setContentText(context.getString(R .string.notification_outgoing_call_tap_to_return))
197+ .setSubText(data.userName)
167198 .setAutoCancel(false )
168199 .setOngoing(true )
169200 .setSilent(true )
@@ -188,6 +219,7 @@ class CallNotificationBuilder @Inject constructor(
188219 .setSmallIcon(R .drawable.notification_icon_small)
189220 .setContentTitle(title)
190221 .setContentText(content)
222+ .setSubText(data.userName)
191223 .setAutoCancel(false )
192224 .setOngoing(true )
193225 .setVibrate(VIBRATE_PATTERN )
@@ -214,6 +246,7 @@ class CallNotificationBuilder @Inject constructor(
214246 return NotificationCompat .Builder (context, channelId)
215247 .setContentTitle(title)
216248 .setContentText(context.getString(R .string.notification_ongoing_call_content))
249+ .setSubText(data.userName)
217250 .setPriority(NotificationCompat .PRIORITY_DEFAULT )
218251 .setCategory(NotificationCompat .CATEGORY_CALL )
219252 .setVisibility(NotificationCompat .VISIBILITY_PUBLIC )
@@ -275,19 +308,23 @@ class CallNotificationBuilder @Inject constructor(
275308 }
276309}
277310
311+ data class IncomingCallsForUser (val userId : UserId , val userName : String , val incomingCalls : List <Call >)
312+
278313data class CallNotificationIds (val userIdString : String , val conversationIdString : String )
279314
280315data class CallNotificationData (
281316 val userId : QualifiedID ,
317+ val userName : String ,
282318 val conversationId : ConversationId ,
283319 val conversationName : String? ,
284320 val conversationType : Conversation .Type ,
285321 val callerName : String? ,
286322 val callerTeamName : String? ,
287323 val callStatus : CallStatus
288324) {
289- constructor (userId: UserId , call: Call ) : this (
325+ constructor (userId: UserId , call: Call , userName : String ) : this (
290326 userId,
327+ userName,
291328 call.conversationId,
292329 call.conversationName,
293330 call.conversationType,
@@ -296,5 +333,3 @@ data class CallNotificationData(
296333 call.status
297334 )
298335}
299-
300- fun Map.Entry <UserId , Call >.toCallNotificationData () = CallNotificationData (userId = key, call = value)
0 commit comments