Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: not able to open some conversations [WPB-11325] #3721

Merged
merged 1 commit into from
Dec 10, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import com.wire.android.R
import com.wire.android.appLogger
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.notification.NotificationConstants.INCOMING_CALL_ID_PREFIX
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.CoreLogic
import com.wire.kalium.logic.data.call.Call
import com.wire.kalium.logic.data.call.CallStatus
import com.wire.kalium.logic.data.conversation.Conversation
import com.wire.kalium.logic.data.id.ConversationId
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.session.CurrentSessionResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -54,9 +57,10 @@ import javax.inject.Singleton
@Singleton
@Suppress("TooManyFunctions")
class CallNotificationManager @Inject constructor(
private val context: Context,
context: Context,
dispatcherProvider: DispatcherProvider,
val builder: CallNotificationBuilder,
@KaliumCoreLogic private val coreLogic: CoreLogic,
) {

private val notificationManager = NotificationManagerCompat.from(context)
Expand All @@ -83,8 +87,19 @@ class CallNotificationManager @Inject constructor(
hideOutdatedIncomingCallNotifications(allCurrentCalls)
// show current incoming call notifications
appLogger.i("$TAG: showing ${newCalls.size} new incoming calls (all incoming calls: ${allCurrentCalls.size})")

val currentSessionId = (coreLogic.getGlobalScope().session.currentSession() as? CurrentSessionResult.Success)?.let {
if (it.accountInfo.isValid()) it.accountInfo.userId else null
}
newCalls.forEach { data ->
showIncomingCallNotification(data)
/**
* For now only show full screen intent for current session, as if shown for another session it will switch to that
* session and the user won't know that he/she is receiving a call as a different account.
* For calls that are not for the current session it will show the notification as a heads up notification.
* In the future we can implement showing on the incoming call screen as what account the user will answer
* or even give them the option to change it themselves on that screen.
*/
showIncomingCallNotification(data = data, asFullScreenIntent = currentSessionId == data.userId)
}
}
}
Expand Down Expand Up @@ -139,14 +154,14 @@ class CallNotificationManager @Inject constructor(

@SuppressLint("MissingPermission")
@VisibleForTesting
internal fun showIncomingCallNotification(data: CallNotificationData) {
internal fun showIncomingCallNotification(data: CallNotificationData, asFullScreenIntent: Boolean) {
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)
val notification = builder.getIncomingCallNotification(data, asFullScreenIntent)
notificationManager.notify(tag, id, notification)
}

Expand Down Expand Up @@ -189,12 +204,11 @@ class CallNotificationBuilder @Inject constructor(
)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setFullScreenIntent(outgoingCallPendingIntent(context, conversationIdString), true)
.setContentIntent(outgoingCallPendingIntent(context, conversationIdString))
.build()
}

fun getIncomingCallNotification(data: CallNotificationData): Notification {
fun getIncomingCallNotification(data: CallNotificationData, asFullScreenIntent: Boolean): Notification {
val conversationIdString = data.conversationId.toString()
val userIdString = data.userId.toString()
val title = getNotificationTitle(data)
Expand All @@ -220,8 +234,14 @@ class CallNotificationBuilder @Inject constructor(
)
.setVibrate(VIBRATE_PATTERN)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setFullScreenIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString, userIdString), true)
.setContentIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString, userIdString))
.let {
if (asFullScreenIntent) {
it.setFullScreenIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString, userIdString), true)
} else {
it
}
}
.build()

// Added FLAG_INSISTENT so the ringing sound repeats itself until an action is done.
Expand Down Expand Up @@ -255,7 +275,6 @@ class CallNotificationBuilder @Inject constructor(
)
)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setFullScreenIntent(openOngoingCallPendingIntent(context, conversationIdString), true)
.setContentIntent(openOngoingCallPendingIntent(context, conversationIdString))
.build()
}
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog
import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.LocalSyncStateObserver
import com.wire.android.util.SwitchAccountObserver
import com.wire.android.util.SyncStateObserver
import com.wire.android.util.debug.FeatureVisibilityFlags
import com.wire.android.util.debug.LocalFeatureVisibilityFlags
Expand All @@ -135,6 +136,9 @@ class WireActivity : AppCompatActivity() {
@Inject
lateinit var lockCodeTimeManager: Lazy<LockCodeTimeManager>

@Inject
lateinit var switchAccountObserver: SwitchAccountObserver

private val viewModel: WireActivityViewModel by viewModels()

private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels()
Expand Down Expand Up @@ -346,6 +350,19 @@ class WireActivity : AppCompatActivity() {
navigator.navController.removeOnDestinationChangedListener(currentScreenManager)
}
}

