Skip to content

Commit

Permalink
Merge branch 'release/candidate' into feat/disable-x86-support
Browse files Browse the repository at this point in the history
  • Loading branch information
MohamadJaara authored Nov 8, 2024
2 parents 1a7485f + e8b5148 commit 8da9e44
Show file tree
Hide file tree
Showing 31 changed files with 724 additions and 309 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ package com.wire.android.notification
import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.notification.NotificationConstants.INCOMING_CALL_ID_PREFIX
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallStatus
Expand All @@ -34,17 +36,16 @@ import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
Expand All @@ -62,75 +63,108 @@ class CallNotificationManager @Inject constructor(

private val notificationManager = NotificationManagerCompat.from(context)
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default())
private val incomingCallsForUsers = MutableStateFlow<Map<UserId, Call>>(mapOf())
private val incomingCallsForUsers = MutableStateFlow<Map<UserId, IncomingCallsForUser>>(mapOf())
private val reloadCallNotification = MutableSharedFlow<CallNotificationIds>()

init {
scope.launch {
incomingCallsForUsers
.debounce { if (it.isEmpty()) 0L else DEBOUNCE_TIME } // debounce to avoid showing and hiding notification too fast
.map { it.entries.firstOrNull()?.toCallNotificationData() }
.map {
it.values.map { (userId, userName, calls) ->
calls.map { call ->
CallNotificationData(userId, call, userName)
}
}.flatten()
}
.scan(emptyList<CallNotificationData>() to emptyList<CallNotificationData>()) { (previousCalls, _), currentCalls ->
currentCalls to (currentCalls - previousCalls.toSet())
}
.distinctUntilChanged()
.reloadIfNeeded()
.collectLatest { incomingCallData ->
if (incomingCallData == null) {
hideIncomingCallNotification()
} else {
appLogger.i("$TAG: showing incoming call")
showIncomingCallNotification(incomingCallData)
.flatMapLatest { (allCurrentCalls, newCalls) ->
reloadCallNotification
.map { (userIdString, conversationIdString) ->
allCurrentCalls to allCurrentCalls.filter { // emit call that needs to be reloaded as newOrUpdated
it.userId.toString() == userIdString && it.conversationId.toString() == conversationIdString
}
}
.filter { (_, newCalls) -> newCalls.isNotEmpty() } // only emit if there is something to reload
.onStart { emit(allCurrentCalls to newCalls) }
}
.collectLatest { (allCurrentCalls, newCalls) ->
// remove outdated incoming call notifications
hideOutdatedIncomingCallNotifications(allCurrentCalls)
// show current incoming call notifications
appLogger.i("$TAG: showing ${newCalls.size} new incoming calls (all incoming calls: ${allCurrentCalls.size})")
newCalls.forEach { data ->
showIncomingCallNotification(data)
}
}
}
}

fun reloadIfNeeded(data: CallNotificationData): Flow<CallNotificationData> = reloadCallNotification
.filter { reloadCallNotificationIds -> // check if the reload action is for the same call
reloadCallNotificationIds.userIdString == data.userId.toString()
&& reloadCallNotificationIds.conversationIdString == data.conversationId.toString()
@VisibleForTesting
internal fun hideOutdatedIncomingCallNotifications(currentIncomingCalls: List<CallNotificationData>) {
val currentIncomingCallNotificationIds = currentIncomingCalls.map {
NotificationConstants.getIncomingCallId(it.userId.toString(), it.conversationId.toString())
}
.map { data }
.onStart { emit(data) }

private fun Flow<CallNotificationData?>.reloadIfNeeded(): Flow<CallNotificationData?> = this.flatMapLatest { callEntry ->
callEntry?.let { reloadIfNeeded(it) } ?: flowOf(null)
hideIncomingCallNotifications { _, id -> !currentIncomingCallNotificationIds.contains(id) }
}

fun reloadCallNotifications(reloadCallNotificationIds: CallNotificationIds) = scope.launch {
reloadCallNotification.emit(reloadCallNotificationIds)
}

fun handleIncomingCallNotifications(calls: List<Call>, userId: UserId) {
fun handleIncomingCalls(calls: List<Call>, userId: UserId, userName: String) {
if (calls.isEmpty()) {
incomingCallsForUsers.update { it.filter { it.key != userId } }
incomingCallsForUsers.update {
it.minus(userId)
}
} else {
incomingCallsForUsers.update { it.filter { it.key != userId } + (userId to calls.first()) }
incomingCallsForUsers.update {
it.plus(userId to IncomingCallsForUser(userId, userName, calls))
}
}
}

fun hideAllNotifications() {
hideIncomingCallNotification()
private fun hideIncomingCallNotifications(predicate: (tag: String, id: Int) -> Boolean) {
notificationManager.activeNotifications.filter {
it.tag?.startsWith(INCOMING_CALL_ID_PREFIX) == true && predicate(it.tag, it.id)
}.forEach {
it.hideIncomingCallNotification()
}
}

private fun hideIncomingCallNotification() {
fun hideAllIncomingCallNotifications() = hideIncomingCallNotifications { _, _ -> true }

fun hideAllIncomingCallNotificationsForUser(userId: UserId) =
hideIncomingCallNotifications { tag, _ -> tag == NotificationConstants.getIncomingCallTag(userId.toString()) }

fun hideIncomingCallNotification(userIdString: String, conversationIdString: String) =
hideIncomingCallNotifications { _, id -> id == NotificationConstants.getIncomingCallId(userIdString, conversationIdString) }

private fun StatusBarNotification.hideIncomingCallNotification() {
appLogger.i("$TAG: hiding incoming call")

// This delay is just so when the user receives two calling signals one straight after the other [INCOMING -> CANCEL]
// 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
// cancelling vibration as probably when we were cancelling, the vibration object was still being created and started and thus
// never stopped.
// 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 cancelling vibration as probably when we were cancelling, the vibration object was still being created and started
// and thus never stopped.
TimeUnit.MILLISECONDS.sleep(CANCEL_CALL_NOTIFICATION_DELAY)
notificationManager.cancel(NotificationIds.CALL_INCOMING_NOTIFICATION_ID.ordinal)
notificationManager.cancel(tag, id)
}

@SuppressLint("MissingPermission")
@VisibleForTesting
internal fun showIncomingCallNotification(data: CallNotificationData) {
appLogger.i("$TAG: showing incoming call notification for user ${data.userId.toLogString()}")
val notification = builder.getIncomingCallNotification(data)
notificationManager.notify(
NotificationIds.CALL_INCOMING_NOTIFICATION_ID.ordinal,
notification
appLogger.i(
"$TAG: showing incoming call notification for user ${data.userId.toLogString()}" +
" and conversation ${data.conversationId.toLogString()}"
)
val tag = NotificationConstants.getIncomingCallTag(data.userId.toString())
val id = NotificationConstants.getIncomingCallId(data.userId.toString(), data.conversationId.toString())
val notification = builder.getIncomingCallNotification(data)
notificationManager.notify(tag, id, notification)
}

// Notifications
Expand All @@ -141,10 +175,6 @@ class CallNotificationManager @Inject constructor(

@VisibleForTesting
internal const val DEBOUNCE_TIME = 200L

fun hideIncomingCallNotification(context: Context) {
NotificationManagerCompat.from(context).cancel(NotificationIds.CALL_INCOMING_NOTIFICATION_ID.ordinal)
}
}
}

Expand All @@ -164,6 +194,7 @@ class CallNotificationBuilder @Inject constructor(
.setSmallIcon(R.drawable.notification_icon_small)
.setContentTitle(data.conversationName)
.setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return))
.setSubText(data.userName)
.setAutoCancel(false)
.setOngoing(true)
.setSilent(true)
Expand All @@ -188,6 +219,7 @@ class CallNotificationBuilder @Inject constructor(
.setSmallIcon(R.drawable.notification_icon_small)
.setContentTitle(title)
.setContentText(content)
.setSubText(data.userName)
.setAutoCancel(false)
.setOngoing(true)
.setVibrate(VIBRATE_PATTERN)
Expand All @@ -214,6 +246,7 @@ class CallNotificationBuilder @Inject constructor(
return NotificationCompat.Builder(context, channelId)
.setContentTitle(title)
.setContentText(context.getString(R.string.notification_ongoing_call_content))
.setSubText(data.userName)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
Expand Down Expand Up @@ -275,19 +308,23 @@ class CallNotificationBuilder @Inject constructor(
}
}

data class IncomingCallsForUser(val userId: UserId, val userName: String, val incomingCalls: List<Call>)

data class CallNotificationIds(val userIdString: String, val conversationIdString: String)

data class CallNotificationData(
val userId: QualifiedID,
val userName: String,
val conversationId: ConversationId,
val conversationName: String?,
val conversationType: Conversation.Type,
val callerName: String?,
val callerTeamName: String?,
val callStatus: CallStatus
) {
constructor(userId: UserId, call: Call) : this(
constructor(userId: UserId, call: Call, userName: String) : this(
userId,
userName,
call.conversationId,
call.conversationName,
call.conversationType,
Expand All @@ -296,5 +333,3 @@ data class CallNotificationData(
call.status
)
}

fun Map.Entry<UserId, Call>.toCallNotificationData() = CallNotificationData(userId = key, call = value)
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ object NotificationConstants {
// MessagesSummaryNotification ID depends on User, use fun getMessagesSummaryId(userId: UserId) to get it
private const val MESSAGE_SUMMARY_ID_STRING = "wire_messages_summary_notification"

private const val INCOMING_CALL_TAG_PREFIX = "wire_incoming_call_tag_"
const val INCOMING_CALL_ID_PREFIX = "wire_incoming_call_"

fun getConversationNotificationId(conversationIdString: String, userIdString: String) = (conversationIdString + userIdString).hashCode()
fun getMessagesGroupKey(userId: UserId?): String = "$MESSAGE_GROUP_KEY_PREFIX${userId?.toString() ?: ""}"
fun getMessagesSummaryId(userId: UserId): Int = "$MESSAGE_SUMMARY_ID_STRING$userId".hashCode()
Expand All @@ -60,6 +63,10 @@ object NotificationConstants {
fun getPingsChannelId(userId: UserId): String = getChanelIdForUser(userId, PING_CHANNEL_ID)
fun getIncomingChannelId(userId: UserId): String = getChanelIdForUser(userId, INCOMING_CALL_CHANNEL_ID)
fun getOutgoingChannelId(userId: UserId): String = getChanelIdForUser(userId, OUTGOING_CALL_CHANNEL_ID)
fun getIncomingCallId(userIdString: String, conversationIdString: String): Int =
"$INCOMING_CALL_ID_PREFIX${userIdString}_$conversationIdString".hashCode()

fun getIncomingCallTag(userIdString: String): String = "$INCOMING_CALL_TAG_PREFIX$userIdString"

/**
* @return NotificationChannelId [String] specific for user, use it to post a notifications.
Expand All @@ -72,7 +79,12 @@ object NotificationConstants {

// Notification IDs (has to be unique!)
enum class NotificationIds {
CALL_INCOMING_NOTIFICATION_ID,
@Suppress("unused")
@Deprecated(
message = "Do not use it, it's here just because we use .ordinal as ID and ID for the foreground service notification cannot be 0",
level = DeprecationLevel.ERROR
)
ZERO_ID,
CALL_OUTGOING_ONGOING_NOTIFICATION_ID,
PERSISTENT_NOTIFICATION_ID,
MESSAGE_SYNC_NOTIFICATION_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.wire.android.util.CurrentScreen
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.lifecycle.ConnectionPolicyManager
import com.wire.android.util.logIfEmptyUserName
import com.wire.kalium.logger.obfuscateId
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.id.ConversationId
Expand All @@ -51,6 +52,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -249,7 +251,7 @@ class WireNotificationManager @Inject constructor(
// and remove the notifications that were displayed previously
appLogger.i("$TAG no Users -> hide all the notifications")
messagesNotificationManager.hideAllNotifications()
callNotificationManager.hideAllNotifications()
callNotificationManager.hideAllIncomingCallNotifications()
servicesManager.stopCallService()

return
Expand Down Expand Up @@ -297,6 +299,7 @@ class WireNotificationManager @Inject constructor(

private fun stopObservingForUser(userId: UserId, observingJobs: ObservingJobs) {
messagesNotificationManager.hideAllNotificationsForUser(userId)
callNotificationManager.hideAllIncomingCallNotificationsForUser(userId)
observingJobs.userJobs[userId]?.cancelAll()
observingJobs.userJobs.remove(userId)
}
Expand Down Expand Up @@ -336,20 +339,26 @@ class WireNotificationManager @Inject constructor(
) {
appLogger.d("$TAG observe incoming calls")

coreLogic.getSessionScope(userId).observeE2EIRequired()
.map { it is E2EIRequiredResult.NoGracePeriod }
.distinctUntilChanged()
.flatMapLatest { isBlockedByE2EIRequired ->
if (isBlockedByE2EIRequired) {
appLogger.d("$TAG calls were blocked as E2EI is required")
flowOf(listOf())
} else {
coreLogic.getSessionScope(userId).calls.getIncomingCalls()
coreLogic.getSessionScope(userId).let { userSessionScope ->
userSessionScope.observeE2EIRequired()
.map { it is E2EIRequiredResult.NoGracePeriod }
.distinctUntilChanged()
.flatMapLatest { isBlockedByE2EIRequired ->
if (isBlockedByE2EIRequired) {
appLogger.d("$TAG calls were blocked as E2EI is required")
flowOf(listOf())
} else {
userSessionScope.calls.getIncomingCalls()
}.map { calls ->
userSessionScope.users.getSelfUser().first()
.also { it.logIfEmptyUserName() }
.let { it.handle ?: it.name ?: "" } to calls
}
}
}
.collect { calls ->
callNotificationManager.handleIncomingCallNotifications(calls, userId)
}
.collect { (userName, calls) ->
callNotificationManager.handleIncomingCalls(calls, userId, userName)
}
}
}

/**
Expand All @@ -366,6 +375,7 @@ class WireNotificationManager @Inject constructor(
val selfUserNameState = coreLogic.getSessionScope(userId)
.users
.getSelfUser()
.onEach { it.logIfEmptyUserName() }
.map { it.handle ?: it.name ?: "" }
.distinctUntilChanged()
.stateIn(scope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import android.content.Intent
import com.wire.android.appLogger
import com.wire.android.notification.CallNotificationIds
import com.wire.android.notification.CallNotificationManager
import com.wire.kalium.logger.obfuscateId
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

Expand All @@ -36,7 +37,10 @@ class CallNotificationDismissedReceiver : BroadcastReceiver() { // requires zero
override fun onReceive(context: Context, intent: Intent) {
val conversationIdString: String = intent.getStringExtra(EXTRA_CONVERSATION_ID) ?: return
val userIdString: String = intent.getStringExtra(EXTRA_USER_ID) ?: return
appLogger.i("CallNotificationDismissedReceiver: onReceive")
appLogger.i(
"CallNotificationDismissedReceiver: onReceive for user ${userIdString.obfuscateId()}" +
" and conversation ${conversationIdString.obfuscateId()}"
)
callNotificationManager.reloadCallNotifications(CallNotificationIds(userIdString, conversationIdString))
}

Expand Down
Loading

0 comments on commit 8da9e44

Please sign in to comment.