Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/src/main/kotlin/com/wire/android/GlobalObserversManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
Expand Down Expand Up @@ -67,8 +68,10 @@ class GlobalObserversManager @Inject constructor(
scope.launch { setUpNotifications() }
scope.launch {
coreLogic.getGlobalScope().observeValidAccounts().distinctUntilChanged().collectLatest {
if (it.isNotEmpty()) {
coreLogic.getSessionScope(it.first().first.id).calls.endCallOnConversationChange()
coroutineScope {
it.forEach {
launch { coreLogic.getSessionScope(it.first.id).calls.endCallOnConversationChange() }
}
}
}
}
Expand Down Expand Up @@ -107,8 +110,15 @@ class GlobalObserversManager @Inject constructor(
.distinctUntilChanged()
.collectLatest {
// create notification channels for all valid users
appLogger.i("GlobalObserversManager: creating notification channels for users: ${it.map { it.first.id }}")
notificationChannelsManager.createUserNotificationChannels(it.map { it.first })

if (it.isEmpty()) {
appLogger.i("GlobalObserversManager: no valid users, stopping observing notifications")
notificationManager.clearWhenNoUsers()
return@collectLatest
}

// do not observe notifications for users with PersistentWebSocketEnabled, it will be done in PersistentWebSocketService
it.filter { (_, isPersistentWebSocketEnabled) -> !isPersistentWebSocketEnabled }
.map { (selfUser, _) -> selfUser.id }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import android.content.Context
import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.CallStyle
import androidx.core.app.NotificationManagerCompat
Expand Down Expand Up @@ -127,14 +128,17 @@
}
}

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) }

fun hideAllCallNotifications() {
hideIncomingCallNotifications { _, _ -> true }
notificationManager.cancel(NotificationIds.CALL_OUTGOING_ONGOING_NOTIFICATION_ID.ordinal)

Check warning on line 139 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L138-L139

Added lines #L138 - L139 were not covered by tests
}

fun bringBackIncomingCallNotification(userIdString: String, conversationIdString: String) {
scope.launch {
val userId = qualifiedIdMapper.fromStringToQualifiedID(userIdString)
Expand Down Expand Up @@ -213,14 +217,15 @@
val userIdString = data.userId.toString()
val conversationIdString = data.conversationId.toString()
val channelId = NotificationConstants.getOutgoingChannelId(data.userId)
val person = Person.Builder().setName(data.conversationName ?: "").build()
val title = getNotificationTitle(data = data, defaultGroupName = R.string.notification_outgoing_call_default_group_name)
val person = Person.Builder().setName(title).build()

val notificationBuilder = NotificationCompat.Builder(context, channelId)
return notificationBuilder
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(NR.drawable.notification_icon_small)
.setContentTitle(data.conversationName)
.setContentTitle(title)
.setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return))
.setSubText(data.userName)
.setAutoCancel(false)
Expand All @@ -241,7 +246,7 @@
fun getIncomingCallNotification(data: CallNotificationData, asFullScreenIntent: Boolean): Notification {
val conversationIdString = data.conversationId.toString()
val userIdString = data.userId.toString()
val title = getNotificationTitle(data)
val title = getNotificationTitle(data = data, defaultGroupName = R.string.notification_incoming_call_default_group_name)

Check warning on line 249 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L249

Added line #L249 was not covered by tests
val content = getNotificationBody(data)
val channelId = NotificationConstants.getIncomingChannelId(data.userId)
val person = Person.Builder().setName(title).build()
Expand Down Expand Up @@ -287,7 +292,7 @@
val channelId = NotificationConstants.ONGOING_CALL_CHANNEL_ID
val conversationIdString = data.conversationId.toString()
val userIdString = data.userId.toString()
val title = getNotificationTitle(data)
val title = getNotificationTitle(data = data, defaultGroupName = R.string.notification_ongoing_call_default_group_name)
val person = Person.Builder().setName(title).build()

return NotificationCompat.Builder(context, channelId)
Expand Down Expand Up @@ -315,41 +320,55 @@
/**
* @return placeholder Notification for CallService, that can be shown immediately after starting the Service
* (e.g. in [android.app.Service.onCreate]). It has no any [NotificationCompat.Action], on click - just opens the app.
* This notification should be replace by the user-specific notification (with corresponding [NotificationCompat.Action],
* This notification should be replaced by the user-specific notification (with corresponding [NotificationCompat.Action],
* [android.content.Intent] and title) once it's possible (e.g. in [android.app.Service.onStartCommand])
*/
fun getCallServicePlaceholderNotification(): Notification {
val channelId = NotificationConstants.ONGOING_CALL_CHANNEL_ID
return NotificationCompat.Builder(context, channelId)
.setContentTitle(context.getString(R.string.app_name))

Check warning on line 329 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L329

Added line #L329 was not covered by tests
.setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(NR.drawable.notification_icon_small)
.setAutoCancel(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setAutoCancel(false)

Check warning on line 336 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L335-L336

Added lines #L335 - L336 were not covered by tests
.setOngoing(true)
.setOnlyAlertOnce(true)
.setSilent(true)

Check warning on line 339 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L338-L339

Added lines #L338 - L339 were not covered by tests
.setContentIntent(openAppPendingIntent(context))
.build()
}

// Notifications content
private fun getNotificationBody(data: CallNotificationData) =
private fun getNotificationBody(
data: CallNotificationData,
@StringRes defaultCallerName: Int = R.string.notification_call_default_caller_name

Check warning on line 347 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L347

Added line #L347 was not covered by tests
) =
when (data.conversationType) {
is Conversation.Type.Group -> {
val name = data.callerName ?: context.getString(R.string.notification_call_default_caller_name)
(data.callerTeamName?.let { "$name @$it" } ?: name)
val name = data.callerName.orIfNullOrBlank(context.getString(defaultCallerName))

Check warning on line 351 in app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt#L351

Added line #L351 was not covered by tests
(data.callerTeamName.takeUnless { it.isNullOrBlank() }?.let { "$name @$it" } ?: name)
.let { context.getString(R.string.notification_group_channel_call_content, it) }
}

else -> context.getString(R.string.notification_incoming_call_content)
}

fun getNotificationTitle(data: CallNotificationData): String =
fun getNotificationTitle(
data: CallNotificationData,
@StringRes defaultGroupName: Int = R.string.notification_incoming_call_default_group_name,
@StringRes defaultCallerName: Int = R.string.notification_call_default_caller_name
): String =
when (data.conversationType) {
is Conversation.Type.Group -> data.conversationName ?: context.getString(R.string.notification_call_default_group_name)
is Conversation.Type.Group -> {
data.conversationName.orIfNullOrBlank(context.getString(defaultGroupName))
}

else -> {
val name = data.callerName ?: context.getString(R.string.notification_call_default_caller_name)
data.callerTeamName?.let { "$name @$it" } ?: name
val name = data.callerName.orIfNullOrBlank(context.getString(defaultCallerName))
data.callerTeamName.takeUnless { it.isNullOrBlank() }?.let { "$name @$it" } ?: name
}
}

Expand All @@ -358,6 +377,8 @@
}
}