DisposableEffect(switchAccountObserver, navigator) {
NavigationSwitchAccountActions {
lifecycleScope.launch(Dispatchers.Main) {
navigator.navigate(it)
}
}.let {
switchAccountObserver.register(it)
onDispose {
switchAccountObserver.unregister(it)
}
}
}
}

@Composable
Expand Down
144 changes: 49 additions & 95 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ import com.wire.kalium.logic.feature.user.webSocketStatus.ObservePersistentWebSo
import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString
import dagger.Lazy
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
Expand Down Expand Up @@ -131,24 +130,19 @@ class WireActivityViewModel @Inject constructor(
private val _observeSyncFlowState: MutableStateFlow<SyncState?> = MutableStateFlow(null)
val observeSyncFlowState: StateFlow<SyncState?> = _observeSyncFlowState

private val userIdDeferred: Deferred<UserId?> = viewModelScope.async(dispatchers.io()) {
currentSessionFlow.get().invoke()
.distinctUntilChanged()
.map { result ->
if (result is CurrentSessionResult.Success) {
if (result.accountInfo.isValid()) {
result.accountInfo.userId
} else {
null
}
} else {
null
}
}
.distinctUntilChanged()
.flowOn(dispatchers.io())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1).first()
}
private val observeCurrentAccountInfo: SharedFlow<AccountInfo?> = currentSessionFlow.get().invoke()
.map { (it as? CurrentSessionResult.Success)?.accountInfo }
.distinctUntilChanged()
.flowOn(dispatchers.io())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)

private val observeCurrentValidUserId: SharedFlow<UserId?> = observeCurrentAccountInfo
.map {
if (it?.isValid() == true) it.userId else null
}
.distinctUntilChanged()
.flowOn(dispatchers.io())
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)

init {
observeSyncState()
Expand All @@ -159,21 +153,10 @@ class WireActivityViewModel @Inject constructor(
observeLogoutState()
}

@Suppress("TooGenericExceptionCaught")
private fun shouldEnrollToE2ei() = viewModelScope.async(dispatchers.io()) {
try {
val userId = userIdDeferred.await()
if (userId != null) {
observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(userId)
.observeIfE2EIIsRequiredDuringLogin().first() ?: false
} else {
false
}
} catch (e: NullPointerException) {
appLogger.e("Error while observing E2EI state: $e")
false
}
}
private suspend fun shouldEnrollToE2ei(): Boolean = observeCurrentValidUserId.first()?.let {
observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it)
.observeIfE2EIIsRequiredDuringLogin().first() ?: false
} ?: false

