Skip to content

Commit

Permalink
fix: crash when stopping OngoingCallService [WPB-2320] [WPB-1836] [WP…
Browse files Browse the repository at this point in the history
…B-3457] (#2084)

Co-authored-by: Michał Saleniuk <[email protected]>
Co-authored-by: Tommaso Piazza <[email protected]>
Co-authored-by: Michał Saleniuk <[email protected]>
  • Loading branch information
4 people authored Aug 11, 2023
1 parent cbd5ae1 commit db45542
Show file tree
Hide file tree
Showing 8 changed files with 990 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,59 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.call.Call
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
@Suppress("TooManyFunctions")
class CallNotificationManager @Inject constructor(private val context: Context) {
class CallNotificationManager @Inject constructor(
context: Context,
dispatcherProvider: DispatcherProvider,
val builder: CallNotificationBuilder,
) {

private val notificationManager = NotificationManagerCompat.from(context)
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default())
private val incomingCallsForUsers = MutableStateFlow<List<Pair<UserId, Call>>>(emptyList())

init {
scope.launch {
incomingCallsForUsers
.debounce { if (it.isEmpty()) 0L else DEBOUNCE_TIME } // debounce to avoid showing and hiding notification too fast
.distinctUntilChanged()
.collectLatest {
if (it.isEmpty()) {
hideIncomingCallNotification()
} else {
it.first().let { (userId, call) ->
appLogger.i("$TAG: showing incoming call")
showIncomingCallNotification(call, userId)
}
}
}
}
}

fun handleIncomingCallNotifications(calls: List<Call>, userId: QualifiedID?) {
if (calls.isEmpty() || userId == null) {
hideIncomingCallNotification()
fun handleIncomingCallNotifications(calls: List<Call>, userId: UserId) {
if (calls.isEmpty()) {
incomingCallsForUsers.update { it.filter { it.first != userId } }
} else {
appLogger.i("$TAG: showing incoming call")
showIncomingCallNotification(calls.first(), userId)
incomingCallsForUsers.update { it.filter { it.first != userId } + (userId to calls.first()) }
}
}

Expand All @@ -64,13 +97,32 @@ class CallNotificationManager @Inject constructor(private val context: Context)
notificationManager.cancel(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID)
}

private fun showIncomingCallNotification(call: Call, userId: QualifiedID) {
val notification = getIncomingCallNotification(call, userId)
@VisibleForTesting
internal fun showIncomingCallNotification(call: Call, userId: QualifiedID) {
appLogger.i("$TAG: showing incoming call for user ${userId.toLogString()}")
val notification = builder.getIncomingCallNotification(call, userId)
notificationManager.notify(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID, notification)
}

// Notifications
private fun getIncomingCallNotification(call: Call, userId: QualifiedID): Notification {

companion object {
private const val TAG = "CallNotificationManager"
private const val CANCEL_CALL_NOTIFICATION_DELAY = 300L
@VisibleForTesting
internal const val DEBOUNCE_TIME = 200L

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

@Singleton
class CallNotificationBuilder @Inject constructor(
private val context: Context,
) {
fun getIncomingCallNotification(call: Call, userId: QualifiedID): Notification {
val conversationIdString = call.conversationId.toString()
val userIdString = userId.toString()
val title = getNotificationTitle(call)
Expand Down Expand Up @@ -106,7 +158,7 @@ class CallNotificationManager @Inject constructor(private val context: Context)
return NotificationCompat.Builder(context, channelId)
.setContentTitle(callName)
.setContentText(context.getString(R.string.notification_ongoing_call_content))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.notification_icon_small)
Expand All @@ -128,7 +180,7 @@ class CallNotificationManager @Inject constructor(private val context: Context)
val channelId = NotificationConstants.ONGOING_CALL_CHANNEL_ID
return NotificationCompat.Builder(context, channelId)
.setContentText(context.getString(R.string.notification_ongoing_call_content))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(R.drawable.notification_icon_small)
Expand Down Expand Up @@ -160,13 +212,7 @@ class CallNotificationManager @Inject constructor(private val context: Context)
}

companion object {
private const val TAG = "CallNotificationManager"
private const val INCOMING_CALL_TIMEOUT: Long = 30 * 1000
private val VIBRATE_PATTERN = longArrayOf(0, 1000, 1000)
private const val CANCEL_CALL_NOTIFICATION_DELAY = 300L

fun hideIncomingCallNotification(context: Context) {
NotificationManagerCompat.from(context).cancel(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ class NotificationChannelsManager @Inject constructor(
private fun createOngoingNotificationChannel() {
val channelId = NotificationConstants.ONGOING_CALL_CHANNEL_ID
val notificationChannel = NotificationChannelCompat
.Builder(channelId, NotificationManagerCompat.IMPORTANCE_MAX)
.Builder(channelId, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(NotificationConstants.ONGOING_CALL_CHANNEL_NAME)
.setVibrationEnabled(false)
.setImportance(NotificationManagerCompat.IMPORTANCE_MAX)
.setImportance(NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setSound(null, null)
.build()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.wire.kalium.logic.data.notification.LocalNotificationConversation
import com.wire.kalium.logic.data.notification.LocalNotificationMessage
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.message.MarkMessagesAsNotifiedUseCase
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.session.GetAllSessionsResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
Expand All @@ -51,10 +52,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -75,8 +79,8 @@ class WireNotificationManager @Inject constructor(
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default())
private val fetchOnceMutex = Mutex()
private val fetchOnceJobs = hashMapOf<UserId, Job>()
private val observingWhileRunningJobs = hashMapOf<UserId, ObservingJobs>()
private val observingPersistentlyJobs = hashMapOf<UserId, ObservingJobs>()
private var observingWhileRunningJobs = ObservingJobs()
private var observingPersistentlyJobs = ObservingJobs()

/**
* Stops all the ObservingNotifications jobs that are currently running, for a specific User.
Expand Down Expand Up @@ -164,8 +168,8 @@ class WireNotificationManager @Inject constructor(

private suspend fun observeMessageNotificationsOnceJob(userId: UserId): Job? {
val isMessagesAlreadyObserving =
observingWhileRunningJobs[userId]?.run { messagesJob.isActive }
?: observingPersistentlyJobs[userId]?.run { messagesJob.isActive }
observingWhileRunningJobs.userJobs[userId]?.run { messagesJob.isActive }
?: observingPersistentlyJobs.userJobs[userId]?.run { messagesJob.isActive }
?: false

return if (isMessagesAlreadyObserving) {
Expand All @@ -179,8 +183,8 @@ class WireNotificationManager @Inject constructor(

private suspend fun observeCallNotificationsOnceJob(userId: UserId): Job? {
val isCallsAlreadyObserving =
observingWhileRunningJobs[userId]?.run { incomingCallsJob.isActive }
?: observingPersistentlyJobs[userId]?.run { incomingCallsJob.isActive }
observingWhileRunningJobs.userJobs[userId]?.run { incomingCallsJob.isActive }
?: observingPersistentlyJobs.userJobs[userId]?.run { incomingCallsJob.isActive }
?: false

return if (isCallsAlreadyObserving) {
Expand Down Expand Up @@ -220,12 +224,12 @@ class WireNotificationManager @Inject constructor(
private suspend fun observeNotificationsAndCalls(
userIds: List<UserId>,
scope: CoroutineScope,
observingJobs: HashMap<UserId, ObservingJobs>
observingJobs: ObservingJobs
) {
val currentScreenState = currentScreenManager.observeCurrentScreen(scope)

// removing notifications and stop observing it for the users that are not logged in anymore
observingJobs.keys.filter { !userIds.contains(it) }
observingJobs.userJobs.keys.filter { !userIds.contains(it) }
.forEach { userId -> stopObservingForUser(userId, observingJobs) }

if (userIds.isEmpty()) {
Expand All @@ -242,9 +246,9 @@ class WireNotificationManager @Inject constructor(

// start observing notifications only for new users
userIds
.filter { observingJobs[it]?.isAllActive() != true }
.filter { observingJobs.userJobs[it]?.isAllActive() != true }
.forEach { userId ->
val jobs = ObservingJobs(
val jobs = UserObservingJobs(
currentScreenJob = scope.launch(dispatcherProvider.default()) {
observeCurrentScreenAndHideNotifications(currentScreenState, userId)
},
Expand All @@ -254,18 +258,23 @@ class WireNotificationManager @Inject constructor(
messagesJob = scope.launch(dispatcherProvider.default()) {
observeMessageNotifications(userId, currentScreenState)
},
ongoingCallJob = scope.launch(dispatcherProvider.default()) {
observeOngoingCalls(currentScreenState, userId)
}
)
observingJobs[userId] = jobs
observingJobs.userJobs[userId] = jobs
}

// start observing ongoing calls for all users, but only if not yet started
if (observingJobs.ongoingCallJob.get().let { it == null || !it.isActive }) {
val job = scope.launch(dispatcherProvider.default()) {
observeOngoingCalls(currentScreenState)
}
observingJobs.ongoingCallJob.set(job)
}
}

private fun stopObservingForUser(userId: UserId, observingJobs: HashMap<UserId, ObservingJobs>) {
private fun stopObservingForUser(userId: UserId, observingJobs: ObservingJobs) {
messagesNotificationManager.hideAllNotificationsForUser(userId)
observingJobs[userId]?.cancelAll()
observingJobs.remove(userId)
observingJobs.userJobs[userId]?.cancelAll()
observingJobs.userJobs.remove(userId)
}

/**
Expand Down Expand Up @@ -355,45 +364,37 @@ class WireNotificationManager @Inject constructor(
}

/**
* Infinitely listen for the established calls and run OngoingCall foreground Service
* Infinitely listen for the established calls of a current user and run OngoingCall foreground Service
* to show corresponding notification and do not lose a call.
* @param userId QualifiedID of User that we want to observe for
* @param currentScreenState StateFlow that informs which screen is currently visible,
* so we can listen established calls only when the app is in background.
*/
private suspend fun observeOngoingCalls(
currentScreenState: StateFlow<CurrentScreen>,
userId: UserId
) {
private suspend fun observeOngoingCalls(currentScreenState: StateFlow<CurrentScreen>) {
currentScreenState
.flatMapLatest { currentScreen ->
if (currentScreen !is CurrentScreen.InBackground) {
flowOf(null)
} else {
try {
coreLogic.getSessionScope(userId).calls
.establishedCall()
.map {
it.firstOrNull()?.let { call ->
OngoingCallData(callNotificationManager.getNotificationTitle(call), call.conversationId, userId)
}
coreLogic.getGlobalScope().session.currentSessionFlow()
.flatMapLatest {
if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) {
coreLogic.getSessionScope(it.accountInfo.userId).calls.establishedCall()
.map {
it.firstOrNull()
}
} else {
flowOf(null)
}
} catch (e: IllegalStateException) {
flowOf(null)
}
}
}
}
.distinctUntilChanged()
.collect { ongoingCallData ->
if (ongoingCallData == null) {
servicesManager.stopOngoingCallService()
} else {
servicesManager.startOngoingCallService(
ongoingCallData.notificationTitle,
ongoingCallData.conversationId,
ongoingCallData.userId
)
}
.onCompletion {
servicesManager.stopOngoingCallService()
}
.collect { call ->
if (call != null) servicesManager.startOngoingCallService()
else servicesManager.stopOngoingCallService()
}
}

Expand Down Expand Up @@ -453,25 +454,26 @@ class WireNotificationManager @Inject constructor(
val userName: String
)

private data class OngoingCallData(val notificationTitle: String, val conversationId: ConversationId, val userId: UserId)

private data class ObservingJobs(
private data class UserObservingJobs(
val currentScreenJob: Job,
val incomingCallsJob: Job,
val messagesJob: Job,
val ongoingCallJob: Job,
) {
fun cancelAll() {
currentScreenJob.cancel()
incomingCallsJob.cancel()
messagesJob.cancel()
ongoingCallJob.cancel()
}

fun isAllActive(): Boolean =
currentScreenJob.isActive && incomingCallsJob.isActive && messagesJob.isActive && ongoingCallJob.isActive
currentScreenJob.isActive && incomingCallsJob.isActive && messagesJob.isActive
}

private data class ObservingJobs(
val ongoingCallJob: AtomicReference<Job?> = AtomicReference(),
val userJobs: ConcurrentHashMap<QualifiedID, UserObservingJobs> = ConcurrentHashMap()
)

companion object {
private const val TAG = "WireNotificationManager"
private const val STAY_ALIVE_TIME_ON_PUSH_MS = 1000L
Expand Down
Loading

0 comments on commit db45542

Please sign in to comment.