private fun String?.orIfNullOrBlank(defaultValue: String): String = takeUnless { it.isNullOrBlank() } ?: defaultValue

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

data class CallNotificationData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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.dropWhile
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
Expand Down Expand Up @@ -117,6 +118,18 @@ class WireNotificationManager @Inject constructor(
scope: CoroutineScope
) = observeNotificationsAndCalls(userIds, scope, observingPersistentlyJobs)

/**
* When there are no valid users with active sessions, we want to stop all the notifications and calls observing jobs,
* hide all the notifications and stop the call service, because they are not relevant anymore.
*/
fun clearWhenNoUsers() {
observingWhileRunningJobs.cancelAndClearAll()
observingPersistentlyJobs.cancelAndClearAll()
messagesNotificationManager.hideAllNotifications()
callNotificationManager.hideAllCallNotifications()
servicesManager.stopCallService()
}

/**
* Become online, process all the Pending events,
* and display notifications for new events.
Expand Down Expand Up @@ -305,14 +318,6 @@ class WireNotificationManager @Inject constructor(
.forEach { userId -> stopObservingForUser(userId, observingJobs) }

if (userIds.isEmpty()) {
// userIds.isEmpty() means there is no current user (logged out e.g.)
// so we need to unsubscribe from the notification changes (done by canceling all the jobs above)
// and remove the notifications that were displayed previously
appLogger.i("$TAG no Users -> hide all the notifications")
messagesNotificationManager.hideAllNotifications()
callNotificationManager.hideAllIncomingCallNotifications()
servicesManager.stopCallService()

return
}

Expand Down Expand Up @@ -498,10 +503,17 @@ class WireNotificationManager @Inject constructor(
coreLogic.getGlobalScope().session.currentSessionFlow()
.flatMapLatest {
if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) {
combine(
coreLogic.getSessionScope(it.accountInfo.userId).calls.establishedCall(),
coreLogic.getSessionScope(it.accountInfo.userId).calls.observeOutgoingCall()
) { establishedCalls, outgoingCalls -> (establishedCalls + outgoingCalls).isNotEmpty() }
val sessionScope = coreLogic.getSessionScope(it.accountInfo.userId)
// wait for the initial cleanup of stale open calls to be completed before starting to observe the calls,
// to avoid starting the service by mistake for the calls that are already stale
sessionScope.calls.observeStaleOpenCallsCleanup()
.dropWhile { completed -> !completed }
.flatMapLatest {
combine(
sessionScope.calls.establishedCall(),
sessionScope.calls.observeOutgoingCall()
) { establishedCalls, outgoingCalls -> (establishedCalls + outgoingCalls).isNotEmpty() }
}
} else {
flowOf(null)
}
Expand Down Expand Up @@ -593,7 +605,14 @@ class WireNotificationManager @Inject constructor(
private data class ObservingJobs(
val outgoingOngoingCallJob: AtomicReference<Job?> = AtomicReference(),
val userJobs: ConcurrentHashMap<QualifiedID, UserObservingJobs> = ConcurrentHashMap()
)
) {
fun cancelAndClearAll() {
outgoingOngoingCallJob.get()?.cancel()
outgoingOngoingCallJob.set(null)
userJobs.values.forEach { it.cancelAll() }
userJobs.clear()
}
}

companion object {
private const val TAG = "WireNotificationManager"
Expand Down
59 changes: 47 additions & 12 deletions app/src/main/kotlin/com/wire/android/services/CallService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,23 @@

package com.wire.android.services

import android.Manifest.permission.RECORD_AUDIO
import android.app.Notification
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.IBinder
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import com.wire.android.appLogger
import com.wire.android.notification.CallNotificationData
import com.wire.android.notification.CallNotificationManager
import com.wire.android.notification.NotificationChannelsManager
import com.wire.android.notification.NotificationIds
import com.wire.android.services.CallService.Action
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.common.functional.fold
import com.wire.kalium.logic.data.call.CallStatus
Expand Down Expand Up @@ -59,6 +64,12 @@
@Inject
lateinit var callNotificationManager: CallNotificationManager

@Inject
lateinit var notificationChannelsManager: NotificationChannelsManager

Check warning on line 68 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L68

Added line #L68 was not covered by tests

@Inject
lateinit var currentScreenManager: CurrentScreenManager

Check warning on line 71 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L71

Added line #L71 was not covered by tests

@Inject
lateinit var dispatcherProvider: DispatcherProvider

Expand All @@ -79,8 +90,8 @@

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
appLogger.i("$TAG: onStartCommand")
startForegroundWithPlaceholderNotification()

Check warning on line 93 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L93

Added line #L93 was not covered by tests
val action = intent?.getActionTypeExtra(EXTRA_ACTION_TYPE) ?: Action.Start.Default
generatePlaceholderForegroundNotification()
_serviceState.value = ServiceState.FOREGROUND
scope.launch {
lifecycleManager.handleAction(action)
Expand Down Expand Up @@ -123,18 +134,40 @@
}
}

private fun generatePlaceholderForegroundNotification() {
private fun startForegroundWithPlaceholderNotification() {
appLogger.i("$TAG: generating foregroundNotification placeholder...")
val notification: Notification =
callNotificationManager.builder.getCallServicePlaceholderNotification()
ServiceCompat.startForeground(
this,
NotificationIds.CALL_OUTGOING_ONGOING_NOTIFICATION_ID.ordinal,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
)

appLogger.i("$TAG: started foreground with placeholder notification")
notificationChannelsManager.createOngoingCallNotificationChannel()
val notification: Notification = callNotificationManager.builder.getCallServicePlaceholderNotification()
val hasRecordAudioPermission = ContextCompat.checkSelfPermission(this, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val isAppVisible = currentScreenManager.isAppVisibleFlow().value

Check warning on line 142 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L139-L142

Added lines #L139 - L142 were not covered by tests
try {
ServiceCompat.startForeground(
this,
NotificationIds.CALL_OUTGOING_ONGOING_NOTIFICATION_ID.ordinal,
notification,
when (hasRecordAudioPermission) {

Check warning on line 148 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L144-L148

Added lines #L144 - L148 were not covered by tests
true -> ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE

// Safety measure, we don't have the required permission, but startForeground still needs to be called to avoid crash.
// In that case, we start it with the type that doesn't require the runtime permission to avoid potential crash,
// and then the service is being stopped right after starting foreground.
false -> ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL

Check warning on line 154 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L154

Added line #L154 was not covered by tests
}
)
appLogger.i("$TAG: started foreground with placeholder notification, isAppVisible:$isAppVisible")

Check warning on line 157 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L157

Added line #L157 was not covered by tests
if (!hasRecordAudioPermission) {
scope.launch {
appLogger.i("$TAG: Stopping service started without RECORD_AUDIO permission")
lifecycleManager.handleAction(Action.Stop)

Check warning on line 161 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L159-L161

Added lines #L159 - L161 were not covered by tests
}
}
} catch (e: SecurityException) {
throw StartForegroundException(

Check warning on line 165 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L165

Added line #L165 was not covered by tests
message = "$TAG: Unable to start foreground service, " +
"RECORD_AUDIO permission: $hasRecordAudioPermission, isAppVisible:$isAppVisible, original message: ${e.message}",
cause = e,

Check warning on line 168 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L167-L168

Added lines #L167 - L168 were not covered by tests
)
}
}

companion object {
Expand Down Expand Up @@ -174,3 +207,5 @@
private fun Intent.getActionTypeExtra(name: String): Action? = getBundleExtra(name)?.let {
Bundlizer.unbundle(Action.serializer(), it)
}

private class StartForegroundException(override val message: String, override val cause: Throwable?) : RuntimeException(message, cause)

Check warning on line 211 in app/src/main/kotlin/com/wire/android/services/CallService.kt

View check run for this annotation

Codecov / codecov/patch

app/src/main/kotlin/com/wire/android/services/CallService.kt#L211

Added line #L211 was not covered by tests
Loading
Loading