private fun observeAppThemeState() {
viewModelScope.launch(dispatchers.io()) {
Expand All @@ -185,33 +168,28 @@ class WireActivityViewModel @Inject constructor(
}
}

@Suppress("TooGenericExceptionCaught")
private fun observeSyncState() {
viewModelScope.launch(dispatchers.io()) {
try {
val userId = userIdDeferred.await()
if (userId != null) {
observeSyncStateUseCaseProviderFactory.create(userId).observeSyncState()
} else {
flowOf(null)
.distinctUntilChanged()
.collect { _observeSyncFlowState.emit(it) }
observeCurrentValidUserId
.flatMapLatest { userId ->
userId?.let {
observeSyncStateUseCaseProviderFactory.create(userId).observeSyncState()
} ?: flowOf(null)
}
.distinctUntilChanged()
.flowOn(dispatchers.io())
.collect {
_observeSyncFlowState.emit(it)
}
} catch (e: NullPointerException) {
appLogger.e("Error while observing sync state: $e")
}
}
}

private fun observeLogoutState() {
viewModelScope.launch(dispatchers.io()) {
currentSessionFlow.get().invoke()
.distinctUntilChanged()
observeCurrentAccountInfo
.collect {
if (it is CurrentSessionResult.Success) {
if (it.accountInfo.isValid().not()) {
handleInvalidSession((it.accountInfo as AccountInfo.Invalid).logoutReason)
}
if (it is AccountInfo.Invalid) {
handleInvalidSession(it.logoutReason)
}
}
}
Expand Down Expand Up @@ -244,43 +222,29 @@ class WireActivityViewModel @Inject constructor(
}
}

@Suppress("TooGenericExceptionCaught")
private fun observeScreenshotCensoringConfigState() {
viewModelScope.launch(dispatchers.io()) {
try {
val userId = userIdDeferred.await()
if (userId != null) {
observeScreenshotCensoringConfigUseCaseProviderFactory.create(userId)
.observeScreenshotCensoringConfig().collect { result ->
globalAppState = globalAppState.copy(
screenshotCensoringEnabled = result is ObserveScreenshotCensoringConfigResult.Enabled
)
}
} else {
globalAppState = globalAppState.copy(
screenshotCensoringEnabled = false
)
observeCurrentValidUserId
.flatMapLatest { currentValidUserId ->
currentValidUserId?.let {
observeScreenshotCensoringConfigUseCaseProviderFactory.create(it)
.observeScreenshotCensoringConfig()
.map { result ->
result is ObserveScreenshotCensoringConfigResult.Enabled
}
} ?: flowOf(false)
}
.collect {
globalAppState = globalAppState.copy(screenshotCensoringEnabled = it)
}
} catch (exception: NullPointerException) {
globalAppState = globalAppState.copy(
screenshotCensoringEnabled = false
)
}
}
}

suspend fun initialAppState(): InitialAppState {
val shouldMigrate = viewModelScope.async(dispatchers.io()) {
shouldMigrate()
}
val shouldLogin = viewModelScope.async(dispatchers.io()) {
shouldLogIn()
}
val shouldEnrollToE2ei = shouldEnrollToE2ei()
return when {
shouldMigrate.await() -> InitialAppState.NOT_MIGRATED
shouldLogin.await() -> InitialAppState.NOT_LOGGED_IN
shouldEnrollToE2ei.await() -> InitialAppState.ENROLL_E2EI
suspend fun initialAppState(): InitialAppState = withContext(dispatchers.io()) {
when {
shouldMigrate() -> InitialAppState.NOT_MIGRATED
shouldLogIn() -> InitialAppState.NOT_LOGGED_IN
shouldEnrollToE2ei() -> InitialAppState.ENROLL_E2EI
else -> InitialAppState.LOGGED_IN
}
}
Expand Down Expand Up @@ -517,17 +481,7 @@ class WireActivityViewModel @Inject constructor(
globalAppState = globalAppState.copy(conversationJoinedDialog = null)
}

private suspend fun shouldLogIn(): Boolean = !hasValidCurrentSession()

private suspend fun hasValidCurrentSession(): Boolean =
// TODO: the usage of currentSessionFlow is a temporary solution, it should be replaced with a proper solution
currentSessionFlow.get().invoke().first().let {
when (it) {
is CurrentSessionResult.Failure.Generic -> false
CurrentSessionResult.Failure.SessionNotFound -> false
is CurrentSessionResult.Success -> true
}
}
private suspend fun shouldLogIn(): Boolean = observeCurrentValidUserId.first() == null

private suspend fun shouldMigrate(): Boolean = migrationManager.get().shouldMigrate()

Expand Down
10 changes: 9 additions & 1 deletion app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ import android.os.Build
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.wire.android.util.SwitchAccountObserver
import androidx.lifecycle.lifecycleScope
import com.wire.android.ui.AppLockActivity
import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import javax.inject.Inject

@AndroidEntryPoint
abstract class CallActivity : AppCompatActivity() {

@Inject
lateinit var switchAccountObserver: SwitchAccountObserver

companion object {
const val EXTRA_CONVERSATION_ID = "conversation_id"
const val EXTRA_USER_ID = "user_id"
Expand All @@ -40,7 +48,7 @@ abstract class CallActivity : AppCompatActivity() {
fun switchAccountIfNeeded(userId: String?) {
userId?.let {
qualifiedIdMapper.fromStringToQualifiedID(it).run {
callActivityViewModel.switchAccountIfNeeded(this)
callActivityViewModel.switchAccountIfNeeded(userId = this, actions = switchAccountObserver)
}
}
}
Expand Down
Loading
Loading