From ff32e24a60d35036fa6d8b8de795c8e74046880e Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 9 Apr 2024 14:58:35 +0200 Subject: [PATCH 01/29] refactor: move calling feature a separate activity --- app/src/main/AndroidManifest.xml | 12 +- .../style/NavigationAnimationStyles.kt | 10 -- .../navigation/style/ScreenModeStyle.kt | 28 ---- .../notification/NotificationActions.kt | 2 +- .../android/notification/PendingIntents.kt | 35 ++--- .../notification/WireNotificationManager.kt | 1 - .../com/wire/android/ui/AppLockActivity.kt | 1 + .../com/wire/android/ui/WireActivity.kt | 31 +--- .../wire/android/ui/WireActivityViewModel.kt | 1 - .../wire/android/ui/calling/CallActivity.kt | 141 ++++++++++++++++++ .../com/wire/android/ui/calling/CallScreen.kt | 78 ++++++++++ .../wire/android/ui/calling/CallingNavArgs.kt | 24 --- .../ui/calling/SharedCallingViewModel.kt | 21 ++- .../ui/calling/incoming/IncomingCallScreen.kt | 89 +++++++---- .../calling/incoming/IncomingCallViewModel.kt | 81 ++++++---- .../initiating/InitiatingCallScreen.kt | 41 +++-- .../initiating/InitiatingCallViewModel.kt | 29 ++-- .../ui/calling/ongoing/OngoingCallScreen.kt | 37 ++--- .../calling/ongoing/OngoingCallViewModel.kt | 59 +++++--- .../topappbar/CommonTopAppBarViewModel.kt | 5 +- .../appLock/unlock/EnterLockCodeScreen.kt | 2 - .../home/conversations/ConversationScreen.kt | 59 ++++++-- .../conversationslist/ConversationRouter.kt | 11 +- .../wire/android/util/CurrentScreenManager.kt | 24 --- .../util/deeplink/DeepLinkProcessor.kt | 79 +++++----- .../android/util/ui/ScreenSettingsUtil.kt | 79 ---------- .../android/ui/WireActivityViewModelTest.kt | 29 ---- .../ui/calling/OngoingCallViewModelTest.kt | 4 +- .../ui/calling/SharedCallingViewModelTest.kt | 6 +- .../incoming/IncomingCallViewModelTest.kt | 16 +- .../initiating/InitiatingCallViewModelTest.kt | 76 +++++----- .../topappbar/CommonTopAppBarViewModelTest.kt | 3 +- .../android/util/DeepLinkProcessorTest.kt | 34 ----- gradle/libs.versions.toml | 4 +- kalium | 2 +- 35 files changed, 615 insertions(+), 539 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt delete mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt delete mode 100644 app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c04179745f5..d240aed87da 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,18 +85,26 @@ android:name=".ui.AppLockActivity" android:exported="true" android:hardwareAccelerated="true" - android:launchMode="singleTask" + android:launchMode="singleTop" android:screenOrientation="portrait" android:theme="@style/AppTheme" /> + + + diff --git a/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt b/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt index 5399d72263c..facbccec657 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/style/NavigationAnimationStyles.kt @@ -27,13 +27,3 @@ object SlideNavigationAnimation : WireDestinationStyleAnimated { object PopUpNavigationAnimation : WireDestinationStyleAnimated { override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP } - -object WakeUpScreenPopUpNavigationAnimation : WireDestinationStyleAnimated, ScreenModeStyle { - override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP - override fun screenMode(): ScreenMode = ScreenMode.WAKE_UP -} - -object KeepOnScreenPopUpNavigationAnimation : WireDestinationStyleAnimated, ScreenModeStyle { - override fun animationType(): TransitionAnimationType = TransitionAnimationType.POP_UP - override fun screenMode(): ScreenMode = ScreenMode.KEEP_ON -} diff --git a/app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt b/app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt deleted file mode 100644 index 048828392b7..00000000000 --- a/app/src/main/kotlin/com/wire/android/navigation/style/ScreenModeStyle.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.navigation.style - -interface ScreenModeStyle { - fun screenMode(): ScreenMode -} - -enum class ScreenMode { - KEEP_ON, // keep screen on while that NavigationItem is visible (i.e CallScreen) - WAKE_UP, // wake up the device on navigating to that NavigationItem (i.e IncomingCall) - NONE // do not wake up and allow device to sleep -} diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt index d5d4077830a..c187e94eeac 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt @@ -51,7 +51,7 @@ fun getActionReply( fun getOpenIncomingCallAction(context: Context, conversationId: String, userId: String) = getAction( context.getString(R.string.notification_action_open_call), - openIncomingCallPendingIntent(context, conversationId, userId) + fullScreenIncomingCallPendingIntent(context, conversationId, userId) ) fun getDeclineCallAction(context: Context, conversationId: String, userId: String) = getAction( diff --git a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt index 7c0c8e91130..fbeb7276856 100644 --- a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt +++ b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt @@ -28,6 +28,8 @@ import com.wire.android.notification.broadcastreceivers.CallNotificationDismissR import com.wire.android.notification.broadcastreceivers.EndOngoingCallReceiver import com.wire.android.notification.broadcastreceivers.NotificationReplyReceiver import com.wire.android.ui.WireActivity +import com.wire.android.ui.calling.CallActivity +import com.wire.android.ui.calling.CallScreenType import com.wire.android.util.deeplink.DeepLinkProcessor fun messagePendingIntent(context: Context, conversationId: String, userId: String?): PendingIntent { @@ -81,17 +83,6 @@ fun replyMessagePendingIntent(context: Context, conversationId: String, userId: PendingIntent.FLAG_MUTABLE ) -fun openIncomingCallPendingIntent(context: Context, conversationId: String, userId: String): PendingIntent { - val intent = openIncomingCallIntent(context, conversationId, userId) - - return PendingIntent.getActivity( - context.applicationContext, - OPEN_INCOMING_CALL_REQUEST_CODE, - intent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) -} - fun openOngoingCallPendingIntent(context: Context, conversationId: String): PendingIntent { val intent = openOngoingCallIntent(context, conversationId) @@ -132,27 +123,21 @@ fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String context, FULL_SCREEN_REQUEST_CODE, intent, - PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) } private fun openIncomingCallIntent(context: Context, conversationId: String, userId: String) = - Intent(context.applicationContext, WireActivity::class.java).apply { - data = Uri.Builder() - .scheme(DeepLinkProcessor.DEEP_LINK_SCHEME) - .authority(DeepLinkProcessor.INCOMING_CALL_DEEPLINK_HOST) - .appendPath(conversationId) - .appendQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM, userId) - .build() + Intent(context.applicationContext, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_USER_ID, userId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name) } private fun openOngoingCallIntent(context: Context, conversationId: String) = - Intent(context.applicationContext, WireActivity::class.java).apply { - data = Uri.Builder() - .scheme(DeepLinkProcessor.DEEP_LINK_SCHEME) - .authority(DeepLinkProcessor.ONGOING_CALL_DEEPLINK_HOST) - .appendPath(conversationId) - .build() + Intent(context.applicationContext, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name) } private fun openMigrationLoginIntent(context: Context, userHandle: String) = diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index 7bf369a96ff..69f29465d36 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -305,7 +305,6 @@ class WireNotificationManager @Inject constructor( when (screens) { is CurrentScreen.Conversation -> messagesNotificationManager.hideNotification(screens.id, userId) is CurrentScreen.OtherUserProfile -> messagesNotificationManager.hideNotification(screens.id, userId) - is CurrentScreen.IncomingCallScreen -> callNotificationManager.hideIncomingCallNotification() else -> {} } } diff --git a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt index 47769edce5f..7add69f70fb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt @@ -74,6 +74,7 @@ class AppLockActivity : AppCompatActivity() { } } } + companion object { const val SET_TEAM_APP_LOCK = "set_team_app_lock" } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index a8fe5eba5c6..150135973e8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -63,6 +63,7 @@ import com.wire.android.navigation.NavigationGraph import com.wire.android.navigation.navigateToItem import com.wire.android.navigation.rememberNavigator import com.wire.android.ui.calling.ProximitySensorManager +import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel @@ -72,10 +73,8 @@ import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination -import com.wire.android.ui.destinations.IncomingCallScreenDestination import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination -import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination @@ -103,7 +102,6 @@ import com.wire.android.util.SyncStateObserver import com.wire.android.util.debug.FeatureVisibilityFlags import com.wire.android.util.debug.LocalFeatureVisibilityFlags import com.wire.android.util.deeplink.DeepLinkResult -import com.wire.android.util.ui.updateScreenSettings import dagger.Lazy import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers @@ -122,9 +120,6 @@ class WireActivity : AppCompatActivity() { @Inject lateinit var currentScreenManager: CurrentScreenManager - @Inject - lateinit var proximitySensorManager: ProximitySensorManager - @Inject lateinit var lockCodeTimeManager: Lazy @@ -154,9 +149,6 @@ class WireActivity : AppCompatActivity() { lifecycle.addObserver(currentScreenManager) WindowCompat.setDecorFitsSystemWindows(window, false) - appLogger.i("$TAG proximity sensor") - proximitySensorManager.initialize() - lifecycleScope.launch(Dispatchers.Default) { appLogger.i("$TAG persistent connection status") @@ -219,7 +211,9 @@ class WireActivity : AppCompatActivity() { CommonTopAppBar( commonTopAppBarState = commonTopAppBarViewModel.state, onReturnToCallClick = { establishedCall -> - navigator.navigate(NavigationCommand(OngoingCallScreenDestination(establishedCall.conversationId))) + getOngoingCallIntent(this@WireActivity, establishedCall.conversationId.toString()).run { + startActivity(this) + } }, ) CompositionLocalProvider(LocalNavigator provides navigator) { @@ -263,7 +257,6 @@ class WireActivity : AppCompatActivity() { DisposableEffect(navController) { val updateScreenSettingsListener = NavController.OnDestinationChangedListener { _, navDestination, _ -> currentKeyboardController?.hide() - updateScreenSettings(navDestination) } navController.addOnDestinationChangedListener(updateScreenSettingsListener) navController.addOnDestinationChangedListener(currentScreenManager) @@ -480,13 +473,6 @@ class WireActivity : AppCompatActivity() { } } } - - proximitySensorManager.registerListener() - } - - override fun onPause() { - super.onPause() - proximitySensorManager.unRegisterListener() } override fun onSaveInstanceState(outState: Bundle) { @@ -532,15 +518,6 @@ class WireActivity : AppCompatActivity() { // do nothing, already handled in ViewModel } - is DeepLinkResult.IncomingCall -> { - if (result.switchedAccount) navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE)) - navigate(NavigationCommand(IncomingCallScreenDestination(result.conversationsId))) - } - - is DeepLinkResult.OngoingCall -> { - navigate(NavigationCommand(OngoingCallScreenDestination(result.conversationsId))) - } - is DeepLinkResult.OpenConversation -> { if (result.switchedAccount) navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE)) navigate(NavigationCommand(ConversationScreenDestination(result.conversationsId), BackStackMode.UPDATE_EXISTED)) diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 12ba8586963..79ae1d5e2fc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -486,7 +486,6 @@ class WireActivityViewModel @Inject constructor( CurrentScreen.InBackground, is CurrentScreen.Conversation, CurrentScreen.Home, - is CurrentScreen.CallScreen, is CurrentScreen.OtherUserProfile, CurrentScreen.AuthRelated, CurrentScreen.SomeOther -> true diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt new file mode 100644 index 00000000000..8eb46928fc9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -0,0 +1,141 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.calling + +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.WindowManager +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.core.view.WindowCompat +import com.wire.android.appLogger +import com.wire.android.notification.CallNotificationManager +import com.wire.android.ui.LocalActivity +import com.wire.android.ui.common.snackbar.LocalSnackbarHostState +import com.wire.android.ui.theme.WireTheme +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class CallActivity : AppCompatActivity() { + + @Inject + lateinit var callNotificationManager: CallNotificationManager + + @Inject + lateinit var proximitySensorManager: ProximitySensorManager + + private val qualifiedIdMapper = QualifiedIdMapperImpl(null) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + callNotificationManager.hideAllNotifications() + + appLogger.i("${TAG} Initializing proximity sensor..") + proximitySensorManager.initialize() + + WindowCompat.setDecorFitsSystemWindows(window, false) + setUpCallingFlags() + + val conversationId = intent.extras?.getString(EXTRA_CONVERSATION_ID) + val screenType = intent.extras?.getString(EXTRA_SCREEN_TYPE) + + setContent { + val snackbarHostState = remember { SnackbarHostState() } + CompositionLocalProvider( + LocalSnackbarHostState provides snackbarHostState, + LocalActivity provides this + ) { + WireTheme { + conversationId?.let { + screenType?.let { screenType -> + val startDestination = CallScreenType.valueOf(screenType) + CallScreen( + conversationId = qualifiedIdMapper.fromStringToQualifiedID(it), + startDestination = startDestination + ) + } + } + } + } + } + } + + override fun onResume() { + super.onResume() + proximitySensorManager.registerListener() + } + + override fun onPause() { + super.onPause() + proximitySensorManager.unRegisterListener() + } + + companion object { + private const val TAG = "CallActivity" + const val EXTRA_CONVERSATION_ID = "conversation_id" + const val EXTRA_USER_ID = "user_id" + const val EXTRA_SCREEN_TYPE = "screen_type" + } +} + +fun CallActivity.setUpCallingFlags() { + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + setShowWhenLocked(true) + setTurnScreenOn(true) + } else { + window.addFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + } +} + +fun getOngoingCallIntent( + activity: Activity, + conversationId: String +) = Intent(activity, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name) +} + +fun getIncomingCallIntent( + activity: Activity, + conversationId: String +) = Intent(activity, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name) +} + +fun getInitiatingCallIntent( + activity: Activity, + conversationId: String +) = Intent(activity, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Initiating.name) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt new file mode 100644 index 00000000000..5d9f46f593b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt @@ -0,0 +1,78 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.calling + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.wire.android.ui.calling.incoming.IncomingCallScreen +import com.wire.android.ui.calling.initiating.InitiatingCallScreen +import com.wire.android.ui.calling.ongoing.OngoingCallScreen +import com.wire.kalium.logic.data.id.ConversationId + +enum class CallScreenType { + Incoming, + Ongoing, + Initiating +} + +@Composable +fun CallScreen( + conversationId: ConversationId, + startDestination: CallScreenType, + navController: NavHostController = rememberNavController() +) { + + Scaffold { innerPadding -> + NavHost( + navController = navController, + startDestination = "${startDestination.name}/$conversationId", + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + composable( + route = "${CallScreenType.Incoming.name}/{conversationId}", + arguments = listOf(navArgument("conversationId") { type = NavType.StringType }) + ) { + IncomingCallScreen( + conversationId = conversationId, + onCallAccepted = { + navController.navigate("${CallScreenType.Ongoing.name}/${conversationId}") + } + ) + } + composable(route = "${CallScreenType.Ongoing.name}/{conversationId}") { + OngoingCallScreen(conversationId) + } + composable(route = "${CallScreenType.Initiating.name}/{conversationId}") { + InitiatingCallScreen(conversationId) { + navController.navigate("${CallScreenType.Ongoing.name}/${conversationId}") + } + } + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt deleted file mode 100644 index c8892b46466..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallingNavArgs.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.ui.calling - -import com.wire.kalium.logic.data.id.ConversationId - -data class CallingNavArgs( - val conversationId: ConversationId -) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt index 26d101a46e1..fb8ef346e35 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt @@ -30,7 +30,6 @@ import com.wire.android.mapper.UICallParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.media.CallRinger import com.wire.android.model.ImageAsset -import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.android.util.dispatchers.DispatcherProvider @@ -41,7 +40,8 @@ import com.wire.kalium.logic.data.call.ConversationType import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails -import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase @@ -55,6 +55,9 @@ import com.wire.kalium.logic.feature.call.usecase.UnMuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase import com.wire.kalium.logic.util.PlatformView +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharedFlow @@ -69,9 +72,9 @@ import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") -@HiltViewModel -class SharedCallingViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = SharedCallingViewModel.Factory::class) +class SharedCallingViewModel @AssistedInject constructor( + @Assisted val conversationId: ConversationId, private val conversationDetails: ObserveConversationDetailsUseCase, private val allCalls: GetAllCallsWithSortedParticipantsUseCase, private val endCall: EndCallUseCase, @@ -92,9 +95,6 @@ class SharedCallingViewModel @Inject constructor( private val dispatchers: DispatcherProvider ) : ViewModel() { - private val callingNavArgs: CallingNavArgs = savedStateHandle.navArgs() - val conversationId: QualifiedID = callingNavArgs.conversationId - var callState by mutableStateOf(CallState(conversationId)) init { @@ -302,4 +302,9 @@ class SharedCallingViewModel @Inject constructor( setVideoPreview(conversationId, PlatformView(view)) } } + + @AssistedFactory + interface Factory { + fun create(conversationId: ConversationId): SharedCallingViewModel + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index b222ffe6876..ad50c182a11 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.calling.incoming +import android.content.Intent import android.view.View import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box @@ -36,16 +37,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.navigation.BackStackMode -import com.wire.android.navigation.NavigationCommand -import com.wire.android.navigation.Navigator -import com.wire.android.navigation.style.WakeUpScreenPopUpNavigationAnimation +import com.wire.android.ui.AppLockActivity +import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.CallState -import com.wire.android.ui.calling.CallingNavArgs import com.wire.android.ui.calling.SharedCallingViewModel import com.wire.android.ui.calling.common.CallVideoPreview import com.wire.android.ui.calling.common.CallerDetails @@ -58,7 +54,6 @@ import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog import com.wire.android.ui.common.dialogs.calling.JoinAnywayDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.theme.wireTypography import com.wire.android.util.permission.PermissionDenialType @@ -66,21 +61,32 @@ import com.wire.android.util.permission.rememberCallingRecordAudioRequestFlow import com.wire.kalium.logic.data.call.ConversationType import com.wire.kalium.logic.data.id.ConversationId -@RootNavGraph -@Destination( - navArgsDelegate = CallingNavArgs::class, - style = WakeUpScreenPopUpNavigationAnimation::class -) @Composable fun IncomingCallScreen( - navigator: Navigator, - sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(), - incomingCallViewModel: IncomingCallViewModel = hiltViewModel() + conversationId: ConversationId, + incomingCallViewModel: IncomingCallViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ), + sharedCallingViewModel: SharedCallingViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ), + onCallAccepted: () -> Unit ) { - val permissionPermanentlyDeniedDialogState = rememberVisibilityState() + val activity = LocalActivity.current + + val permissionPermanentlyDeniedDialogState = + rememberVisibilityState() val audioPermissionCheck = AudioPermissionCheckFlow( - onAcceptCall = incomingCallViewModel::acceptCall, + onAcceptCall = { + incomingCallViewModel.acceptCall { + Intent(activity, AppLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + }.run { + activity.startActivity(this) + } + } + }, onPermanentPermissionDecline = { permissionPermanentlyDeniedDialogState.show( PermissionPermanentlyDeniedDialogState.Visible( @@ -95,19 +101,27 @@ fun IncomingCallScreen( if (incomingCallState.shouldShowJoinCallAnywayDialog) { JoinAnywayDialog( onDismiss = ::dismissJoinCallAnywayDialog, - onConfirm = ::acceptCallAnyway + onConfirm = { + acceptCallAnyway { + Intent(activity, AppLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + }.run { + activity.startActivity(this) + } + } + } ) } } LaunchedEffect(incomingCallViewModel.incomingCallState.flowState) { - when (val flowState = incomingCallViewModel.incomingCallState.flowState) { - is IncomingCallState.FlowState.CallClosed -> navigator.navigateBack() - is IncomingCallState.FlowState.CallAccepted -> navigator.navigate( - NavigationCommand( - OngoingCallScreenDestination(flowState.conversationId), - BackStackMode.REMOVE_CURRENT_AND_REPLACE - ) - ) + when (incomingCallViewModel.incomingCallState.flowState) { + is IncomingCallState.FlowState.CallClosed -> { + activity.finish() + } + + is IncomingCallState.FlowState.CallAccepted -> { + onCallAccepted() + } is IncomingCallState.FlowState.Default -> { /* do nothing */ } @@ -119,7 +133,10 @@ fun IncomingCallScreen( toggleMute = { sharedCallingViewModel.toggleMute(true) }, toggleSpeaker = ::toggleSpeaker, toggleVideo = ::toggleVideo, - declineCall = incomingCallViewModel::declineCall, + declineCall = { + incomingCallViewModel::declineCall + activity.finish() + }, acceptCall = audioPermissionCheck::launch, onVideoPreviewCreated = ::setVideoPreview, onSelfClearVideoPreview = ::clearVideoPreview, @@ -250,6 +267,10 @@ private fun IncomingCallContent( } } +fun launchLockScreenForIncomingCallScreen() { + appLogger.d("IncomingCall - App locked") +} + @Composable fun AudioPermissionCheckFlow( onAcceptCall: () -> Unit, @@ -266,5 +287,15 @@ fun AudioPermissionCheckFlow( @Preview @Composable fun PreviewIncomingCallScreen() { - IncomingCallContent(CallState(ConversationId("value", "domain")), {}, {}, {}, {}, {}, {}, {}, {}) + IncomingCallContent( + callState = CallState(ConversationId("value", "domain")), + toggleMute = { }, + toggleSpeaker = { }, + toggleVideo = { }, + declineCall = { }, + acceptCall = { }, + onVideoPreviewCreated = { }, + onSelfClearVideoPreview = { }, + onPermissionPermanentlyDenied = { }, + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt index ddf6c5b3e8f..69392400f5b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt @@ -21,32 +21,32 @@ package com.wire.android.ui.calling.incoming import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.media.CallRinger -import com.wire.android.ui.calling.CallingNavArgs -import com.wire.android.ui.navArgs +import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.feature.call.usecase.AnswerCallUseCase import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RejectCallUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class IncomingCallViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = IncomingCallViewModel.Factory::class) +class IncomingCallViewModel @AssistedInject constructor( + @Assisted val conversationId: ConversationId, private val incomingCalls: GetIncomingCallsUseCase, private val rejectCall: RejectCallUseCase, private val acceptCall: AnswerCallUseCase, @@ -54,11 +54,9 @@ class IncomingCallViewModel @Inject constructor( private val muteCall: MuteCallUseCase, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val endCall: EndCallUseCase, + private val lockCodeTimeManager: LockCodeTimeManager ) : ViewModel() { - private val incomingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs() - private val conversationId: QualifiedID = incomingCallNavArgs.conversationId - private lateinit var observeIncomingCallJob: Job private var establishedCallConversationId: ConversationId? = null @@ -94,7 +92,8 @@ class IncomingCallViewModel @Inject constructor( calls.find { call -> call.conversationId == conversationId }.also { if (it == null) { callRinger.stop() - incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) + incomingCallState = + incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) } } } @@ -106,7 +105,8 @@ class IncomingCallViewModel @Inject constructor( launch { rejectCall(conversationId = conversationId) } launch { callRinger.stop() - incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) + incomingCallState = + incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) } } } @@ -119,30 +119,44 @@ class IncomingCallViewModel @Inject constructor( incomingCallState = incomingCallState.copy(shouldShowJoinCallAnywayDialog = false) } - fun acceptCallAnyway() { + fun acceptCallAnyway(onAppLocked: () -> Unit) { viewModelScope.launch { - establishedCallConversationId?.let { - endCall(it) - // we need to update mute state to false, so if the user re-join the call te mic will will be muted - muteCall(it, false) - delay(DELAY_END_CALL) + lockCodeTimeManager.observeAppLock().first().let { + if (it) { + onAppLocked() + } else { + establishedCallConversationId?.let { + endCall(it) + // we need to update mute state to false, so if the user re-join the call te mic will will be muted + muteCall(it, false) + delay(DELAY_END_CALL) + } + acceptCall(onAppLocked) + } } - acceptCall() } } - fun acceptCall() { + fun acceptCall(onAppLocked: () -> Unit) { viewModelScope.launch { - if (incomingCallState.hasEstablishedCall) { - showJoinCallAnywayDialog() - } else { - callRinger.stop() - - dismissJoinCallAnywayDialog() - observeIncomingCallJob.cancel() - - acceptCall(conversationId = conversationId) - incomingCallState = incomingCallState.copy(flowState = IncomingCallState.FlowState.CallAccepted(conversationId)) + lockCodeTimeManager.observeAppLock().first().let { + if (it) { + onAppLocked() + } else { + if (incomingCallState.hasEstablishedCall) { + showJoinCallAnywayDialog() + } else { + callRinger.stop() + + dismissJoinCallAnywayDialog() + observeIncomingCallJob.cancel() + + acceptCall(conversationId = conversationId) + incomingCallState = incomingCallState.copy( + flowState = IncomingCallState.FlowState.CallAccepted(conversationId) + ) + } + } } } } @@ -150,4 +164,9 @@ class IncomingCallViewModel @Inject constructor( companion object { const val DELAY_END_CALL = 200L } + + @AssistedFactory + interface Factory { + fun create(conversationId: ConversationId): IncomingCallViewModel + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt index 7d6440df304..783706a69ad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt @@ -38,15 +38,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R -import com.wire.android.navigation.BackStackMode -import com.wire.android.navigation.style.KeepOnScreenPopUpNavigationAnimation -import com.wire.android.navigation.NavigationCommand -import com.wire.android.navigation.Navigator +import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.CallState -import com.wire.android.ui.calling.CallingNavArgs import com.wire.android.ui.calling.SharedCallingViewModel import com.wire.android.ui.calling.common.CallVideoPreview import com.wire.android.ui.calling.common.CallerDetails @@ -56,31 +50,36 @@ import com.wire.android.ui.common.bottomsheet.WireBottomSheetScaffold import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.permission.PermissionDenialType import com.wire.kalium.logic.data.id.ConversationId -@RootNavGraph -@Destination( - navArgsDelegate = CallingNavArgs::class, - style = KeepOnScreenPopUpNavigationAnimation::class -) @Composable fun InitiatingCallScreen( - navigator: Navigator, - navArgs: CallingNavArgs, - sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(), - initiatingCallViewModel: InitiatingCallViewModel = hiltViewModel() + conversationId: ConversationId, + sharedCallingViewModel: SharedCallingViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ), + initiatingCallViewModel: InitiatingCallViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ), + onCallAccepted: () -> Unit ) { - val permissionPermanentlyDeniedDialogState = rememberVisibilityState() + val permissionPermanentlyDeniedDialogState = + rememberVisibilityState() + + val activity = LocalActivity.current LaunchedEffect(initiatingCallViewModel.state.flowState) { when (initiatingCallViewModel.state.flowState) { - InitiatingCallState.FlowState.CallClosed -> navigator.navigateBack() - InitiatingCallState.FlowState.CallEstablished -> - navigator.navigate(NavigationCommand(OngoingCallScreenDestination(navArgs.conversationId), BackStackMode.REMOVE_CURRENT)) + InitiatingCallState.FlowState.CallClosed -> { + activity.finish() + } + + InitiatingCallState.FlowState.CallEstablished -> { + onCallAccepted() + } InitiatingCallState.FlowState.Default -> { /* do nothing */ } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt index f57349cbb4e..45f5a109c30 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt @@ -21,29 +21,28 @@ package com.wire.android.ui.calling.initiating import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.media.CallRinger -import com.wire.android.ui.calling.CallingNavArgs -import com.wire.android.ui.navArgs -import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import java.util.Calendar -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class InitiatingCallViewModel @Inject constructor( - savedStateHandle: SavedStateHandle, +@HiltViewModel(assistedFactory = InitiatingCallViewModel.Factory::class) +class InitiatingCallViewModel @AssistedInject constructor( + @Assisted val conversationId: ConversationId, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, private val startCall: StartCallUseCase, private val endCall: EndCallUseCase, @@ -51,9 +50,6 @@ class InitiatingCallViewModel @Inject constructor( private val callRinger: CallRinger ) : ViewModel() { - private val initiatingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs() - private val conversationId: QualifiedID = initiatingCallNavArgs.conversationId - private val callStartTime: Long = Calendar.getInstance().timeInMillis private var wasCallHangUp: Boolean = false @@ -108,7 +104,11 @@ class InitiatingCallViewModel @Inject constructor( conversationId = conversationId ) when (result) { - StartCallUseCase.Result.Success -> callRinger.ring(resource = R.raw.ringing_from_me, isIncomingCall = false) + StartCallUseCase.Result.Success -> callRinger.ring( + resource = R.raw.ringing_from_me, + isIncomingCall = false + ) + StartCallUseCase.Result.SyncFailure -> {} // TODO: handle case where start call fails } } @@ -120,4 +120,9 @@ class InitiatingCallViewModel @Inject constructor( state = state.copy(flowState = InitiatingCallState.FlowState.CallClosed) } } + + @AssistedFactory + interface Factory { + fun create(conversationId: ConversationId): InitiatingCallViewModel + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index 9b7ad29ce64..b98130484a5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -51,12 +51,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R -import com.wire.android.navigation.Navigator -import com.wire.android.navigation.style.WakeUpScreenPopUpNavigationAnimation -import com.wire.android.ui.calling.CallingNavArgs +import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.ConversationName import com.wire.android.ui.calling.SharedCallingViewModel import com.wire.android.ui.calling.controlbuttons.CameraButton @@ -89,27 +85,32 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import java.util.Locale -@RootNavGraph -@Destination( - navArgsDelegate = CallingNavArgs::class, - style = WakeUpScreenPopUpNavigationAnimation::class -) @Composable fun OngoingCallScreen( - navigator: Navigator, - ongoingCallViewModel: OngoingCallViewModel = hiltViewModel(), - sharedCallingViewModel: SharedCallingViewModel = hiltViewModel(), + conversationId: ConversationId, + ongoingCallViewModel: OngoingCallViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ), + sharedCallingViewModel: SharedCallingViewModel = hiltViewModel( + creationCallback = { factory -> factory.create(conversationId = conversationId) } + ) ) { val permissionPermanentlyDeniedDialogState = rememberVisibilityState() + val activity = LocalActivity.current + LaunchedEffect(ongoingCallViewModel.state.flowState) { when (ongoingCallViewModel.state.flowState) { - OngoingCallState.FlowState.CallClosed -> navigator.navigateBack() + OngoingCallState.FlowState.CallClosed -> { + activity.finish() + } + OngoingCallState.FlowState.Default -> { /* do nothing */ } } } + with(sharedCallingViewModel.callState) { OngoingCallContent( conversationId = conversationId, @@ -126,7 +127,7 @@ fun OngoingCallScreen( shouldShowDoubleTapToast = ongoingCallViewModel.shouldShowDoubleTapToast, toggleSpeaker = sharedCallingViewModel::toggleSpeaker, toggleMute = sharedCallingViewModel::toggleMute, - hangUpCall = { sharedCallingViewModel.hangUpCall(navigator::navigateBack) }, + hangUpCall = { sharedCallingViewModel.hangUpCall { activity.finish() } }, toggleVideo = sharedCallingViewModel::toggleVideo, flipCamera = sharedCallingViewModel::flipCamera, setVideoPreview = { @@ -137,7 +138,7 @@ fun OngoingCallScreen( sharedCallingViewModel.clearVideoPreview() ongoingCallViewModel.stopSendingVideoFeed() }, - navigateBack = navigator::navigateBack, + navigateBack = { activity.finish() }, requestVideoStreams = ongoingCallViewModel::requestVideoStreams, hideDoubleTapToast = ongoingCallViewModel::hideDoubleTapToast, onPermissionPermanentlyDenied = { @@ -151,7 +152,9 @@ fun OngoingCallScreen( } } ) - BackHandler(enabled = isCameraOn, navigator::navigateBack) + BackHandler(enabled = isCameraOn) { + activity.finish() + } } PermissionPermanentlyDeniedDialog( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt index 9b3abb360b7..714c5e8196d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt @@ -28,30 +28,33 @@ import androidx.lifecycle.viewModelScope import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.CurrentAccount -import com.wire.android.ui.calling.CallingNavArgs import com.wire.android.ui.calling.model.UICallParticipant -import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallClient import com.wire.kalium.logic.data.call.VideoState -import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList") -@HiltViewModel -class OngoingCallViewModel @Inject constructor( +@HiltViewModel(assistedFactory = OngoingCallViewModel.Factory::class) +class OngoingCallViewModel @AssistedInject constructor( savedStateHandle: SavedStateHandle, + @Assisted + val conversationIdInjected: ConversationId, @CurrentAccount private val currentUserId: UserId, private val globalDataStore: GlobalDataStore, @@ -60,9 +63,12 @@ class OngoingCallViewModel @Inject constructor( private val setVideoSendState: SetVideoSendStateUseCase, private val currentScreenManager: CurrentScreenManager ) : ViewModel() { + private val qualifiedIdMapper = QualifiedIdMapperImpl(null) - private val ongoingCallNavArgs: CallingNavArgs = savedStateHandle.navArgs() - private val conversationId: QualifiedID = ongoingCallNavArgs.conversationId + private val conversationIdString: String? = savedStateHandle["conversationId"] + private val conversationId = + conversationIdString?.let { qualifiedIdMapper.fromStringToQualifiedID(it) } + ?: conversationIdInjected var shouldShowDoubleTapToast: Boolean by mutableStateOf(false) private set @@ -98,6 +104,7 @@ class OngoingCallViewModel @Inject constructor( setVideoSendState(conversationId, VideoState.STARTED) } } + fun stopSendingVideoFeed() { viewModelScope.launch { setVideoSendState(conversationId, VideoState.STOPPED) @@ -109,10 +116,10 @@ class OngoingCallViewModel @Inject constructor( .distinctUntilChanged() .collect { calls -> val currentCall = calls.find { call -> call.conversationId == conversationId } - val currentScreen = currentScreenManager.observeCurrentScreen(viewModelScope).first() - val isCurrentlyOnOngoingScreen = currentScreen is CurrentScreen.OngoingCallScreen + val currentScreen = + currentScreenManager.observeCurrentScreen(viewModelScope).first() val isOnBackground = currentScreen is CurrentScreen.InBackground - if (currentCall == null && (isCurrentlyOnOngoingScreen || isOnBackground)) { + if (currentCall == null && isOnBackground) { state = state.copy(flowState = OngoingCallState.FlowState.CallClosed) } } @@ -137,25 +144,30 @@ class OngoingCallViewModel @Inject constructor( private fun startDoubleTapToastDisplayCountDown() { doubleTapIndicatorCountDownTimer?.cancel() - doubleTapIndicatorCountDownTimer = object : CountDownTimer(DOUBLE_TAP_TOAST_DISPLAY_TIME, COUNT_DOWN_INTERVAL) { - override fun onTick(p0: Long) { - appLogger.i("startDoubleTapToastDisplayCountDown: $p0") - } + doubleTapIndicatorCountDownTimer = + object : CountDownTimer(DOUBLE_TAP_TOAST_DISPLAY_TIME, COUNT_DOWN_INTERVAL) { + override fun onTick(p0: Long) { + appLogger.i("startDoubleTapToastDisplayCountDown: $p0") + } - override fun onFinish() { - shouldShowDoubleTapToast = false - viewModelScope.launch { - globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false) + override fun onFinish() { + shouldShowDoubleTapToast = false + viewModelScope.launch { + globalDataStore.setShouldShowDoubleTapToastStatus( + currentUserId.toString(), + false + ) + } } } - } doubleTapIndicatorCountDownTimer?.start() } private fun showDoubleTapToast() { viewModelScope.launch { delay(DELAY_TO_SHOW_DOUBLE_TAP_TOAST) - shouldShowDoubleTapToast = globalDataStore.getShouldShowDoubleTapToast(currentUserId.toString()) + shouldShowDoubleTapToast = + globalDataStore.getShouldShowDoubleTapToast(currentUserId.toString()) if (shouldShowDoubleTapToast) { startDoubleTapToastDisplayCountDown() } @@ -174,4 +186,9 @@ class OngoingCallViewModel @Inject constructor( const val COUNT_DOWN_INTERVAL = 1000L const val DELAY_TO_SHOW_DOUBLE_TAP_TOAST = 500L } + + @AssistedFactory + interface Factory { + fun create(conversationId: ConversationId): OngoingCallViewModel + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index 1747775931b..0e5f5b0123a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -32,7 +32,6 @@ import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -44,7 +43,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject -@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class CommonTopAppBarViewModel @Inject constructor( private val currentScreenManager: CurrentScreenManager, @@ -114,11 +112,10 @@ class CommonTopAppBarViewModel @Inject constructor( connectivity: Connectivity, activeCall: Call? ): ConnectivityUIState { - val canDisplayActiveCall = currentScreen !is CurrentScreen.OngoingCallScreen val canDisplayConnectivityIssues = currentScreen !is CurrentScreen.AuthRelated - if (activeCall != null && canDisplayActiveCall) { + if (activeCall != null) { return ConnectivityUIState.EstablishedCall(activeCall.conversationId, activeCall.isMuted) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt index 70667c36575..6a56a3707a9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/unlock/EnterLockCodeScreen.kt @@ -86,8 +86,6 @@ fun EnterLockCodeScreen( BackHandler { if (navigator.navController.previousBackStackEntry?.destination() is AppUnlockWithBiometricsScreenDestination) { navigator.navigateBack() - } else { - navigator.finish() } } LaunchedEffect(viewModel.state.done) { diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index bfe8941993d..6b4dc21a997 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -84,6 +84,11 @@ import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator +import com.wire.android.ui.LocalActivity +import com.wire.android.ui.calling.CallActivity +import com.wire.android.ui.calling.CallScreenType +import com.wire.android.ui.calling.getInitiatingCallIntent +import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.colorsScheme @@ -103,10 +108,10 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.GroupConversationDetailsScreenDestination -import com.wire.android.ui.destinations.InitiatingCallScreenDestination +//import com.wire.android.ui.destinations.InitiatingCallScreenDestination import com.wire.android.ui.destinations.MediaGalleryScreenDestination import com.wire.android.ui.destinations.MessageDetailsScreenDestination -import com.wire.android.ui.destinations.OngoingCallScreenDestination +//import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.home.conversations.AuthorHeaderHelper.rememberShouldHaveSmallBottomPadding @@ -217,6 +222,8 @@ fun ConversationScreen( // then ViewModel also detects it's removed and calls onNotFound which can execute navigateBack again and close the app var alreadyDeletedByUser by rememberSaveable { mutableStateOf(false) } + val activity = LocalActivity.current + LaunchedEffect(alreadyDeletedByUser) { if (!alreadyDeletedByUser) { conversationInfoViewModel.observeConversationDetails(navigator::navigateBack) @@ -248,7 +255,13 @@ fun ConversationScreen( appLogger.i("showing showJoinAnywayDialog..") JoinAnywayDialog( onDismiss = ::dismissJoinCallAnywayDialog, - onConfirm = { joinAnyway { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } } + onConfirm = { + joinAnyway { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } + } ) } } @@ -257,7 +270,9 @@ fun ConversationScreen( ConversationScreenDialogType.ONGOING_ACTIVE_CALL -> { OngoingActiveCallDialog(onJoinAnyways = { conversationCallViewModel.endEstablishedCallIfAny { - navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(conversationCallViewModel.conversationId))) + getInitiatingCallIntent(activity, conversationCallViewModel.conversationId.toString()).run { + activity.startActivity(this) + } } showDialog.value = ConversationScreenDialogType.NONE }, onDialogDismiss = { @@ -281,9 +296,15 @@ fun ConversationScreen( coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, onOpenInitiatingCallScreen = { - navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it))) + getInitiatingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } } - ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } + ) { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } }, onDialogDismiss = { showDialog.value = ConversationScreenDialogType.NONE @@ -307,9 +328,15 @@ fun ConversationScreen( coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, onOpenInitiatingCallScreen = { - navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it))) + getInitiatingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } + ) { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) } - ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } + } }, onDialogDismiss = { showDialog.value = ConversationScreenDialogType.NONE } ) @@ -364,12 +391,22 @@ fun ConversationScreen( coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, onOpenInitiatingCallScreen = { - navigator.navigate(NavigationCommand(InitiatingCallScreenDestination(it))) + getInitiatingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } } - ) { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } + ) { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } }, onJoinCall = { - conversationCallViewModel.joinOngoingCall { navigator.navigate(NavigationCommand(OngoingCallScreenDestination(it))) } + conversationCallViewModel.joinOngoingCall { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } }, onReactionClick = { messageId, emoji -> conversationMessagesViewModel.toggleReaction(messageId, emoji) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt index 668e09e7cc3..8489c7e8c36 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversationslist/ConversationRouter.kt @@ -30,6 +30,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator +import com.wire.android.ui.LocalActivity +import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.bottomsheet.conversation.ConversationOptionNavigation import com.wire.android.ui.common.bottomsheet.conversation.ConversationSheetContent import com.wire.android.ui.common.bottomsheet.conversation.rememberConversationSheetState @@ -44,7 +46,6 @@ import com.wire.android.ui.common.visbility.VisibilityState import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.ConversationScreenDestination import com.wire.android.ui.destinations.NewConversationSearchPeopleScreenDestination -import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.home.HomeSnackbarState import com.wire.android.ui.home.conversations.PermissionPermanentlyDeniedDialogState @@ -86,6 +87,8 @@ fun ConversationRouterHomeBridge( val viewModel: ConversationListViewModel = hiltViewModel() + val activity = LocalActivity.current + LaunchedEffect(conversationsSource) { viewModel.updateConversationsSource(conversationsSource) } @@ -198,7 +201,11 @@ fun ConversationRouterHomeBridge( { userId -> navigator.navigate(NavigationCommand(OtherUserProfileScreenDestination(userId))) } } val onJoinedCall: (ConversationId) -> Unit = remember(navigator) { - { conversationId -> navigator.navigate(NavigationCommand(OngoingCallScreenDestination(conversationId))) } + { + getOngoingCallIntent(activity, it.toString()).run { + activity.startActivity(this) + } + } } with(viewModel.conversationListState) { diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt index ba44de5a9e1..a2d69dc3e99 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -38,19 +38,15 @@ import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination -import com.wire.android.ui.destinations.IncomingCallScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination -import com.wire.android.ui.destinations.InitiatingCallScreenDestination import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination -import com.wire.android.ui.destinations.OngoingCallScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.RegisterDeviceScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.QualifiedID import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -153,17 +149,6 @@ sealed class CurrentScreen { // Another User Profile Screen is opened data class OtherUserProfile(val id: ConversationId) : CurrentScreen() - sealed class CallScreen(open val id: QualifiedID) : CurrentScreen() - - // Ongoing call screen is opened - class OngoingCallScreen(override val id: QualifiedID) : CallScreen(id) - - // Incoming call screen is opened - class IncomingCallScreen(override val id: QualifiedID) : CallScreen(id) - - // Initiating call screen is opened - class InitiatingCallScreen(override val id: QualifiedID) : CallScreen(id) - // Import media screen is opened object ImportMedia : CurrentScreen() @@ -193,15 +178,6 @@ sealed class CurrentScreen { is OtherUserProfileScreenDestination -> destination.argsFrom(arguments).conversationId?.let { OtherUserProfile(it) } ?: SomeOther - is OngoingCallScreenDestination -> - OngoingCallScreen(destination.argsFrom(arguments).conversationId) - - is IncomingCallScreenDestination -> - IncomingCallScreen(destination.argsFrom(arguments).conversationId) - - is InitiatingCallScreenDestination -> - InitiatingCallScreen(destination.argsFrom(arguments).conversationId) - is ImportMediaScreenDestination -> ImportMedia is SelfDevicesScreenDestination -> DeviceManager diff --git a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt index e8de020f3ac..5065fa3fd71 100644 --- a/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt +++ b/app/src/main/kotlin/com/wire/android/util/deeplink/DeepLinkProcessor.kt @@ -47,12 +47,17 @@ sealed class DeepLinkResult { data class Failure(val ssoError: SSOFailureCodes) : SSOLogin() } - data class IncomingCall(val conversationsId: ConversationId, val switchedAccount: Boolean = false) : DeepLinkResult() + data class OpenConversation( + val conversationsId: ConversationId, + val switchedAccount: Boolean = false + ) : DeepLinkResult() + + data class OpenOtherUserProfile(val userId: QualifiedID, val switchedAccount: Boolean = false) : + DeepLinkResult() + + data class JoinConversation(val code: String, val key: String, val domain: String?) : + DeepLinkResult() - data class OngoingCall(val conversationsId: ConversationId) : DeepLinkResult() - data class OpenConversation(val conversationsId: ConversationId, val switchedAccount: Boolean = false) : DeepLinkResult() - data class OpenOtherUserProfile(val userId: QualifiedID, val switchedAccount: Boolean = false) : DeepLinkResult() - data class JoinConversation(val code: String, val key: String, val domain: String?) : DeepLinkResult() data class MigrationLogin(val userHandle: String) : DeepLinkResult() } @@ -69,10 +74,12 @@ class DeepLinkProcessor @Inject constructor( return when (uri.host) { ACCESS_DEEPLINK_HOST -> getCustomServerConfigDeepLinkResult(uri) SSO_LOGIN_DEEPLINK_HOST -> getSSOLoginDeepLinkResult(uri) - INCOMING_CALL_DEEPLINK_HOST -> getIncomingCallDeepLinkResult(uri, switchedAccount) - ONGOING_CALL_DEEPLINK_HOST -> getOngoingCallDeepLinkResult(uri) CONVERSATION_DEEPLINK_HOST -> getOpenConversationDeepLinkResult(uri, switchedAccount) - OTHER_USER_PROFILE_DEEPLINK_HOST -> getOpenOtherUserProfileDeepLinkResult(uri, switchedAccount) + OTHER_USER_PROFILE_DEEPLINK_HOST -> getOpenOtherUserProfileDeepLinkResult( + uri, + switchedAccount + ) + MIGRATION_LOGIN_HOST -> getOpenMigrationLoginDeepLinkResult(uri) JOIN_CONVERSATION_DEEPLINK_HOST -> getJoinConversationDeepLinkResult(uri) else -> DeepLinkResult.Unknown @@ -80,25 +87,32 @@ class DeepLinkProcessor @Inject constructor( } private suspend fun switchAccountIfNeeded(uri: Uri): Boolean { - uri.getQueryParameter(USER_TO_USE_QUERY_PARAM)?.toQualifiedID(qualifiedIdMapper)?.let { userId -> - val shouldSwitchAccount = when (val result = currentSession()) { - is CurrentSessionResult.Failure.Generic -> true - CurrentSessionResult.Failure.SessionNotFound -> true - is CurrentSessionResult.Success -> result.accountInfo.userId != userId + uri.getQueryParameter(USER_TO_USE_QUERY_PARAM)?.toQualifiedID(qualifiedIdMapper) + ?.let { userId -> + val shouldSwitchAccount = when (val result = currentSession()) { + is CurrentSessionResult.Failure.Generic -> true + CurrentSessionResult.Failure.SessionNotFound -> true + is CurrentSessionResult.Success -> result.accountInfo.userId != userId + } + if (shouldSwitchAccount) { + return accountSwitch(SwitchAccountParam.SwitchToAccount(userId)) == SwitchAccountResult.SwitchedToAnotherAccount + } } - if (shouldSwitchAccount) { - return accountSwitch(SwitchAccountParam.SwitchToAccount(userId)) == SwitchAccountResult.SwitchedToAnotherAccount - } - } return false } - private fun getOpenConversationDeepLinkResult(uri: Uri, switchedAccount: Boolean): DeepLinkResult = + private fun getOpenConversationDeepLinkResult( + uri: Uri, + switchedAccount: Boolean + ): DeepLinkResult = uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let { conversationId -> DeepLinkResult.OpenConversation(conversationId, switchedAccount) } ?: DeepLinkResult.Unknown - private fun getOpenOtherUserProfileDeepLinkResult(uri: Uri, switchedAccount: Boolean): DeepLinkResult = + private fun getOpenOtherUserProfileDeepLinkResult( + uri: Uri, + switchedAccount: Boolean + ): DeepLinkResult = uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let { DeepLinkResult.OpenOtherUserProfile(it, switchedAccount) } ?: DeepLinkResult.Unknown @@ -109,18 +123,9 @@ class DeepLinkProcessor @Inject constructor( else DeepLinkResult.MigrationLogin(it) } ?: DeepLinkResult.Unknown - private fun getCustomServerConfigDeepLinkResult(uri: Uri) = uri.getQueryParameter(SERVER_CONFIG_PARAM)?.let { - DeepLinkResult.CustomServerConfig(it) - } ?: DeepLinkResult.Unknown - - private fun getIncomingCallDeepLinkResult(uri: Uri, switchedAccount: Boolean) = - uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let { - DeepLinkResult.IncomingCall(it, switchedAccount) - } ?: DeepLinkResult.Unknown - - private fun getOngoingCallDeepLinkResult(uri: Uri) = - uri.lastPathSegment?.toQualifiedID(qualifiedIdMapper)?.let { - DeepLinkResult.OngoingCall(it) + private fun getCustomServerConfigDeepLinkResult(uri: Uri) = + uri.getQueryParameter(SERVER_CONFIG_PARAM)?.let { + DeepLinkResult.CustomServerConfig(it) } ?: DeepLinkResult.Unknown private fun getSSOLoginDeepLinkResult(uri: Uri): DeepLinkResult { @@ -162,8 +167,6 @@ class DeepLinkProcessor @Inject constructor( const val SSO_LOGIN_COOKIE_PARAM = "cookie" const val SSO_LOGIN_ERROR_PARAM = "error" const val SSO_LOGIN_SERVER_CONFIG_PARAM = "location" - const val INCOMING_CALL_DEEPLINK_HOST = "incoming-call" - const val ONGOING_CALL_DEEPLINK_HOST = "ongoing-call" const val CONVERSATION_DEEPLINK_HOST = "conversation" const val OTHER_USER_PROFILE_DEEPLINK_HOST = "other-user-profile" const val MIGRATION_LOGIN_HOST = "migration-login" @@ -176,7 +179,10 @@ class DeepLinkProcessor @Inject constructor( } enum class SSOFailureCodes(val label: String, val errorCode: Int) { - ServerErrorUnsupportedSaml("server-error-unsupported-saml", SSOServerErrorCode.SERVER_ERROR_UNSUPPORTED_SAML), + ServerErrorUnsupportedSaml( + "server-error-unsupported-saml", + SSOServerErrorCode.SERVER_ERROR_UNSUPPORTED_SAML + ), BadSuccessRedirect("bad-success-redirect", SSOServerErrorCode.BAD_SUCCESS_REDIRECT), BadFailureRedirect("bad-failure-redirect", SSOServerErrorCode.BAD_FAILURE_REDIRECT), BadUsername("bad-username", SSOServerErrorCode.BAD_USERNAME), @@ -185,7 +191,10 @@ enum class SSOFailureCodes(val label: String, val errorCode: Int) { NotFound("not-found", SSOServerErrorCode.NOT_FOUND), Forbidden("forbidden", SSOServerErrorCode.FORBIDDEN), NoMatchingAuthReq("no-matching-auth-req", SSOServerErrorCode.NO_MATCHING_AUTH_REQ), - InsufficientPermissions("insufficient-permissions", SSOServerErrorCode.INSUFFICIENT_PERMISSIONS), + InsufficientPermissions( + "insufficient-permissions", + SSOServerErrorCode.INSUFFICIENT_PERMISSIONS + ), Unknown("unknown", SSOServerErrorCode.UNKNOWN); companion object { diff --git a/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt b/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt deleted file mode 100644 index 907702cb7bf..00000000000 --- a/app/src/main/kotlin/com/wire/android/util/ui/ScreenSettingsUtil.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.android.util.ui - -import android.app.Activity -import android.os.Build -import android.view.WindowManager -import androidx.navigation.NavDestination -import com.wire.android.navigation.style.ScreenMode -import com.wire.android.navigation.style.ScreenModeStyle -import com.wire.android.navigation.toDestination - -fun Activity.updateScreenSettings(navDestination: NavDestination) { - val screenMode = (navDestination.toDestination()?.style as? ScreenModeStyle)?.screenMode() ?: ScreenMode.NONE - updateScreenSettings(screenMode) -} - -private fun Activity.updateScreenSettings(screenMode: ScreenMode?) { - when (screenMode) { - ScreenMode.WAKE_UP -> wakeUpDevice() - ScreenMode.KEEP_ON -> addScreenOnFlags() - else -> removeScreenOnFlags() - } -} - -private fun Activity.wakeUpDevice() { - - addScreenOnFlags() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(true) - setTurnScreenOn(true) - } else { - window.addFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) - } -} - -private fun Activity.addScreenOnFlags() { - window.addFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - ) -} - -private fun Activity.removeScreenOnFlags() { - window.clearFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - ) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - setShowWhenLocked(false) - setTurnScreenOn(false) - } else { - window.clearFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) - } -} diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 4e3905c7ba6..41c7c7b5df1 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -232,35 +232,6 @@ class WireActivityViewModelTest { verify(exactly = 1) { arrangement.onDeepLinkResult(result) } } - @Test - fun `given Intent with IncomingCall, when currentSession is present, then initialAppState is LOGGED_IN and result IncomingCall`() = - runTest { - val result = DeepLinkResult.IncomingCall(ConversationId("val", "dom")) - val (arrangement, viewModel) = Arrangement() - .withSomeCurrentSession() - .withDeepLinkResult(result) - .arrange() - - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) - - assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) - verify(exactly = 1) { arrangement.onDeepLinkResult(result) } - } - - @Test - fun `given Intent with IncomingCall, when currentSession is absent, then initialAppState is NOT_LOGGED_IN`() = runTest { - val result = DeepLinkResult.IncomingCall(ConversationId("val", "dom")) - val (arrangement, viewModel) = Arrangement() - .withNoCurrentSession() - .withDeepLinkResult(result) - .arrange() - - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) - - assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) - verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } - } - @Test fun `given Intent with OpenConversation, when currentSession is present, then initialAppState is LOGGED_IN and result OpenConversation`() = runTest { diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt index ddb7a36e4ff..8fd9b84edaa 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt @@ -25,7 +25,6 @@ import com.wire.android.config.NavigationTestExtension import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.ui.calling.ongoing.OngoingCallViewModel import com.wire.android.ui.home.conversationslist.model.Membership -import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.kalium.logic.data.call.Call @@ -81,7 +80,7 @@ class OngoingCallViewModelTest { @BeforeEach fun setup() { MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = conversationId) + every { savedStateHandle.get("conversationId") } returns conversationId.toString() coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall())) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false @@ -89,6 +88,7 @@ class OngoingCallViewModelTest { ongoingCallViewModel = OngoingCallViewModel( savedStateHandle = savedStateHandle, + conversationIdInjected = conversationId, establishedCalls = establishedCall, requestVideoStreams = requestVideoStreams, currentScreenManager = currentScreenManager, diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index 82e7ffe8208..bf8d15d9e63 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -63,9 +63,6 @@ import org.junit.jupiter.api.extension.ExtendWith @ExtendWith(NavigationTestExtension::class) class SharedCallingViewModelTest { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var allCalls: GetAllCallsWithSortedParticipantsUseCase @@ -133,7 +130,6 @@ class SharedCallingViewModelTest { fun setup() { val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain") MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId) coEvery { allCalls.invoke() } returns emptyFlow() coEvery { observeConversationDetails.invoke(any()) } returns emptyFlow() coEvery { observeSpeaker.invoke() } returns emptyFlow() @@ -142,7 +138,7 @@ class SharedCallingViewModelTest { ) sharedCallingViewModel = SharedCallingViewModel( - savedStateHandle = savedStateHandle, + conversationId = conversationId, conversationDetails = observeConversationDetails, allCalls = allCalls, endCall = endCall, diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt index 7b6ae3ab5db..4d6e5c2a9b5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt @@ -18,12 +18,10 @@ package com.wire.android.ui.calling.incoming -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.media.CallRinger -import com.wire.android.ui.calling.CallingNavArgs -import com.wire.android.ui.navArgs +import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation @@ -59,9 +57,6 @@ class IncomingCallViewModelTest { class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var rejectCall: RejectCallUseCase @@ -83,9 +78,11 @@ class IncomingCallViewModelTest { @MockK lateinit var muteCall: MuteCallUseCase + @MockK + lateinit var lockCodeTimeManager: LockCodeTimeManager + init { MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId) // Default empty values coEvery { rejectCall(any()) } returns Unit @@ -106,14 +103,15 @@ class IncomingCallViewModelTest { } fun arrange() = this to IncomingCallViewModel( - savedStateHandle = savedStateHandle, + conversationId = dummyConversationId, incomingCalls = incomingCalls, rejectCall = rejectCall, acceptCall = acceptCall, callRinger = callRinger, observeEstablishedCalls = observeEstablishedCalls, endCall = endCall, - muteCall = muteCall + muteCall = muteCall, + lockCodeTimeManager = lockCodeTimeManager ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt index 34987789c08..9f44e203da9 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt @@ -18,12 +18,9 @@ package com.wire.android.ui.calling.initiating -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.media.CallRinger -import com.wire.android.ui.calling.CallingNavArgs -import com.wire.android.ui.navArgs import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase @@ -48,47 +45,46 @@ import org.junit.jupiter.api.extension.ExtendWith class InitiatingCallViewModelTest { @Test - fun `given an outgoing call, when the user ends call, then invoke endCall useCase and close the screen`() = runTest { - // Given - val (arrangement, viewModel) = Arrangement() - .withEndingCall() - .withStartCallSucceeding() - .arrange() - - // When - viewModel.hangUpCall() - advanceUntilIdle() - - // Then - with(arrangement) { - coVerify(exactly = 1) { endCall(any()) } - coVerify(exactly = 1) { callRinger.stop() } + fun `given an outgoing call, when the user ends call, then invoke endCall useCase and close the screen`() = + runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withEndingCall() + .withStartCallSucceeding() + .arrange() + + // When + viewModel.hangUpCall() + advanceUntilIdle() + + // Then + with(arrangement) { + coVerify(exactly = 1) { endCall(any()) } + coVerify(exactly = 1) { callRinger.stop() } + } + assertTrue { viewModel.state.flowState is InitiatingCallState.FlowState.CallClosed } } - assertTrue { viewModel.state.flowState is InitiatingCallState.FlowState.CallClosed } - } @Test - fun `given a start call error, when user tries to start a call, call ring tone is not called`() = runTest { - // Given - val (arrangement, viewModel) = Arrangement() - .withNoInternetConnection() - .withStartCallSucceeding() - .arrange() - - // When - viewModel.initiateCall() - - // Then - with(arrangement) { - coVerify(exactly = 0) { callRinger.ring(any()) } + fun `given a start call error, when user tries to start a call, call ring tone is not called`() = + runTest { + // Given + val (arrangement, viewModel) = Arrangement() + .withNoInternetConnection() + .withStartCallSucceeding() + .arrange() + + // When + viewModel.initiateCall() + + // Then + with(arrangement) { + coVerify(exactly = 0) { callRinger.ring(any()) } + } } - } private class Arrangement { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var establishedCalls: ObserveEstablishedCallsUseCase @@ -104,9 +100,11 @@ class InitiatingCallViewModelTest { @MockK lateinit var endCall: EndCallUseCase + val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain") + val initiatingCallViewModel by lazy { InitiatingCallViewModel( - savedStateHandle = savedStateHandle, + conversationId = dummyConversationId, observeEstablishedCalls = establishedCalls, startCall = startCall, endCall = endCall, @@ -116,9 +114,7 @@ class InitiatingCallViewModelTest { } init { - val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain") MockKAnnotations.init(this) - every { savedStateHandle.navArgs() } returns CallingNavArgs(conversationId = dummyConversationId) coEvery { isLastCallClosed.invoke(any(), any()) } returns flowOf(false) coEvery { establishedCalls() } returns flowOf(emptyList()) every { callRinger.ring(any(), any(), any()) } returns Unit diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt index 7ad54890d65..9c0b4f28d6d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt @@ -103,11 +103,10 @@ class CommonTopAppBarViewModelTest { } @Test - fun givenActiveCallAndCallScreenAndConnectivityIssues_whenGettingState_thenShouldHaveConnectivityInfo() = runTest { + fun givenActiveCallAndConnectivityIssues_whenGettingState_thenShouldHaveConnectivityInfo() = runTest { val (_, commonTopAppBarViewModel) = Arrangement() .withCurrentSessionExist() .withActiveCall() - .withCurrentScreen(CurrentScreen.OngoingCallScreen(mockk())) .withSyncState(SyncState.Waiting) .arrange() diff --git a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt index 6fe11e9b580..f14c907c145 100644 --- a/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt +++ b/app/src/test/kotlin/com/wire/android/util/DeepLinkProcessorTest.kt @@ -105,34 +105,6 @@ class DeepLinkProcessorTest { ) } - @Test - fun `given a incoming call deeplink for current user, returns IncomingCall with conversationId and not switched account`() = runTest { - val (arrangement, deepLinkProcessor) = Arrangement() - .withIncomingCallDeepLink(CURRENT_USER_ID) - .withCurrentSessionSuccess(CURRENT_USER_ID) - .arrange() - val incomingCallResult = deepLinkProcessor(arrangement.uri) - assertInstanceOf(DeepLinkResult.IncomingCall::class.java, incomingCallResult) - assertEquals( - DeepLinkResult.IncomingCall(CONVERSATION_ID, false), - incomingCallResult - ) - } - - @Test - fun `given a incoming call deeplink for other user, returns IncomingCall with conversationId and switched account`() = runTest { - val (arrangement, deepLinkProcessor) = Arrangement() - .withIncomingCallDeepLink(OTHER_USER_ID) - .withCurrentSessionSuccess(CURRENT_USER_ID) - .arrange() - val incomingCallResult = deepLinkProcessor(arrangement.uri) - assertInstanceOf(DeepLinkResult.IncomingCall::class.java, incomingCallResult) - assertEquals( - DeepLinkResult.IncomingCall(CONVERSATION_ID, true), - incomingCallResult - ) - } - @Test fun `given a invalid deeplink, returns Unknown object`() = runTest { val (arrangement, deepLinkProcessor) = Arrangement() @@ -248,12 +220,6 @@ class DeepLinkProcessorTest { coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns null } - fun withIncomingCallDeepLink(userId: UserId = CURRENT_USER_ID) = apply { - coEvery { uri.host } returns DeepLinkProcessor.INCOMING_CALL_DEEPLINK_HOST - coEvery { uri.lastPathSegment } returns CONVERSATION_ID.toString() - coEvery { uri.getQueryParameter(DeepLinkProcessor.USER_TO_USE_QUERY_PARAM) } returns userId.toString() - } - fun withConversationDeepLink(userId: UserId = CURRENT_USER_ID) = apply { coEvery { uri.host } returns DeepLinkProcessor.CONVERSATION_DEEPLINK_HOST coEvery { uri.lastPathSegment } returns CONVERSATION_ID.toString() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 855b04fcd06..e3727b48e90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,8 +52,8 @@ compose-navigation = "2.7.7" # adjusted to work with compose-destinations "1.9.5 compose-destinations = "1.10.2" # Hilt -hilt = "2.48.1" -hilt-composeNavigation = "1.1.0" +hilt = "2.49" +hilt-composeNavigation = "1.2.0" hilt-work = "1.2.0" # Android UI diff --git a/kalium b/kalium index 3ff6e66006b..8e94e8a026a 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 3ff6e66006b022c2d9196b09602917761057df20 +Subproject commit 8e94e8a026ac5c64c456e3717f7ea2dbe7860c64 From eabcf67663661d5ef8634d9473738c75ecfbe0a8 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 10 Apr 2024 12:32:17 +0200 Subject: [PATCH 02/29] chore: unit test --- .../ui/calling/incoming/IncomingCallScreen.kt | 31 ++++++----- .../calling/incoming/IncomingCallViewModel.kt | 24 ++++++--- .../incoming/IncomingCallViewModelTest.kt | 52 ++++++++++++++++--- 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index ad50c182a11..486a2a92eb6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -41,6 +41,7 @@ import com.wire.android.R import com.wire.android.appLogger import com.wire.android.ui.AppLockActivity import com.wire.android.ui.LocalActivity +import com.wire.android.ui.calling.CallActivity import com.wire.android.ui.calling.CallState import com.wire.android.ui.calling.SharedCallingViewModel import com.wire.android.ui.calling.common.CallVideoPreview @@ -80,11 +81,7 @@ fun IncomingCallScreen( val audioPermissionCheck = AudioPermissionCheckFlow( onAcceptCall = { incomingCallViewModel.acceptCall { - Intent(activity, AppLockActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - }.run { - activity.startActivity(this) - } + (activity as CallActivity).openAppLockActivity() } }, onPermanentPermissionDecline = { @@ -103,11 +100,7 @@ fun IncomingCallScreen( onDismiss = ::dismissJoinCallAnywayDialog, onConfirm = { acceptCallAnyway { - Intent(activity, AppLockActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - }.run { - activity.startActivity(this) - } + (activity as CallActivity).openAppLockActivity() } } ) @@ -134,8 +127,14 @@ fun IncomingCallScreen( toggleSpeaker = ::toggleSpeaker, toggleVideo = ::toggleVideo, declineCall = { - incomingCallViewModel::declineCall - activity.finish() + incomingCallViewModel.declineCall( + onAppLocked = { + (activity as CallActivity).openAppLockActivity() + }, + onCallRejected = { + activity.finish() + } + ) }, acceptCall = audioPermissionCheck::launch, onVideoPreviewCreated = ::setVideoPreview, @@ -159,6 +158,14 @@ fun IncomingCallScreen( ) } +fun CallActivity.openAppLockActivity() { + Intent(this, AppLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + }.run { + startActivity(this) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun IncomingCallContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt index 69392400f5b..3b91915fe63 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModel.kt @@ -99,14 +99,24 @@ class IncomingCallViewModel @AssistedInject constructor( } } - fun declineCall() { + fun declineCall( + onAppLocked: () -> Unit, + onCallRejected: () -> Unit + ) { viewModelScope.launch { - observeIncomingCallJob.cancel() - launch { rejectCall(conversationId = conversationId) } - launch { - callRinger.stop() - incomingCallState = - incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) + lockCodeTimeManager.observeAppLock().first().let { + if (it) { + onAppLocked() + } else { + observeIncomingCallJob.cancel() + launch { rejectCall(conversationId = conversationId) } + launch { + callRinger.stop() + incomingCallState = + incomingCallState.copy(flowState = IncomingCallState.FlowState.CallClosed) + } + onCallRejected() + } } } } diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt index 4d6e5c2a9b5..a180a28366b 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/incoming/IncomingCallViewModelTest.kt @@ -94,6 +94,13 @@ class IncomingCallViewModelTest { coEvery { muteCall(any(), any()) } returns Unit } + fun withAppNotLocked() = apply { + every { lockCodeTimeManager.observeAppLock() } returns flowOf(false) + } + fun withAppLocked() = apply { + every { lockCodeTimeManager.observeAppLock() } returns flowOf(true) + } + fun withEstablishedCalls(flow: Flow>) = apply { coEvery { observeEstablishedCalls.invoke() } returns flow } @@ -115,22 +122,50 @@ class IncomingCallViewModelTest { ) } + @Test + fun `given app Locked, when the user decline the call, then do not reject the call`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withAppLocked() + .arrange() + + viewModel.declineCall({}, {}) + + coVerify(inverse = true) { arrangement.rejectCall(conversationId = any()) } + verify(inverse = true) { arrangement.callRinger.stop() } + } + @Test fun `given an incoming call, when the user decline the call, then the reject call use case is called`() = runTest { - val (arrangement, viewModel) = Arrangement().arrange() + val (arrangement, viewModel) = Arrangement() + .withAppNotLocked() + .arrange() - viewModel.declineCall() + viewModel.declineCall({}, {}) coVerify(exactly = 1) { arrangement.rejectCall(conversationId = any()) } verify(exactly = 1) { arrangement.callRinger.stop() } assertTrue { viewModel.incomingCallState.flowState is IncomingCallState.FlowState.CallClosed } } + @Test + fun `given app locked, when user tries to accept an incoming call, then do not accept the call`() = runTest { + val (arrangement, viewModel) = Arrangement() + .withAppLocked() + .arrange() + + viewModel.acceptCall({}) + + coVerify(inverse = true) { arrangement.acceptCall(conversationId = any()) } + verify(inverse = true) { arrangement.callRinger.stop() } + } + @Test fun `given no ongoing call, when user tries to accept an incoming call, then invoke answerCall call use case`() = runTest { - val (arrangement, viewModel) = Arrangement().arrange() + val (arrangement, viewModel) = Arrangement() + .withAppNotLocked() + .arrange() - viewModel.acceptCall() + viewModel.acceptCall({}) advanceUntilIdle() coVerify(exactly = 1) { arrangement.acceptCall(conversationId = any()) } @@ -143,10 +178,11 @@ class IncomingCallViewModelTest { @Test fun `given an ongoing call, when user tries to accept an incoming call, then show JoinCallAnywayDialog`() = runTest { val (arrangement, viewModel) = Arrangement() + .withAppNotLocked() .withEstablishedCalls(flowOf(listOf(provideCall(ConversationId("value", "Domain"))))) .arrange() - viewModel.acceptCall() + viewModel.acceptCall({}) assertTrue { viewModel.incomingCallState.flowState is IncomingCallState.FlowState.Default } assertEquals(true, viewModel.incomingCallState.shouldShowJoinCallAnywayDialog) @@ -160,11 +196,12 @@ class IncomingCallViewModelTest { val establishedCallsChannel = Channel>(capacity = Channel.UNLIMITED) .also { it.send(listOf(provideCall(ConversationId("value", "Domain")))) } val (arrangement, viewModel) = Arrangement() + .withAppNotLocked() .withEstablishedCalls(establishedCallsChannel.consumeAsFlow()) .withEndCall { establishedCallsChannel.send(listOf()) } .arrange() - viewModel.acceptCallAnyway() + viewModel.acceptCallAnyway({}) advanceUntilIdle() coVerify(exactly = 1) { arrangement.endCall(any()) } @@ -175,10 +212,11 @@ class IncomingCallViewModelTest { @Test fun `given join dialog displayed, when user dismisses it, then hide it`() = runTest { val (arrangement, viewModel) = Arrangement() + .withAppNotLocked() .withEstablishedCalls(flowOf(listOf(provideCall()))) .arrange() - viewModel.acceptCall() + viewModel.acceptCall({}) viewModel.dismissJoinCallAnywayDialog() From 2fdcf1f60209055d90bb1d69e104f56f7e85aaaa Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 10 Apr 2024 17:14:11 +0200 Subject: [PATCH 03/29] chore: unit test --- .../ui/common/topappbar/CommonTopAppBarViewModelTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt index 9c0b4f28d6d..7f5b25832c5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt @@ -107,6 +107,7 @@ class CommonTopAppBarViewModelTest { val (_, commonTopAppBarViewModel) = Arrangement() .withCurrentSessionExist() .withActiveCall() + .withCurrentScreen(CurrentScreen.Home) .withSyncState(SyncState.Waiting) .arrange() @@ -114,7 +115,7 @@ class CommonTopAppBarViewModelTest { val state = commonTopAppBarViewModel.state val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.WaitingConnection::class + info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class } @Test From c3473f082bca3490d766b9b7dc06a2272c1b1d6a Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 10 Apr 2024 18:04:55 +0200 Subject: [PATCH 04/29] chore: detekt --- .../wire/android/notification/PendingIntents.kt | 2 -- .../kotlin/com/wire/android/ui/WireActivity.kt | 1 - .../com/wire/android/ui/calling/CallActivity.kt | 2 +- .../com/wire/android/ui/calling/CallScreen.kt | 17 +++++++++-------- .../ui/calling/SharedCallingViewModel.kt | 3 --- .../ui/calling/incoming/IncomingCallScreen.kt | 1 + .../calling/initiating/InitiatingCallScreen.kt | 1 + .../ui/calling/ongoing/OngoingCallScreen.kt | 1 + .../ui/home/conversations/ConversationScreen.kt | 2 -- .../android/feature/AccountSwitchUseCaseTest.kt | 1 - .../ui/calling/SharedCallingViewModelTest.kt | 3 --- 11 files changed, 13 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt index fbeb7276856..2e207272458 100644 --- a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt +++ b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt @@ -173,14 +173,12 @@ fun openAppPendingIntent(context: Context): PendingIntent { private const val MESSAGE_NOTIFICATIONS_SUMMARY_REQUEST_CODE = 0 private const val DECLINE_CALL_REQUEST_CODE = "decline_call_" -private const val OPEN_INCOMING_CALL_REQUEST_CODE = 2 private const val FULL_SCREEN_REQUEST_CODE = 3 private const val OPEN_ONGOING_CALL_REQUEST_CODE = 4 private const val OPEN_MIGRATION_LOGIN_REQUEST_CODE = 5 private const val END_ONGOING_CALL_REQUEST_CODE = "hang_up_call_" private const val OPEN_MESSAGE_REQUEST_CODE_PREFIX = "open_message_" private const val OPEN_OTHER_USER_PROFILE_CODE_PREFIX = "open_other_user_profile_" -private const val CALL_REQUEST_CODE_PREFIX = "call_" private const val REPLY_MESSAGE_REQUEST_CODE_PREFIX = "reply_" private fun getRequestCode(conversationId: String, prefix: String): Int = (prefix + conversationId).hashCode() diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index 150135973e8..a1e5dae3b7f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -62,7 +62,6 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.NavigationGraph import com.wire.android.navigation.navigateToItem import com.wire.android.navigation.rememberNavigator -import com.wire.android.ui.calling.ProximitySensorManager import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 8eb46928fc9..eb099b991db 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -52,7 +52,7 @@ class CallActivity : AppCompatActivity() { super.onCreate(savedInstanceState) callNotificationManager.hideAllNotifications() - appLogger.i("${TAG} Initializing proximity sensor..") + appLogger.i("$TAG Initializing proximity sensor..") proximitySensorManager.initialize() WindowCompat.setDecorFitsSystemWindows(window, false) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt index 5d9f46f593b..e388dc03c13 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt @@ -33,12 +33,6 @@ import com.wire.android.ui.calling.initiating.InitiatingCallScreen import com.wire.android.ui.calling.ongoing.OngoingCallScreen import com.wire.kalium.logic.data.id.ConversationId -enum class CallScreenType { - Incoming, - Ongoing, - Initiating -} - @Composable fun CallScreen( conversationId: ConversationId, @@ -61,7 +55,7 @@ fun CallScreen( IncomingCallScreen( conversationId = conversationId, onCallAccepted = { - navController.navigate("${CallScreenType.Ongoing.name}/${conversationId}") + navController.navigate("${CallScreenType.Ongoing.name}/$conversationId") } ) } @@ -70,9 +64,16 @@ fun CallScreen( } composable(route = "${CallScreenType.Initiating.name}/{conversationId}") { InitiatingCallScreen(conversationId) { - navController.navigate("${CallScreenType.Ongoing.name}/${conversationId}") + navController.navigate("${CallScreenType.Ongoing.name}/$conversationId") } } } } } + +enum class CallScreenType { + Incoming, + Ongoing, + Initiating +} + diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt index fb8ef346e35..c3d0c3ddf82 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/SharedCallingViewModel.kt @@ -22,7 +22,6 @@ import android.view.View import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger @@ -41,7 +40,6 @@ import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.FlipToBackCameraUseCase import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase @@ -69,7 +67,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch -import javax.inject.Inject @Suppress("LongParameterList", "TooManyFunctions") @HiltViewModel(assistedFactory = SharedCallingViewModel.Factory::class) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index 486a2a92eb6..9a9c14cbff6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -62,6 +62,7 @@ import com.wire.android.util.permission.rememberCallingRecordAudioRequestFlow import com.wire.kalium.logic.data.call.ConversationType import com.wire.kalium.logic.data.id.ConversationId +@Suppress("ParameterWrapping") @Composable fun IncomingCallScreen( conversationId: ConversationId, diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt index 783706a69ad..1bf0dc8da96 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt @@ -55,6 +55,7 @@ import com.wire.android.ui.theme.wireDimensions import com.wire.android.util.permission.PermissionDenialType import com.wire.kalium.logic.data.id.ConversationId +@Suppress("ParameterWrapping") @Composable fun InitiatingCallScreen( conversationId: ConversationId, diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index b98130484a5..aa147cd807a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -85,6 +85,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import java.util.Locale +@Suppress("ParameterWrapping") @Composable fun OngoingCallScreen( conversationId: ConversationId, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index d49cf6dbc34..bd001635ec8 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -82,8 +82,6 @@ import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.LocalActivity -import com.wire.android.ui.calling.CallActivity -import com.wire.android.ui.calling.CallScreenType import com.wire.android.ui.calling.getInitiatingCallIntent import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader diff --git a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt index 0e8364e5bbb..d2551459b28 100644 --- a/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/AccountSwitchUseCaseTest.kt @@ -149,7 +149,6 @@ class AccountSwitchUseCaseTest { MockKAnnotations.init(this, relaxUnitFun = true) } - @OptIn(ExperimentalCoroutinesApi::class) var accountSwitchUseCase: AccountSwitchUseCase = AccountSwitchUseCase( updateCurrentSession, getSessions, diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt index bf8d15d9e63..e1fa9e5325d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/SharedCallingViewModelTest.kt @@ -19,14 +19,12 @@ package com.wire.android.ui.calling import android.view.View -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.config.TestDispatcherProvider import com.wire.android.mapper.UICallParticipantMapper import com.wire.android.mapper.UserTypeMapper import com.wire.android.media.CallRinger -import com.wire.android.ui.navArgs import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager import com.wire.android.util.ui.WireSessionImageLoader @@ -128,7 +126,6 @@ class SharedCallingViewModelTest { @BeforeEach fun setup() { - val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain") MockKAnnotations.init(this) coEvery { allCalls.invoke() } returns emptyFlow() coEvery { observeConversationDetails.invoke(any()) } returns emptyFlow() From eaa8508c65699e1276681c40a0467ac5df642be1 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 10 Apr 2024 18:07:57 +0200 Subject: [PATCH 05/29] chore: detekt --- app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt index e388dc03c13..e52b493e0ad 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt @@ -76,4 +76,3 @@ enum class CallScreenType { Ongoing, Initiating } - From fba80ca229960614323914c071a209e53a96b0ea Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 11 Apr 2024 13:17:59 +0200 Subject: [PATCH 06/29] chore: cleanup --- .../com/wire/android/ui/calling/CallActivity.kt | 9 +++++++++ .../android/ui/calling/incoming/IncomingCallScreen.kt | 11 +---------- .../android/ui/calling/ongoing/OngoingCallScreen.kt | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index eb099b991db..b2b8d114f26 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -30,6 +30,7 @@ import androidx.compose.runtime.remember import androidx.core.view.WindowCompat import com.wire.android.appLogger import com.wire.android.notification.CallNotificationManager +import com.wire.android.ui.AppLockActivity import com.wire.android.ui.LocalActivity import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme @@ -139,3 +140,11 @@ fun getInitiatingCallIntent( putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Initiating.name) } + +fun CallActivity.openAppLockActivity() { + Intent(this, AppLockActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + }.run { + startActivity(this) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index 9a9c14cbff6..b5e4534a935 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.calling.incoming -import android.content.Intent import android.view.View import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box @@ -39,7 +38,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.ui.AppLockActivity import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.CallActivity import com.wire.android.ui.calling.CallState @@ -49,6 +47,7 @@ import com.wire.android.ui.calling.common.CallerDetails import com.wire.android.ui.calling.controlbuttons.AcceptButton import com.wire.android.ui.calling.controlbuttons.CallOptionsControls import com.wire.android.ui.calling.controlbuttons.HangUpButton +import com.wire.android.ui.calling.openAppLockActivity import com.wire.android.ui.common.bottomsheet.WireBottomSheetScaffold import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dialogs.PermissionPermanentlyDeniedDialog @@ -159,14 +158,6 @@ fun IncomingCallScreen( ) } -fun CallActivity.openAppLockActivity() { - Intent(this, AppLockActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT - }.run { - startActivity(this) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun IncomingCallContent( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt index aa147cd807a..90cfd276271 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallScreen.kt @@ -153,7 +153,7 @@ fun OngoingCallScreen( } } ) - BackHandler(enabled = isCameraOn) { + BackHandler { activity.finish() } } From 48ea54b5f16cc4ebb2a92a216f6386691d6dd3c2 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 11 Apr 2024 17:59:50 +0200 Subject: [PATCH 07/29] fix: adjust calling activity flags --- .../wire/android/ui/calling/CallActivity.kt | 19 ++----------------- .../com/wire/android/ui/calling/CallScreen.kt | 11 +++++++---- .../ui/calling/common/CallerDetails.kt | 2 +- .../ui/calling/incoming/IncomingCallScreen.kt | 4 ---- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index b2b8d114f26..52766d25330 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -102,18 +102,11 @@ class CallActivity : AppCompatActivity() { } fun CallActivity.setUpCallingFlags() { - window.addFlags( - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON - ) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) - setTurnScreenOn(true) } else { - window.addFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) + window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) } } @@ -125,14 +118,6 @@ fun getOngoingCallIntent( putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name) } -fun getIncomingCallIntent( - activity: Activity, - conversationId: String -) = Intent(activity, CallActivity::class.java).apply { - putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) - putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name) -} - fun getInitiatingCallIntent( activity: Activity, conversationId: String diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt index e52b493e0ad..db8f54c91d7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt @@ -17,17 +17,19 @@ */ package com.wire.android.ui.calling +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import com.wire.android.ui.calling.incoming.IncomingCallScreen import com.wire.android.ui.calling.initiating.InitiatingCallScreen import com.wire.android.ui.calling.ongoing.OngoingCallScreen @@ -40,7 +42,9 @@ fun CallScreen( navController: NavHostController = rememberNavController() ) { - Scaffold { innerPadding -> + Scaffold( + topBar = { Column(Modifier.size(0.dp)) { } }, + ) { innerPadding -> NavHost( navController = navController, startDestination = "${startDestination.name}/$conversationId", @@ -50,7 +54,6 @@ fun CallScreen( ) { composable( route = "${CallScreenType.Incoming.name}/{conversationId}", - arguments = listOf(navArgument("conversationId") { type = NavType.StringType }) ) { IncomingCallScreen( conversationId = conversationId, diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt index 83bbc428d0a..a9b5baf3c37 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt @@ -71,7 +71,7 @@ fun CallerDetails( proteusVerificationStatus: Conversation.VerificationStatus?, ) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().padding(top = dimensions().spacing32x), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index b5e4534a935..6968d6dd00d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -266,10 +266,6 @@ private fun IncomingCallContent( } } -fun launchLockScreenForIncomingCallScreen() { - appLogger.d("IncomingCall - App locked") -} - @Composable fun AudioPermissionCheckFlow( onAcceptCall: () -> Unit, From cf6836d37674fc370a90016dbf99c5b3f8e20b4d Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 12 Apr 2024 09:34:21 +0200 Subject: [PATCH 08/29] chore: detekt --- app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt index db8f54c91d7..bb87acb63eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.calling import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.absolutePadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size From 5e267db97ce2b5f20988bd29c39c4884255dd83d Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 16 Apr 2024 15:53:57 +0200 Subject: [PATCH 09/29] feat: add correct activity flags for callingActivity --- .../wire/android/ui/calling/CallActivity.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 52766d25330..af8fbe3ec94 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.calling import android.app.Activity +import android.app.KeyguardManager import android.content.Intent import android.os.Build import android.os.Bundle @@ -27,6 +28,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember +import androidx.core.content.getSystemService import androidx.core.view.WindowCompat import com.wire.android.appLogger import com.wire.android.notification.CallNotificationManager @@ -51,13 +53,15 @@ class CallActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + setupCallActivity() + callNotificationManager.hideAllNotifications() appLogger.i("$TAG Initializing proximity sensor..") proximitySensorManager.initialize() WindowCompat.setDecorFitsSystemWindows(window, false) - setUpCallingFlags() val conversationId = intent.extras?.getString(EXTRA_CONVERSATION_ID) val screenType = intent.extras?.getString(EXTRA_SCREEN_TYPE) @@ -101,12 +105,24 @@ class CallActivity : AppCompatActivity() { } } -fun CallActivity.setUpCallingFlags() { - window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) +/** + * Enable the calling activity to be shown in the lockscreen and dismiss the keyguard to enable + * users to answer without unblocking. + */ +private fun CallActivity.setupCallActivity() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) + setTurnScreenOn(true) } else { - window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, + ) + } + + val keyguardManager = getSystemService() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && keyguardManager != null) { + keyguardManager.requestDismissKeyguard(this, null) } } From 2732ea016e1e53b0c28044628695b551abe6f8f5 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Tue, 16 Apr 2024 16:24:47 +0200 Subject: [PATCH 10/29] fix: replace deprecated flags --- .../com/wire/android/ui/calling/CallActivity.kt | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index af8fbe3ec94..06b3de52c19 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -54,10 +54,10 @@ class CallActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setupCallActivity() - callNotificationManager.hideAllNotifications() + setUpCallingFlags() + appLogger.i("$TAG Initializing proximity sensor..") proximitySensorManager.initialize() @@ -105,11 +105,7 @@ class CallActivity : AppCompatActivity() { } } -/** - * Enable the calling activity to be shown in the lockscreen and dismiss the keyguard to enable - * users to answer without unblocking. - */ -private fun CallActivity.setupCallActivity() { +fun CallActivity.setUpCallingFlags() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { setShowWhenLocked(true) setTurnScreenOn(true) @@ -119,11 +115,6 @@ private fun CallActivity.setupCallActivity() { or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, ) } - - val keyguardManager = getSystemService() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && keyguardManager != null) { - keyguardManager.requestDismissKeyguard(this, null) - } } fun getOngoingCallIntent( From db2c6bfee53920d520f6ba4c964bf90052621997 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 17 Apr 2024 11:07:28 +0200 Subject: [PATCH 11/29] chore: address comments --- .../wire/android/ui/calling/CallActivity.kt | 50 +++++++++--- .../com/wire/android/ui/calling/CallScreen.kt | 80 ------------------- .../wire/android/ui/calling/CallScreenType.kt | 25 ++++++ .../calling/ongoing/OngoingCallViewModel.kt | 12 +-- 4 files changed, 67 insertions(+), 100 deletions(-) delete mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 06b3de52c19..6dfd4a3333a 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -18,22 +18,29 @@ package com.wire.android.ui.calling import android.app.Activity -import android.app.KeyguardManager import android.content.Intent import android.os.Build import android.os.Bundle import android.view.WindowManager import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.togetherWith import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.core.content.getSystemService +import androidx.compose.runtime.setValue import androidx.core.view.WindowCompat import com.wire.android.appLogger +import com.wire.android.navigation.style.TransitionAnimationType import com.wire.android.notification.CallNotificationManager import com.wire.android.ui.AppLockActivity import com.wire.android.ui.LocalActivity +import com.wire.android.ui.calling.incoming.IncomingCallScreen +import com.wire.android.ui.calling.initiating.InitiatingCallScreen +import com.wire.android.ui.calling.ongoing.OngoingCallScreen import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl @@ -73,14 +80,39 @@ class CallActivity : AppCompatActivity() { LocalActivity provides this ) { WireTheme { - conversationId?.let { - screenType?.let { screenType -> - val startDestination = CallScreenType.valueOf(screenType) - CallScreen( - conversationId = qualifiedIdMapper.fromStringToQualifiedID(it), - startDestination = startDestination - ) + var currentCallScreenType by remember { mutableStateOf(screenType) } + currentCallScreenType?.let { currentScreenType -> + AnimatedContent( + targetState = currentScreenType, + transitionSpec = { + TransitionAnimationType.POP_UP.enterTransition.togetherWith( + TransitionAnimationType.POP_UP.exitTransition + ) + }, + label = currentScreenType + ) { screenType -> + conversationId?.let { + when (screenType) { + CallScreenType.Initiating.name -> InitiatingCallScreen( + qualifiedIdMapper.fromStringToQualifiedID(it) + ) { + currentCallScreenType = CallScreenType.Ongoing.name + } + + CallScreenType.Ongoing.name -> OngoingCallScreen( + qualifiedIdMapper.fromStringToQualifiedID(it) + ) + + CallScreenType.Incoming.name -> IncomingCallScreen( + qualifiedIdMapper.fromStringToQualifiedID(it) + ) { + currentCallScreenType = CallScreenType.Ongoing.name + } + } + } } + } ?: run { + finish() } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt deleted file mode 100644 index bb87acb63eb..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreen.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.android.ui.calling - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.wire.android.ui.calling.incoming.IncomingCallScreen -import com.wire.android.ui.calling.initiating.InitiatingCallScreen -import com.wire.android.ui.calling.ongoing.OngoingCallScreen -import com.wire.kalium.logic.data.id.ConversationId - -@Composable -fun CallScreen( - conversationId: ConversationId, - startDestination: CallScreenType, - navController: NavHostController = rememberNavController() -) { - - Scaffold( - topBar = { Column(Modifier.size(0.dp)) { } }, - ) { innerPadding -> - NavHost( - navController = navController, - startDestination = "${startDestination.name}/$conversationId", - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ) { - composable( - route = "${CallScreenType.Incoming.name}/{conversationId}", - ) { - IncomingCallScreen( - conversationId = conversationId, - onCallAccepted = { - navController.navigate("${CallScreenType.Ongoing.name}/$conversationId") - } - ) - } - composable(route = "${CallScreenType.Ongoing.name}/{conversationId}") { - OngoingCallScreen(conversationId) - } - composable(route = "${CallScreenType.Initiating.name}/{conversationId}") { - InitiatingCallScreen(conversationId) { - navController.navigate("${CallScreenType.Ongoing.name}/$conversationId") - } - } - } - } -} - -enum class CallScreenType { - Incoming, - Ongoing, - Initiating -} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt new file mode 100644 index 00000000000..e5b43bf08ca --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.calling + +enum class CallScreenType { + Incoming, + Ongoing, + Initiating +} diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt index 714c5e8196d..b61a23d7060 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/ongoing/OngoingCallViewModel.kt @@ -22,7 +22,6 @@ import android.os.CountDownTimer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.appLogger @@ -35,7 +34,6 @@ import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallClient import com.wire.kalium.logic.data.call.VideoState import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.RequestVideoStreamsUseCase @@ -52,9 +50,8 @@ import kotlinx.coroutines.launch @Suppress("LongParameterList") @HiltViewModel(assistedFactory = OngoingCallViewModel.Factory::class) class OngoingCallViewModel @AssistedInject constructor( - savedStateHandle: SavedStateHandle, @Assisted - val conversationIdInjected: ConversationId, + val conversationId: ConversationId, @CurrentAccount private val currentUserId: UserId, private val globalDataStore: GlobalDataStore, @@ -63,13 +60,6 @@ class OngoingCallViewModel @AssistedInject constructor( private val setVideoSendState: SetVideoSendStateUseCase, private val currentScreenManager: CurrentScreenManager ) : ViewModel() { - private val qualifiedIdMapper = QualifiedIdMapperImpl(null) - - private val conversationIdString: String? = savedStateHandle["conversationId"] - private val conversationId = - conversationIdString?.let { qualifiedIdMapper.fromStringToQualifiedID(it) } - ?: conversationIdInjected - var shouldShowDoubleTapToast: Boolean by mutableStateOf(false) private set private var doubleTapIndicatorCountDownTimer: CountDownTimer? = null From cdbbd522ffe81c74ab48359f864194d1129a07e7 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Wed, 17 Apr 2024 12:01:56 +0200 Subject: [PATCH 12/29] chore: unit test --- .../ui/calling/OngoingCallViewModelTest.kt | 66 ++++++++++++------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt index 8fd9b84edaa..50404c04250 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/OngoingCallViewModelTest.kt @@ -18,10 +18,9 @@ package com.wire.android.ui.calling -import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension -import com.wire.android.datastore.GlobalDataStore import com.wire.android.config.NavigationTestExtension +import com.wire.android.datastore.GlobalDataStore import com.wire.android.ui.calling.model.UICallParticipant import com.wire.android.ui.calling.ongoing.OngoingCallViewModel import com.wire.android.ui.home.conversationslist.model.Membership @@ -41,7 +40,6 @@ import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -49,17 +47,14 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(NavigationTestExtension::class) @ExtendWith(CoroutineTestExtension::class) class OngoingCallViewModelTest { - @MockK - private lateinit var savedStateHandle: SavedStateHandle - @MockK private lateinit var establishedCall: ObserveEstablishedCallsUseCase @@ -80,15 +75,15 @@ class OngoingCallViewModelTest { @BeforeEach fun setup() { MockKAnnotations.init(this) - every { savedStateHandle.get("conversationId") } returns conversationId.toString() coEvery { establishedCall.invoke() } returns flowOf(listOf(provideCall())) - coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) + coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow( + CurrentScreen.SomeOther + ) coEvery { globalDataStore.getShouldShowDoubleTapToast(any()) } returns false coEvery { setVideoSendState.invoke(any(), any()) } returns Unit ongoingCallViewModel = OngoingCallViewModel( - savedStateHandle = savedStateHandle, - conversationIdInjected = conversationId, + conversationId = conversationId, establishedCalls = establishedCall, requestVideoStreams = requestVideoStreams, currentScreenManager = currentScreenManager, @@ -113,26 +108,42 @@ class OngoingCallViewModelTest { } @Test - fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = runTest { - val expectedClients = listOf( - CallClient(participant1.id.toString(), participant1.clientId), - CallClient(participant3.id.toString(), participant3.clientId) - ) - coEvery { requestVideoStreams(conversationId = conversationId, expectedClients) } returns Unit - - ongoingCallViewModel.requestVideoStreams(participants) - - coVerify(exactly = 1) { requestVideoStreams(conversationId, expectedClients) } - } + fun givenParticipantsList_WhenRequestingVideoStream_ThenRequestItForOnlyParticipantsWithVideoEnabled() = + runTest { + val expectedClients = listOf( + CallClient(participant1.id.toString(), participant1.clientId), + CallClient(participant3.id.toString(), participant3.clientId) + ) + coEvery { + requestVideoStreams( + conversationId = conversationId, + expectedClients + ) + } returns Unit + + ongoingCallViewModel.requestVideoStreams(participants) + + coVerify(exactly = 1) { requestVideoStreams(conversationId, expectedClients) } + } @Test fun givenDoubleTabIndicatorIsDisplayed_whenUserTapsOnIt_thenHideIt() = runTest { - coEvery { globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false) } returns Unit + coEvery { + globalDataStore.setShouldShowDoubleTapToastStatus( + currentUserId.toString(), + false + ) + } returns Unit ongoingCallViewModel.hideDoubleTapToast() assertEquals(false, ongoingCallViewModel.shouldShowDoubleTapToast) - coVerify(exactly = 1) { globalDataStore.setShouldShowDoubleTapToastStatus(currentUserId.toString(), false) } + coVerify(exactly = 1) { + globalDataStore.setShouldShowDoubleTapToastStatus( + currentUserId.toString(), + false + ) + } } companion object { @@ -174,7 +185,12 @@ class OngoingCallViewModelTest { val participants = listOf(participant1, participant2, participant3) } - private fun provideCall(id: ConversationId = ConversationId("some-dummy-value", "some.dummy.domain")) = Call( + private fun provideCall( + id: ConversationId = ConversationId( + "some-dummy-value", + "some.dummy.domain" + ) + ) = Call( conversationId = id, status = CallStatus.ESTABLISHED, callerId = "caller_id", From a6e3951cf1f0d2d4c78505f480fd109616abeb13 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 18 Apr 2024 14:50:19 +0200 Subject: [PATCH 13/29] feat: minimise incoming and outgoing call screens --- app/src/main/AndroidManifest.xml | 1 + .../android/di/accountScoped/CallsModule.kt | 8 ++ .../notification/CallNotificationManager.kt | 82 ++++++++++++-- .../notification/NotificationActions.kt | 2 +- .../NotificationChannelsManager.kt | 28 +++++ .../notification/NotificationConstants.kt | 4 + .../android/notification/PendingIntents.kt | 22 +++- .../notification/WireNotificationManager.kt | 16 ++- .../com/wire/android/ui/WireActivity.kt | 12 ++ .../wire/android/ui/calling/CallActivity.kt | 27 +++-- .../wire/android/ui/calling/CallScreenType.kt | 2 +- .../ui/calling/common/CallerDetails.kt | 17 ++- .../ui/calling/controlbuttons/AcceptButton.kt | 2 +- .../calling/controlbuttons/DeclineButton.kt | 4 +- .../ui/calling/controlbuttons/HangUpButton.kt | 4 +- .../ui/calling/incoming/IncomingCallScreen.kt | 12 +- .../OutgoingCallScreen.kt} | 39 ++++--- .../OutgoingCallState.kt} | 10 +- .../OutgoingCallViewModel.kt} | 45 +++++--- .../ui/common/topappbar/CommonTopAppBar.kt | 104 +++++++++++++++--- .../topappbar/CommonTopAppBarViewModel.kt | 19 +++- .../common/topappbar/ConnectivityUIState.kt | 10 ++ .../home/conversations/ConversationScreen.kt | 20 ++-- app/src/main/res/values/strings.xml | 3 + .../OutgoingCallViewModelTest.kt} | 64 +++++++++-- .../wire/android/ui/theme/WireDimensions.kt | 12 +- 26 files changed, 451 insertions(+), 118 deletions(-) rename app/src/main/kotlin/com/wire/android/ui/calling/{initiating/InitiatingCallScreen.kt => outgoing/OutgoingCallScreen.kt} (85%) rename app/src/main/kotlin/com/wire/android/ui/calling/{initiating/InitiatingCallState.kt => outgoing/OutgoingCallState.kt} (79%) rename app/src/main/kotlin/com/wire/android/ui/calling/{initiating/InitiatingCallViewModel.kt => outgoing/OutgoingCallViewModel.kt} (71%) rename app/src/test/kotlin/com/wire/android/ui/calling/{initiating/InitiatingCallViewModelTest.kt => outgoing/OutgoingCallViewModelTest.kt} (67%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d240aed87da..7433527e789 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -92,6 +92,7 @@ diff --git a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt index c0f98555a70..d43e14f78b5 100644 --- a/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt +++ b/app/src/main/kotlin/com/wire/android/di/accountScoped/CallsModule.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.call.usecase.FlipToFrontCameraUseCase import com.wire.kalium.logic.feature.call.usecase.GetAllCallsWithSortedParticipantsUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveSpeakerUseCase import com.wire.kalium.logic.feature.call.usecase.SetVideoPreviewUseCase import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase @@ -98,6 +99,13 @@ class CallsModule { ): ObserveEstablishedCallsUseCase = callsScope.establishedCall + @ViewModelScoped + @Provides + fun provideObserveOutgoingCallUseCase( + callsScope: CallsScope + ): ObserveOutgoingCallUseCase = + callsScope.observeOutgoingCall + @ViewModelScoped @Provides fun provideStartCallUseCase(callsScope: CallsScope): StartCallUseCase = diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index 46c875fa29a..c4a5527da36 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -27,6 +27,7 @@ import com.wire.android.appLogger import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.data.call.Call 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 kotlinx.coroutines.CoroutineScope @@ -53,6 +54,7 @@ class CallNotificationManager @Inject constructor( private val notificationManager = NotificationManagerCompat.from(context) private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default()) private val incomingCallsForUsers = MutableStateFlow>>(emptyList()) + private val outgoingCallForUsers = MutableStateFlow>>(emptyList()) init { scope.launch { @@ -70,15 +72,34 @@ class CallNotificationManager @Inject constructor( } } } + scope.launch { + outgoingCallForUsers.collectLatest { + if (it.isEmpty()) { + hideOutgoingCallNotification() + } else { + it.first().let { (userId, call) -> + showOutgoingCallNotification(call.conversationId, userId, call.conversationName ?: "Name") + } + } + } + } } fun handleIncomingCallNotifications(calls: List, userId: UserId) { if (calls.isEmpty()) { - incomingCallsForUsers.update { it.filter { it.first != userId } } + incomingCallsForUsers.update { it.filter { + it.first != userId } } } else { incomingCallsForUsers.update { it.filter { it.first != userId } + (userId to calls.first()) } } } + fun handleOutgoingCallNotifications(calls: List, userId: UserId) { + if (calls.isEmpty()) { + outgoingCallForUsers.update { it.filter { it.first != userId } } + } else { + outgoingCallForUsers.update { it.filter { it.first != userId } + (userId to calls.first()) } + } + } fun hideAllNotifications() { hideIncomingCallNotification() @@ -94,13 +115,23 @@ class CallNotificationManager @Inject constructor( TimeUnit.MILLISECONDS.sleep(CANCEL_CALL_NOTIFICATION_DELAY) notificationManager.cancel(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID) } + private fun hideOutgoingCallNotification() { + appLogger.i("$TAG: hiding outgoing call") + notificationManager.cancel(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID) + } @VisibleForTesting internal fun showIncomingCallNotification(call: Call, userId: QualifiedID) { - appLogger.i("$TAG: showing incoming call for user ${userId.toLogString()}") + appLogger.i("$TAG: showing incoming call notification for user ${userId.toLogString()}") val notification = builder.getIncomingCallNotification(call, userId) notificationManager.notify(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID, notification) } + @VisibleForTesting + internal fun showOutgoingCallNotification(conversationId: ConversationId, userId: UserId, conversationName: String) { + appLogger.i("$TAG: showing outgoing call notification for user ${userId.toLogString()}") + val notification = builder.getOutgoingCallNotification(conversationId, userId, conversationName) + notificationManager.notify(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID, notification) + } // Notifications @@ -120,6 +151,44 @@ class CallNotificationManager @Inject constructor( class CallNotificationBuilder @Inject constructor( private val context: Context, ) { + + fun getOutgoingCallNotification( + conversationId: ConversationId, + userId: UserId, + conversationName: String + ): Notification { + val conversationIdString = conversationId.toString() + val userIdString = userId.toString() + val channelId = NotificationConstants.getOutgoingChannelId(userId) + + return NotificationCompat.Builder(context, channelId) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setSmallIcon(R.drawable.notification_icon_small) + .setContentTitle(conversationName) + .setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return)) + .setAutoCancel(false) + .setOngoing(true) + .setVibrate(VIBRATE_PATTERN) + .setFullScreenIntent( + outgoingCallPendingIntent( + context, + conversationIdString, + userIdString + ), true + ) + .addAction(getHangUpCallAction(context, conversationIdString, userId.toString())) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent( + outgoingCallPendingIntent( + context, + conversationIdString, + userIdString + ) + ) + .build() + } + fun getIncomingCallNotification(call: Call, userId: QualifiedID): Notification { val conversationIdString = call.conversationId.toString() val userIdString = userId.toString() @@ -133,16 +202,14 @@ class CallNotificationBuilder @Inject constructor( .setSmallIcon(R.drawable.notification_icon_small) .setContentTitle(title) .setContentText(content) - .setAutoCancel(true) + .setAutoCancel(false) .setOngoing(true) .setVibrate(VIBRATE_PATTERN) - .setTimeoutAfter(INCOMING_CALL_TIMEOUT) - .setFullScreenIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString, userIdString), true) + .setFullScreenIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString), true) .addAction(getDeclineCallAction(context, conversationIdString, userIdString)) .addAction(getOpenIncomingCallAction(context, conversationIdString, userIdString)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString, userIdString)) - .setDeleteIntent(declineCallPendingIntent(context, conversationIdString, userIdString)) + .setContentIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString)) .build() // Added FLAG_INSISTENT so the ringing sound repeats itself until an action is done. @@ -210,7 +277,6 @@ class CallNotificationBuilder @Inject constructor( } companion object { - private const val INCOMING_CALL_TIMEOUT: Long = 30 * 1000 private val VIBRATE_PATTERN = longArrayOf(0, 1000, 1000) } } diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt index c187e94eeac..0e168601983 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt @@ -51,7 +51,7 @@ fun getActionReply( fun getOpenIncomingCallAction(context: Context, conversationId: String, userId: String) = getAction( context.getString(R.string.notification_action_open_call), - fullScreenIncomingCallPendingIntent(context, conversationId, userId) + fullScreenIncomingCallPendingIntent(context, conversationId) ) fun getDeclineCallAction(context: Context, conversationId: String, userId: String) = getAction( diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt index 751cc4581bd..2e484bd6bc5 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationChannelsManager.kt @@ -45,6 +45,13 @@ class NotificationChannelsManager @Inject constructor( ) } + private val outgoingCallSoundUri by lazy { + Uri.parse( + "${ContentResolver.SCHEME_ANDROID_RESOURCE}://" + + "${context.packageName}/raw/ringing_from_me" + ) + } + private val knockSoundUri by lazy { Uri.parse( "${ContentResolver.SCHEME_ANDROID_RESOURCE}://" + @@ -64,6 +71,7 @@ class NotificationChannelsManager @Inject constructor( val groupId = createNotificationChannelGroup(user.id, user.handle ?: user.name ?: user.id.value) createIncomingCallsChannel(groupId, user.id) + createOutgoingCallChannel(groupId, user.id) createMessagesNotificationChannel(user.id, groupId) createPingNotificationChannel(user.id, groupId) } @@ -122,6 +130,26 @@ class NotificationChannelsManager @Inject constructor( notificationManagerCompat.createNotificationChannel(notificationChannel) } + private fun createOutgoingCallChannel(groupId: String, userId: UserId) { + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(getAudioAttributeUsageByOsLevel()) + .build() + + val channelId = NotificationConstants.getOutgoingChannelId(userId) + val notificationChannel = NotificationChannelCompat + .Builder(channelId, NotificationManagerCompat.IMPORTANCE_DEFAULT) + .setName(NotificationConstants.OUTGOING_CALL_CHANNEL_NAME) + .setImportance(NotificationManagerCompat.IMPORTANCE_DEFAULT) + .setSound(outgoingCallSoundUri, audioAttributes) + .setShowBadge(false) + .setVibrationPattern(VIBRATE_PATTERN) + .setGroup(groupId) + .build() + + notificationManagerCompat.createNotificationChannel(notificationChannel) + } + private fun createOngoingNotificationChannel() { val channelId = NotificationConstants.ONGOING_CALL_CHANNEL_ID val notificationChannel = NotificationChannelCompat diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt index 4ec6ca2adb6..0bd2bb3e2b7 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationConstants.kt @@ -24,7 +24,9 @@ import com.wire.kalium.logic.data.user.UserId object NotificationConstants { private const val INCOMING_CALL_CHANNEL_ID = "com.wire.android.notification_incoming_call_channel" + private const val OUTGOING_CALL_CHANNEL_ID = "com.wire.android.notification_outgoing_call_channel" const val INCOMING_CALL_CHANNEL_NAME = "Incoming calls" + const val OUTGOING_CALL_CHANNEL_NAME = "Outgoing call" const val ONGOING_CALL_CHANNEL_ID = "com.wire.android.notification_ongoing_call_channel" const val ONGOING_CALL_CHANNEL_NAME = "Ongoing calls" @@ -49,6 +51,7 @@ object NotificationConstants { // Notification IDs (has to be unique!) val CALL_INCOMING_NOTIFICATION_ID = "wire_incoming_call_notification".hashCode() + val CALL_OUTGOING_NOTIFICATION_ID = "wire_outgoing_call_notification".hashCode() val CALL_ONGOING_NOTIFICATION_ID = "wire_ongoing_call_notification".hashCode() val PERSISTENT_NOTIFICATION_ID = "wire_persistent_web_socket_notification".hashCode() val MESSAGE_SYNC_NOTIFICATION_ID = "wire_notification_fetch_notification".hashCode() @@ -66,6 +69,7 @@ object NotificationConstants { fun getMessagesChannelId(userId: UserId): String = getChanelIdForUser(userId, MESSAGE_CHANNEL_ID) 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) /** * @return NotificationChannelId [String] specific for user, use it to post a notifications. diff --git a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt index 2e207272458..984c7f2ff37 100644 --- a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt +++ b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt @@ -30,6 +30,7 @@ import com.wire.android.notification.broadcastreceivers.NotificationReplyReceive import com.wire.android.ui.WireActivity import com.wire.android.ui.calling.CallActivity import com.wire.android.ui.calling.CallScreenType +import com.wire.android.ui.calling.getIncomingCallIntent import com.wire.android.util.deeplink.DeepLinkProcessor fun messagePendingIntent(context: Context, conversationId: String, userId: String?): PendingIntent { @@ -116,8 +117,19 @@ fun declineCallPendingIntent(context: Context, conversationId: String, userId: S ) } -fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String, userId: String): PendingIntent { - val intent = openIncomingCallIntent(context, conversationId, userId) +fun outgoingCallPendingIntent(context: Context, conversationId: String, userId: String): PendingIntent { + val intent = openOutgoingCallIntent(context, conversationId, userId) + + return PendingIntent.getActivity( + context, + OUTGOING_CALL_REQUEST_CODE, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) +} + +fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String): PendingIntent { + val intent = getIncomingCallIntent(context, conversationId) return PendingIntent.getActivity( context, @@ -127,11 +139,10 @@ fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String ) } -private fun openIncomingCallIntent(context: Context, conversationId: String, userId: String) = +private fun openOutgoingCallIntent(context: Context, conversationId: String, userId: String) = Intent(context.applicationContext, CallActivity::class.java).apply { putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) - putExtra(CallActivity.EXTRA_USER_ID, userId) - putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Outgoing.name) } private fun openOngoingCallIntent(context: Context, conversationId: String) = @@ -176,6 +187,7 @@ private const val DECLINE_CALL_REQUEST_CODE = "decline_call_" private const val FULL_SCREEN_REQUEST_CODE = 3 private const val OPEN_ONGOING_CALL_REQUEST_CODE = 4 private const val OPEN_MIGRATION_LOGIN_REQUEST_CODE = 5 +private const val OUTGOING_CALL_REQUEST_CODE = 6 private const val END_ONGOING_CALL_REQUEST_CODE = "hang_up_call_" private const val OPEN_MESSAGE_REQUEST_CODE_PREFIX = "open_message_" private const val OPEN_OTHER_USER_PROFILE_CODE_PREFIX = "open_other_user_profile_" diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index 69f29465d36..e66dc0e6459 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -257,6 +257,9 @@ class WireNotificationManager @Inject constructor( incomingCallsJob = scope.launch(dispatcherProvider.default()) { observeIncomingCalls(userId) }, + outgoingCallJob = scope.launch(dispatcherProvider.default()) { + observeOutgoingCalls(userId) + }, messagesJob = scope.launch(dispatcherProvider.default()) { observeMessageNotifications(userId, currentScreenState) }, @@ -336,6 +339,15 @@ class WireNotificationManager @Inject constructor( } } + private suspend fun observeOutgoingCalls( + userId: UserId + ) { + appLogger.d("$TAG observing outgoing calls") + coreLogic.getSessionScope(userId).calls.observeOutgoingCall().collect { + callNotificationManager.handleOutgoingCallNotifications(it, userId) + } + } + /** * Infinitely listen for the new Message notifications and show it. * Can be used for listening for the Notifications when the app is running. @@ -489,16 +501,18 @@ class WireNotificationManager @Inject constructor( private data class UserObservingJobs( val currentScreenJob: Job, val incomingCallsJob: Job, + val outgoingCallJob: Job, val messagesJob: Job, ) { fun cancelAll() { currentScreenJob.cancel() incomingCallsJob.cancel() + outgoingCallJob.cancel() messagesJob.cancel() } fun isAllActive(): Boolean = - currentScreenJob.isActive && incomingCallsJob.isActive && messagesJob.isActive + currentScreenJob.isActive && incomingCallsJob.isActive && messagesJob.isActive && outgoingCallJob.isActive } private data class ObservingJobs( diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt index a1e5dae3b7f..13b72ee8cdc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -62,6 +62,8 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.NavigationGraph import com.wire.android.navigation.navigateToItem import com.wire.android.navigation.rememberNavigator +import com.wire.android.ui.calling.getIncomingCallIntent +import com.wire.android.ui.calling.getOutgoingCallIntent import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar @@ -214,6 +216,16 @@ class WireActivity : AppCompatActivity() { startActivity(this) } }, + onReturnToIncomingCallClick = { + getIncomingCallIntent(this@WireActivity, it.conversationId.toString()).run { + startActivity(this) + } + }, + onReturnToOutgoingCallClick = { + getOutgoingCallIntent(this@WireActivity, it.conversationId.toString()).run { + startActivity(this) + } + } ) CompositionLocalProvider(LocalNavigator provides navigator) { NavigationGraph( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 6dfd4a3333a..9c172719b29 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.calling import android.app.Activity +import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle @@ -39,8 +40,8 @@ import com.wire.android.notification.CallNotificationManager import com.wire.android.ui.AppLockActivity import com.wire.android.ui.LocalActivity import com.wire.android.ui.calling.incoming.IncomingCallScreen -import com.wire.android.ui.calling.initiating.InitiatingCallScreen import com.wire.android.ui.calling.ongoing.OngoingCallScreen +import com.wire.android.ui.calling.outgoing.OutgoingCallScreen import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.theme.WireTheme import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl @@ -61,8 +62,6 @@ class CallActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - callNotificationManager.hideAllNotifications() - setUpCallingFlags() appLogger.i("$TAG Initializing proximity sensor..") @@ -93,10 +92,14 @@ class CallActivity : AppCompatActivity() { ) { screenType -> conversationId?.let { when (screenType) { - CallScreenType.Initiating.name -> InitiatingCallScreen( - qualifiedIdMapper.fromStringToQualifiedID(it) - ) { - currentCallScreenType = CallScreenType.Ongoing.name + CallScreenType.Outgoing.name -> { + OutgoingCallScreen( + conversationId = qualifiedIdMapper.fromStringToQualifiedID( + it + ), + ) { + currentCallScreenType = CallScreenType.Ongoing.name + } } CallScreenType.Ongoing.name -> OngoingCallScreen( @@ -157,14 +160,20 @@ fun getOngoingCallIntent( putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Ongoing.name) } -fun getInitiatingCallIntent( +fun getOutgoingCallIntent( activity: Activity, conversationId: String ) = Intent(activity, CallActivity::class.java).apply { putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) - putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Initiating.name) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Outgoing.name) } +fun getIncomingCallIntent(context: Context, conversationId: String) = + Intent(context.applicationContext, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) + putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Incoming.name) + } + fun CallActivity.openAppLockActivity() { Intent(this, AppLockActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt index e5b43bf08ca..7a0671a3f73 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallScreenType.kt @@ -21,5 +21,5 @@ package com.wire.android.ui.calling enum class CallScreenType { Incoming, Ongoing, - Initiating + Outgoing } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt index a9b5baf3c37..ddcbef47553 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt @@ -18,7 +18,6 @@ package com.wire.android.ui.calling.common -import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -69,9 +68,12 @@ fun CallerDetails( protocolInfo: Conversation.ProtocolInfo?, mlsVerificationStatus: Conversation.VerificationStatus?, proteusVerificationStatus: Conversation.VerificationStatus?, + onMinimiseScreen: () -> Unit ) { Column( - modifier = Modifier.fillMaxSize().padding(top = dimensions().spacing32x), + modifier = Modifier + .fillMaxSize() + .padding(top = dimensions().spacing32x), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -82,7 +84,7 @@ fun CallerDetails( .align(Alignment.Start) .rotate(180f), onClick = { - Toast.makeText(context, "Not implemented yet =)", Toast.LENGTH_SHORT).show() + onMinimiseScreen() } ) { Image( @@ -92,7 +94,9 @@ fun CallerDetails( } if (isCbrEnabled) { Text( - text = stringResource(id = R.string.calling_constant_bit_rate_indication).uppercase(Locale.getDefault()), + text = stringResource(id = R.string.calling_constant_bit_rate_indication).uppercase( + Locale.getDefault() + ), color = colorsScheme().secondaryText, style = MaterialTheme.wireTypography.title03, ) @@ -133,7 +137,7 @@ fun CallerDetails( if (!isCameraOn && conversationType == ConversationType.OneOnOne) { UserProfileAvatar( avatarData = UserAvatarData(avatarAssetId), - size = dimensions().initiatingCallUserAvatarSize, + size = dimensions().outgoingCallUserAvatarSize, modifier = Modifier.padding(top = dimensions().spacing16x) ) } @@ -154,6 +158,7 @@ fun PreviewCallerDetails() { callingLabel = String.EMPTY, protocolInfo = null, mlsVerificationStatus = null, - proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED + proteusVerificationStatus = Conversation.VerificationStatus.VERIFIED, + onMinimiseScreen = { } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/AcceptButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/AcceptButton.kt index 96911bb150e..0d067e117af 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/AcceptButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/AcceptButton.kt @@ -33,7 +33,7 @@ import com.wire.android.ui.common.dimensions @Composable fun AcceptButton( - modifier: Modifier = Modifier.size(dimensions().initiatingCallHangUpButtonSize), + modifier: Modifier = Modifier.size(dimensions().outgoingCallHangUpButtonSize), buttonClicked: () -> Unit ) { WirePrimaryButton( diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt index d7daa5c6cee..c4f99f6dc69 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt @@ -38,13 +38,13 @@ import com.wire.android.ui.common.dimensions @Composable fun DeclineButton(buttonClicked: () -> Unit) { IconButton( - modifier = Modifier.size(dimensions().initiatingCallHangUpButtonSize), + modifier = Modifier.size(dimensions().outgoingCallHangUpButtonSize), onClick = { } ) { Icon( modifier = Modifier .clickable(interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().initiatingCallHangUpButtonSize / 2), + indication = rememberRipple(bounded = false, radius = dimensions().outgoingCallHangUpButtonSize / 2), role = Role.Button, onClick = { buttonClicked() }), painter = painterResource(id = R.drawable.ic_decline), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt index 5754b9195b7..4a8b0353503 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt @@ -60,6 +60,6 @@ fun HangUpButton( @Composable fun PreviewComposableHangUpButton() { HangUpButton(modifier = Modifier - .width(MaterialTheme.wireDimensions.initiatingCallHangUpButtonSize) - .height(MaterialTheme.wireDimensions.initiatingCallHangUpButtonSize), onHangUpButtonClicked = { }) + .width(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize) + .height(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize), onHangUpButtonClicked = { }) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt index 6968d6dd00d..b1bff409bf5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/incoming/IncomingCallScreen.kt @@ -148,6 +148,9 @@ fun IncomingCallScreen( ) ) } + }, + onMinimiseScreen = { + activity.finish() } ) } @@ -169,7 +172,8 @@ private fun IncomingCallContent( acceptCall: () -> Unit, onVideoPreviewCreated: (view: View) -> Unit, onSelfClearVideoPreview: () -> Unit, - onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit + onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit, + onMinimiseScreen: () -> Unit ) { BackHandler { // DO NOTHING @@ -205,7 +209,7 @@ private fun IncomingCallContent( modifier = Modifier.align(alignment = Alignment.CenterStart) ) { HangUpButton( - modifier = Modifier.size(dimensions().initiatingCallHangUpButtonSize), + modifier = Modifier.size(dimensions().outgoingCallHangUpButtonSize), onHangUpButtonClicked = { declineCall() } ) Text( @@ -260,7 +264,8 @@ private fun IncomingCallContent( callingLabel = isCallingString, protocolInfo = callState.protocolInfo, mlsVerificationStatus = callState.mlsVerificationStatus, - proteusVerificationStatus = callState.proteusVerificationStatus + proteusVerificationStatus = callState.proteusVerificationStatus, + onMinimiseScreen = onMinimiseScreen ) } } @@ -292,5 +297,6 @@ fun PreviewIncomingCallScreen() { onVideoPreviewCreated = { }, onSelfClearVideoPreview = { }, onPermissionPermanentlyDenied = { }, + onMinimiseScreen = { } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt similarity index 85% rename from app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt rename to app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt index 1bf0dc8da96..45862ae746c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt @@ -16,7 +16,7 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.ui.calling.initiating +package com.wire.android.ui.calling.outgoing import android.view.View import androidx.activity.compose.BackHandler @@ -57,12 +57,12 @@ import com.wire.kalium.logic.data.id.ConversationId @Suppress("ParameterWrapping") @Composable -fun InitiatingCallScreen( +fun OutgoingCallScreen( conversationId: ConversationId, sharedCallingViewModel: SharedCallingViewModel = hiltViewModel( creationCallback = { factory -> factory.create(conversationId = conversationId) } ), - initiatingCallViewModel: InitiatingCallViewModel = hiltViewModel( + outgoingCallViewModel: OutgoingCallViewModel = hiltViewModel( creationCallback = { factory -> factory.create(conversationId = conversationId) } ), onCallAccepted: () -> Unit @@ -72,27 +72,27 @@ fun InitiatingCallScreen( val activity = LocalActivity.current - LaunchedEffect(initiatingCallViewModel.state.flowState) { - when (initiatingCallViewModel.state.flowState) { - InitiatingCallState.FlowState.CallClosed -> { + LaunchedEffect(outgoingCallViewModel.state.flowState) { + when (outgoingCallViewModel.state.flowState) { + OutgoingCallState.FlowState.CallClosed -> { activity.finish() } - InitiatingCallState.FlowState.CallEstablished -> { + OutgoingCallState.FlowState.CallEstablished -> { onCallAccepted() } - InitiatingCallState.FlowState.Default -> { /* do nothing */ + OutgoingCallState.FlowState.Default -> { /* do nothing */ } } } with(sharedCallingViewModel) { - InitiatingCallContent( + OutgoingCallContent( callState = callState, toggleMute = ::toggleMute, toggleSpeaker = ::toggleSpeaker, toggleVideo = ::toggleVideo, - onHangUpCall = initiatingCallViewModel::hangUpCall, + onHangUpCall = outgoingCallViewModel::hangUpCall, onVideoPreviewCreated = ::setVideoPreview, onSelfClearVideoPreview = ::clearVideoPreview, onPermissionPermanentlyDenied = { @@ -104,6 +104,9 @@ fun InitiatingCallScreen( ) ) } + }, + onMinimiseScreen = { + activity.finish() } ) } @@ -116,7 +119,7 @@ fun InitiatingCallScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun InitiatingCallContent( +private fun OutgoingCallContent( callState: CallState, toggleMute: () -> Unit, toggleSpeaker: () -> Unit, @@ -124,7 +127,8 @@ private fun InitiatingCallContent( onHangUpCall: () -> Unit, onVideoPreviewCreated: (view: View) -> Unit, onSelfClearVideoPreview: () -> Unit, - onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit + onPermissionPermanentlyDenied: (type: PermissionDenialType) -> Unit, + onMinimiseScreen: () -> Unit ) { BackHandler { // DO NOTHING @@ -136,7 +140,7 @@ private fun InitiatingCallContent( sheetDragHandle = null, scaffoldState = scaffoldState, sheetSwipeEnabled = false, - sheetPeekHeight = dimensions().defaultInitiatingCallSheetPeekHeight, + sheetPeekHeight = dimensions().defaultOutgoingCallSheetPeekHeight, sheetContent = { Column( modifier = Modifier.fillMaxSize(), @@ -158,8 +162,8 @@ private fun InitiatingCallContent( ) HangUpButton( modifier = Modifier - .width(MaterialTheme.wireDimensions.initiatingCallHangUpButtonSize) - .height(MaterialTheme.wireDimensions.initiatingCallHangUpButtonSize), + .width(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize) + .height(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize), onHangUpButtonClicked = onHangUpCall ) } @@ -183,6 +187,7 @@ private fun InitiatingCallContent( protocolInfo = callState.protocolInfo, mlsVerificationStatus = callState.mlsVerificationStatus, proteusVerificationStatus = callState.proteusVerificationStatus, + onMinimiseScreen = onMinimiseScreen ) } } @@ -190,6 +195,6 @@ private fun InitiatingCallContent( @Preview @Composable -fun PreviewInitiatingCallScreen() { - InitiatingCallContent(CallState(ConversationId("value", "domain")), {}, {}, {}, {}, {}, {}, {}) +fun PreviewOutgoingCallScreen() { + OutgoingCallContent(CallState(ConversationId("value", "domain")), {}, {}, {}, {}, {}, {}, {}, {}) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallState.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt similarity index 79% rename from app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallState.kt rename to app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt index f418dce6e25..98a89210eb3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt @@ -15,14 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.ui.calling.initiating +package com.wire.android.ui.calling.outgoing -data class InitiatingCallState( +data class OutgoingCallState( val flowState: FlowState = FlowState.Default ) { sealed interface FlowState { - object Default : FlowState - object CallClosed : FlowState - object CallEstablished : FlowState + data object Default : FlowState + data object CallClosed : FlowState + data object CallEstablished : FlowState } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt similarity index 71% rename from app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt rename to app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt index 45f5a109c30..b7073910165 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt @@ -16,7 +16,7 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.ui.calling.initiating +package com.wire.android.ui.calling.outgoing import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -29,21 +29,25 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import org.jetbrains.annotations.VisibleForTesting import java.util.Calendar @Suppress("LongParameterList") -@HiltViewModel(assistedFactory = InitiatingCallViewModel.Factory::class) -class InitiatingCallViewModel @AssistedInject constructor( +@HiltViewModel(assistedFactory = OutgoingCallViewModel.Factory::class) +class OutgoingCallViewModel @AssistedInject constructor( @Assisted val conversationId: ConversationId, private val observeEstablishedCalls: ObserveEstablishedCallsUseCase, + private val observeOutgoingCall: ObserveOutgoingCallUseCase, private val startCall: StartCallUseCase, private val endCall: EndCallUseCase, private val isLastCallClosed: IsLastCallClosedUseCase, @@ -53,7 +57,7 @@ class InitiatingCallViewModel @AssistedInject constructor( private val callStartTime: Long = Calendar.getInstance().timeInMillis private var wasCallHangUp: Boolean = false - var state by mutableStateOf(InitiatingCallState()) + var state by mutableStateOf(OutgoingCallState()) private set init { @@ -84,7 +88,7 @@ class InitiatingCallViewModel @AssistedInject constructor( ).collect { isCurrentCallClosed -> if (isCurrentCallClosed && wasCallHangUp.not()) { stopRingerAndMarkCallAsHungUp() - state = state.copy(flowState = InitiatingCallState.FlowState.CallClosed) + state = state.copy(flowState = OutgoingCallState.FlowState.CallClosed) } } } @@ -96,20 +100,25 @@ class InitiatingCallViewModel @AssistedInject constructor( private fun onCallEstablished() { callRinger.ring(R.raw.ready_to_talk, isLooping = false, isIncomingCall = false) - state = state.copy(flowState = InitiatingCallState.FlowState.CallEstablished) + state = state.copy(flowState = OutgoingCallState.FlowState.CallEstablished) } - internal suspend fun initiateCall() { - val result = startCall( - conversationId = conversationId - ) - when (result) { - StartCallUseCase.Result.Success -> callRinger.ring( - resource = R.raw.ringing_from_me, - isIncomingCall = false - ) + @VisibleForTesting + suspend fun initiateCall() { + observeOutgoingCall().first().run { + if (isEmpty()) { + val result = startCall( + conversationId = conversationId + ) + when (result) { + StartCallUseCase.Result.Success -> callRinger.ring( + resource = R.raw.ringing_from_me, + isIncomingCall = false + ) - StartCallUseCase.Result.SyncFailure -> {} // TODO: handle case where start call fails + StartCallUseCase.Result.SyncFailure -> {} // TODO: handle case where start call fails + } + } } } @@ -117,12 +126,12 @@ class InitiatingCallViewModel @AssistedInject constructor( launch { endCall(conversationId) } launch { stopRingerAndMarkCallAsHungUp() - state = state.copy(flowState = InitiatingCallState.FlowState.CallClosed) + state = state.copy(flowState = OutgoingCallState.FlowState.CallClosed) } } @AssistedFactory interface Factory { - fun create(conversationId: ConversationId): InitiatingCallViewModel + fun create(conversationId: ConversationId): OutgoingCallViewModel } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt index 9dfc96331c0..9ffac1380c2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt @@ -56,11 +56,15 @@ import com.wire.kalium.logic.data.id.ConversationId fun CommonTopAppBar( commonTopAppBarState: CommonTopAppBarState, onReturnToCallClick: (ConnectivityUIState.EstablishedCall) -> Unit, + onReturnToIncomingCallClick: (ConnectivityUIState.IncomingCall) -> Unit, + onReturnToOutgoingCallClick: (ConnectivityUIState.OutgoingCall) -> Unit ) { Column { ConnectivityStatusBar( connectivityInfo = commonTopAppBarState.connectivityState, - onReturnToCallClick = onReturnToCallClick + onReturnToCallClick = onReturnToCallClick, + onReturnToIncomingCallClick = onReturnToIncomingCallClick, + onReturnToOutgoingCallClick = onReturnToOutgoingCallClick ) } } @@ -68,11 +72,15 @@ fun CommonTopAppBar( @Composable private fun ConnectivityStatusBar( connectivityInfo: ConnectivityUIState, - onReturnToCallClick: (ConnectivityUIState.EstablishedCall) -> Unit + onReturnToCallClick: (ConnectivityUIState.EstablishedCall) -> Unit, + onReturnToIncomingCallClick: (ConnectivityUIState.IncomingCall) -> Unit, + onReturnToOutgoingCallClick: (ConnectivityUIState.OutgoingCall) -> Unit ) { val isVisible = connectivityInfo !is ConnectivityUIState.None val backgroundColor = when (connectivityInfo) { - is ConnectivityUIState.EstablishedCall -> MaterialTheme.wireColorScheme.positive + is ConnectivityUIState.EstablishedCall, + is ConnectivityUIState.IncomingCall, + is ConnectivityUIState.OutgoingCall -> MaterialTheme.wireColorScheme.positive ConnectivityUIState.Connecting, ConnectivityUIState.WaitingConnection -> MaterialTheme.wireColorScheme.primary ConnectivityUIState.None -> MaterialTheme.wireColorScheme.background } @@ -94,9 +102,15 @@ private fun ConnectivityStatusBar( .height(MaterialTheme.wireDimensions.ongoingCallLabelHeight) .background(backgroundColor) .run { - if (connectivityInfo is ConnectivityUIState.EstablishedCall) { - clickable(onClick = { onReturnToCallClick(connectivityInfo) }) - } else this + when(connectivityInfo) { + is ConnectivityUIState.EstablishedCall -> + clickable(onClick = { onReturnToCallClick(connectivityInfo) }) + is ConnectivityUIState.IncomingCall -> + clickable(onClick = { onReturnToIncomingCallClick(connectivityInfo) }) + is ConnectivityUIState.OutgoingCall -> + clickable(onClick = { onReturnToOutgoingCallClick(connectivityInfo) }) + else -> this + } } AnimatedVisibility( @@ -112,10 +126,25 @@ private fun ConnectivityStatusBar( when (connectivityInfo) { is ConnectivityUIState.EstablishedCall -> OngoingCallContent(connectivityInfo.isMuted) + + is ConnectivityUIState.IncomingCall -> + IncomingCallContent(callerName = connectivityInfo.callerName) + + is ConnectivityUIState.OutgoingCall -> + OutgoingCallContent(callerName = connectivityInfo.callerName) + ConnectivityUIState.Connecting -> - StatusLabel(R.string.connectivity_status_bar_connecting, MaterialTheme.wireColorScheme.onPrimary) + StatusLabel( + R.string.connectivity_status_bar_connecting, + MaterialTheme.wireColorScheme.onPrimary + ) + ConnectivityUIState.WaitingConnection -> - StatusLabel(R.string.connectivity_status_bar_waiting_for_network, MaterialTheme.wireColorScheme.onPrimary) + StatusLabel( + R.string.connectivity_status_bar_waiting_for_network, + MaterialTheme.wireColorScheme.onPrimary + ) + ConnectivityUIState.None -> {} } } @@ -127,12 +156,39 @@ private fun OngoingCallContent(isMuted: Boolean) { Row { MicrophoneIcon(isMuted, MaterialTheme.wireColorScheme.onPositive) CameraIcon(MaterialTheme.wireColorScheme.onPositive) - StatusLabel(R.string.connectivity_status_bar_return_to_call, MaterialTheme.wireColorScheme.onPositive) + StatusLabel( + R.string.connectivity_status_bar_return_to_call, + MaterialTheme.wireColorScheme.onPositive + ) + } +} + +@Composable +private fun IncomingCallContent(callerName: String?) { + Row { + StatusLabelWithValue( + stringResource = R.string.connectivity_status_bar_return_to_incoming_call, + callerName = callerName, + color = MaterialTheme.wireColorScheme.onPositive + ) + } +} +@Composable +private fun OutgoingCallContent(callerName: String?) { + Row { + StatusLabelWithValue( + stringResource = R.string.connectivity_status_bar_return_to_outgoing_call, + callerName = callerName, + color = MaterialTheme.wireColorScheme.onPositive + ) } } @Composable -private fun StatusLabel(stringResource: Int, color: Color = MaterialTheme.wireColorScheme.onPrimary) { +private fun StatusLabel( + stringResource: Int, + color: Color = MaterialTheme.wireColorScheme.onPrimary +) { Text( text = stringResource(id = stringResource).uppercase(), color = color, @@ -140,6 +196,20 @@ private fun StatusLabel(stringResource: Int, color: Color = MaterialTheme.wireCo ) } +@Composable +private fun StatusLabelWithValue( + stringResource: Int, + callerName: String?, + color: Color = MaterialTheme.wireColorScheme.onPrimary +) { + val defaultCallerName = stringResource(R.string.calling_participant_tile_default_user_name) + Text( + text = stringResource(id = stringResource, callerName ?: defaultCallerName).uppercase(), + color = color, + style = MaterialTheme.wireTypography.title03, + ) +} + @Composable fun StatusLabel(message: String, color: Color = MaterialTheme.wireColorScheme.onPrimary) { Text( @@ -164,7 +234,10 @@ private fun CameraIcon(tint: Color = MaterialTheme.wireColorScheme.onPositive) { } @Composable -private fun MicrophoneIcon(isMuted: Boolean, tint: Color = MaterialTheme.wireColorScheme.onPositive) { +private fun MicrophoneIcon( + isMuted: Boolean, + tint: Color = MaterialTheme.wireColorScheme.onPositive +) { Icon( painter = painterResource( id = if (isMuted) R.drawable.ic_microphone_white_muted @@ -192,14 +265,19 @@ private fun clearStatusBarColor() { @Composable private fun PreviewCommonTopAppBar(connectivityUIState: ConnectivityUIState) { WireTheme { - CommonTopAppBar(CommonTopAppBarState(connectivityUIState), {}) + CommonTopAppBar(CommonTopAppBarState(connectivityUIState), {}, {}, {}) } } @PreviewMultipleThemes @Composable fun PreviewCommonTopAppBar_ConnectivityCallNotMuted() = - PreviewCommonTopAppBar(ConnectivityUIState.EstablishedCall(ConversationId("what", "ever"), false)) + PreviewCommonTopAppBar( + ConnectivityUIState.EstablishedCall( + ConversationId("what", "ever"), + false + ) + ) @PreviewMultipleThemes @Composable diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index 0e5f5b0123a..22a2b94e3ae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -28,6 +28,7 @@ import com.wire.android.util.CurrentScreen import com.wire.android.util.CurrentScreenManager 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.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.session.CurrentSessionResult @@ -66,9 +67,15 @@ class CommonTopAppBarViewModel @Inject constructor( } private suspend fun activeCallFlow(userId: UserId): Flow = coreLogic.sessionScope(userId) { - calls.establishedCall().distinctUntilChanged().map { calls -> + combine( + calls.establishedCall(), + calls.getIncomingCalls(), + calls.observeOutgoingCall(), + ) { establishedCall, incomingCalls, outgoingCalls -> + incomingCalls + outgoingCalls + establishedCall + }.map { calls -> calls.firstOrNull() - } + }.distinctUntilChanged() } init { @@ -116,7 +123,13 @@ class CommonTopAppBarViewModel @Inject constructor( val canDisplayConnectivityIssues = currentScreen !is CurrentScreen.AuthRelated if (activeCall != null) { - return ConnectivityUIState.EstablishedCall(activeCall.conversationId, activeCall.isMuted) + return if(activeCall.status == CallStatus.INCOMING) { + ConnectivityUIState.IncomingCall(activeCall.conversationId, activeCall.callerName) + } else if(activeCall.status == CallStatus.STARTED) { + ConnectivityUIState.OutgoingCall(activeCall.conversationId, activeCall.callerName) + } else { + ConnectivityUIState.EstablishedCall(activeCall.conversationId, activeCall.isMuted) + } } return if (canDisplayConnectivityIssues) { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/ConnectivityUIState.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/ConnectivityUIState.kt index f6d29fe6aab..bb5c56e7e6f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/ConnectivityUIState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/ConnectivityUIState.kt @@ -33,4 +33,14 @@ sealed interface ConnectivityUIState { val conversationId: ConversationId, val isMuted: Boolean ) : ConnectivityUIState + + data class IncomingCall( + val conversationId: ConversationId, + val callerName: String? + ) : ConnectivityUIState + + data class OutgoingCall( + val conversationId: ConversationId, + val callerName: String? + ) : ConnectivityUIState } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 243f645527e..06e54bde0e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -82,7 +82,7 @@ import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.LocalActivity -import com.wire.android.ui.calling.getInitiatingCallIntent +import com.wire.android.ui.calling.getOutgoingCallIntent import com.wire.android.ui.calling.getOngoingCallIntent import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout @@ -266,7 +266,7 @@ fun ConversationScreen( ConversationScreenDialogType.ONGOING_ACTIVE_CALL -> { OngoingActiveCallDialog(onJoinAnyways = { conversationCallViewModel.endEstablishedCallIfAny { - getInitiatingCallIntent(activity, conversationCallViewModel.conversationId.toString()).run { + getOutgoingCallIntent(activity, conversationCallViewModel.conversationId.toString()).run { activity.startActivity(this) } } @@ -291,8 +291,8 @@ fun ConversationScreen( showDialog, coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, - onOpenInitiatingCallScreen = { - getInitiatingCallIntent(activity, it.toString()).run { + onOpenOutgoingCallScreen = { + getOutgoingCallIntent(activity, it.toString()).run { activity.startActivity(this) } } @@ -323,8 +323,8 @@ fun ConversationScreen( showDialog, coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, - onOpenInitiatingCallScreen = { - getInitiatingCallIntent(activity, it.toString()).run { + onOpenOutgoingCallScreen = { + getOutgoingCallIntent(activity, it.toString()).run { activity.startActivity(this) } } @@ -397,8 +397,8 @@ fun ConversationScreen( showDialog, coroutineScope, conversationInfoViewModel.conversationInfoViewState.conversationType, - onOpenInitiatingCallScreen = { - getInitiatingCallIntent(activity, it.toString()).run { + onOpenOutgoingCallScreen = { + getOutgoingCallIntent(activity, it.toString()).run { activity.startActivity(this) } } @@ -623,7 +623,7 @@ private fun startCallIfPossible( showDialog: MutableState, coroutineScope: CoroutineScope, conversationType: Conversation.Type, - onOpenInitiatingCallScreen: (ConversationId) -> Unit, + onOpenOutgoingCallScreen: (ConversationId) -> Unit, onOpenOngoingCallScreen: (ConversationId) -> Unit ) { coroutineScope.launch { @@ -641,7 +641,7 @@ private fun startCallIfPossible( ConversationScreenDialogType.CALL_CONFIRMATION } else { conversationCallViewModel.endEstablishedCallIfAny { - onOpenInitiatingCallScreen(conversationCallViewModel.conversationId) + onOpenOutgoingCallScreen(conversationCallViewModel.conversationId) } ConversationScreenDialogType.NONE } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbf1236e7a2..563036e9943 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -826,6 +826,7 @@ %s calling... Someone Incoming group call + Tap to return to call - Calling... Constant Bit Rate Microphone @@ -860,6 +861,8 @@ Default Return to call + %1$s is calling… + Calling %1$s… Decrypting messages Waiting for network diff --git a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelTest.kt similarity index 67% rename from app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt rename to app/src/test/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelTest.kt index 9f44e203da9..cc14c35abfa 100644 --- a/app/src/test/kotlin/com/wire/android/ui/calling/initiating/InitiatingCallViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModelTest.kt @@ -16,15 +16,20 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.ui.calling.initiating +package com.wire.android.ui.calling.outgoing import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension import com.wire.android.media.CallRinger +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.user.UserId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.call.usecase.StartCallUseCase import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -42,13 +47,14 @@ import org.junit.jupiter.api.extension.ExtendWith @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(NavigationTestExtension::class) @ExtendWith(CoroutineTestExtension::class) -class InitiatingCallViewModelTest { +class OutgoingCallViewModelTest { @Test fun `given an outgoing call, when the user ends call, then invoke endCall useCase and close the screen`() = runTest { // Given val (arrangement, viewModel) = Arrangement() + .withObserveOutgoingCallReturning(listOf(call)) .withEndingCall() .withStartCallSucceeding() .arrange() @@ -62,7 +68,7 @@ class InitiatingCallViewModelTest { coVerify(exactly = 1) { endCall(any()) } coVerify(exactly = 1) { callRinger.stop() } } - assertTrue { viewModel.state.flowState is InitiatingCallState.FlowState.CallClosed } + assertTrue { viewModel.state.flowState is OutgoingCallState.FlowState.CallClosed } } @Test @@ -70,6 +76,7 @@ class InitiatingCallViewModelTest { runTest { // Given val (arrangement, viewModel) = Arrangement() + .withObserveOutgoingCallReturning(listOf(call)) .withNoInternetConnection() .withStartCallSucceeding() .arrange() @@ -83,16 +90,37 @@ class InitiatingCallViewModelTest { } } + @Test + fun `given user already started a call, when user tries to start another call, then do not initiat a new one`() = + runTest { + + // Given + val (arrangement, viewModel) = Arrangement() + .withObserveOutgoingCallReturning(listOf(call)) + .arrange() + + // When + viewModel.initiateCall() + + // Then + with(arrangement) { + coVerify(inverse = true) { startCall(any()) } + } + } + private class Arrangement { @MockK private lateinit var establishedCalls: ObserveEstablishedCallsUseCase + @MockK + private lateinit var observeOutgoingCall: ObserveOutgoingCallUseCase + @MockK private lateinit var isLastCallClosed: IsLastCallClosedUseCase @MockK - private lateinit var startCall: StartCallUseCase + lateinit var startCall: StartCallUseCase @MockK lateinit var callRinger: CallRinger @@ -102,10 +130,11 @@ class InitiatingCallViewModelTest { val dummyConversationId = ConversationId("some-dummy-value", "some.dummy.domain") - val initiatingCallViewModel by lazy { - InitiatingCallViewModel( + val outgoingCallViewModel by lazy { + OutgoingCallViewModel( conversationId = dummyConversationId, observeEstablishedCalls = establishedCalls, + observeOutgoingCall = observeOutgoingCall, startCall = startCall, endCall = endCall, isLastCallClosed = isLastCallClosed, @@ -134,6 +163,27 @@ class InitiatingCallViewModelTest { coEvery { startCall(any(), any()) } returns StartCallUseCase.Result.Success } - fun arrange() = this to initiatingCallViewModel + fun withObserveOutgoingCallReturning(calls: List) = apply { + coEvery { observeOutgoingCall() } returns flowOf(calls) + } + + fun arrange() = this to outgoingCallViewModel + } + + companion object { + val call = Call( + conversationId = ConversationId("caller", "domain"), + status = CallStatus.STARTED, + callerId = UserId("caller", "domain").toString(), + participants = listOf(), + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + maxParticipants = 0, + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername", + callerTeamName = "team_1" + ) } } diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt index a4883ec6252..35cc799c451 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireDimensions.kt @@ -176,14 +176,14 @@ data class WireDimensions( val defaultCallingControlsSize: Dp, val defaultCallingHangUpButtonSize: Dp, val defaultSheetPeekHeight: Dp, - val defaultInitiatingCallSheetPeekHeight: Dp, + val defaultOutgoingCallSheetPeekHeight: Dp, val onGoingCallUserAvatarSize: Dp, val onGoingCallUserAvatarMinimizedSize: Dp, val onGoingCallTileUsernameMaxWidth: Dp, - val initiatingCallUserAvatarSize: Dp, + val outgoingCallUserAvatarSize: Dp, val defaultIncomingCallSheetPeekHeight: Dp, val callingIncomingUserAvatarSize: Dp, - val initiatingCallHangUpButtonSize: Dp, + val outgoingCallHangUpButtonSize: Dp, val ongoingCallLabelHeight: Dp, // Message item val messageItemBottomPadding: Dp, @@ -324,14 +324,14 @@ private val DefaultPhonePortraitWireDimensions: WireDimensions = WireDimensions( defaultCallingControlsSize = 56.dp, defaultCallingHangUpButtonSize = 56.dp, defaultSheetPeekHeight = 100.dp, - defaultInitiatingCallSheetPeekHeight = 281.dp, + defaultOutgoingCallSheetPeekHeight = 281.dp, onGoingCallUserAvatarSize = 90.dp, onGoingCallUserAvatarMinimizedSize = 60.dp, onGoingCallTileUsernameMaxWidth = 120.dp, - initiatingCallUserAvatarSize = 128.dp, + outgoingCallUserAvatarSize = 128.dp, defaultIncomingCallSheetPeekHeight = 280.dp, callingIncomingUserAvatarSize = 128.dp, - initiatingCallHangUpButtonSize = 72.dp, + outgoingCallHangUpButtonSize = 72.dp, messageItemBottomPadding = 6.dp, messageItemHorizontalPadding = 12.dp, conversationOptionsItemMinHeight = 57.dp, From ae1b4e8fda51d0bc44cab954009587aa29bf782d Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 18 Apr 2024 14:51:04 +0200 Subject: [PATCH 14/29] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 9c9cc58bb7c..60b7aa19470 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 9c9cc58bb7c757125148a11b9649ada2ad52d297 +Subproject commit 60b7aa1947004cc9a7a2c84209b8ba3c802c3105 From caf3cdb54ad4a2ffcc29081b81a12b1023821aba Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 18 Apr 2024 17:43:35 +0200 Subject: [PATCH 15/29] chore: detekt --- .../notification/CallNotificationManager.kt | 37 +++++++++----- .../notification/NotificationActions.kt | 2 +- .../android/notification/PendingIntents.kt | 6 +-- .../ui/calling/common/CallerDetails.kt | 2 - .../calling/controlbuttons/DeclineButton.kt | 11 +++-- .../ui/calling/controlbuttons/HangUpButton.kt | 9 ++-- .../ui/calling/outgoing/OutgoingCallScreen.kt | 3 +- .../ui/calling/outgoing/OutgoingCallState.kt | 28 +++++++++++ .../ui/common/topappbar/CommonTopAppBar.kt | 25 +++++++--- .../topappbar/CommonTopAppBarViewModel.kt | 49 ++++++++++--------- 10 files changed, 115 insertions(+), 57 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index c4a5527da36..d9240012287 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -87,8 +87,11 @@ class CallNotificationManager @Inject constructor( fun handleIncomingCallNotifications(calls: List, userId: UserId) { if (calls.isEmpty()) { - incomingCallsForUsers.update { it.filter { - it.first != userId } } + incomingCallsForUsers.update { + it.filter { + it.first != userId + } + } } else { incomingCallsForUsers.update { it.filter { it.first != userId } + (userId to calls.first()) } } @@ -124,13 +127,25 @@ class CallNotificationManager @Inject constructor( internal fun showIncomingCallNotification(call: Call, userId: QualifiedID) { appLogger.i("$TAG: showing incoming call notification for user ${userId.toLogString()}") val notification = builder.getIncomingCallNotification(call, userId) - notificationManager.notify(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID, notification) + notificationManager.notify( + NotificationConstants.CALL_INCOMING_NOTIFICATION_ID, + notification + ) } + @VisibleForTesting - internal fun showOutgoingCallNotification(conversationId: ConversationId, userId: UserId, conversationName: String) { + internal fun showOutgoingCallNotification( + conversationId: ConversationId, + userId: UserId, + conversationName: String + ) { appLogger.i("$TAG: showing outgoing call notification for user ${userId.toLogString()}") - val notification = builder.getOutgoingCallNotification(conversationId, userId, conversationName) - notificationManager.notify(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID, notification) + val notification = + builder.getOutgoingCallNotification(conversationId, userId, conversationName) + notificationManager.notify( + NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID, + notification + ) } // Notifications @@ -171,11 +186,8 @@ class CallNotificationBuilder @Inject constructor( .setOngoing(true) .setVibrate(VIBRATE_PATTERN) .setFullScreenIntent( - outgoingCallPendingIntent( - context, - conversationIdString, - userIdString - ), true + outgoingCallPendingIntent(context, conversationIdString), + true ) .addAction(getHangUpCallAction(context, conversationIdString, userId.toString())) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) @@ -183,7 +195,6 @@ class CallNotificationBuilder @Inject constructor( outgoingCallPendingIntent( context, conversationIdString, - userIdString ) ) .build() @@ -207,7 +218,7 @@ class CallNotificationBuilder @Inject constructor( .setVibrate(VIBRATE_PATTERN) .setFullScreenIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString), true) .addAction(getDeclineCallAction(context, conversationIdString, userIdString)) - .addAction(getOpenIncomingCallAction(context, conversationIdString, userIdString)) + .addAction(getOpenIncomingCallAction(context, conversationIdString)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentIntent(fullScreenIncomingCallPendingIntent(context, conversationIdString)) .build() diff --git a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt index 0e168601983..427acfcd664 100644 --- a/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt +++ b/app/src/main/kotlin/com/wire/android/notification/NotificationActions.kt @@ -49,7 +49,7 @@ fun getActionReply( } } -fun getOpenIncomingCallAction(context: Context, conversationId: String, userId: String) = getAction( +fun getOpenIncomingCallAction(context: Context, conversationId: String) = getAction( context.getString(R.string.notification_action_open_call), fullScreenIncomingCallPendingIntent(context, conversationId) ) diff --git a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt index 984c7f2ff37..f8eca9ef80e 100644 --- a/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt +++ b/app/src/main/kotlin/com/wire/android/notification/PendingIntents.kt @@ -117,8 +117,8 @@ fun declineCallPendingIntent(context: Context, conversationId: String, userId: S ) } -fun outgoingCallPendingIntent(context: Context, conversationId: String, userId: String): PendingIntent { - val intent = openOutgoingCallIntent(context, conversationId, userId) +fun outgoingCallPendingIntent(context: Context, conversationId: String): PendingIntent { + val intent = openOutgoingCallIntent(context, conversationId) return PendingIntent.getActivity( context, @@ -139,7 +139,7 @@ fun fullScreenIncomingCallPendingIntent(context: Context, conversationId: String ) } -private fun openOutgoingCallIntent(context: Context, conversationId: String, userId: String) = +private fun openOutgoingCallIntent(context: Context, conversationId: String) = Intent(context.applicationContext, CallActivity::class.java).apply { putExtra(CallActivity.EXTRA_CONVERSATION_ID, conversationId) putExtra(CallActivity.EXTRA_SCREEN_TYPE, CallScreenType.Outgoing.name) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt index 6d15b96ab8b..ae1f2295dbd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/common/CallerDetails.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -75,7 +74,6 @@ fun CallerDetails( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { - val context = LocalContext.current IconButton( modifier = Modifier .padding(top = dimensions().spacing16x, start = dimensions().spacing6x) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt index c4f99f6dc69..d3a34db51bd 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt @@ -43,10 +43,15 @@ fun DeclineButton(buttonClicked: () -> Unit) { ) { Icon( modifier = Modifier - .clickable(interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().outgoingCallHangUpButtonSize / 2), + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple( + bounded = false, + radius = dimensions().outgoingCallHangUpButtonSize / 2 + ), role = Role.Button, - onClick = { buttonClicked() }), + onClick = { buttonClicked() } + ), painter = painterResource(id = R.drawable.ic_decline), contentDescription = stringResource(id = R.string.content_description_calling_decline_call), tint = Color.Unspecified diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt index 4a8b0353503..5dd2986e095 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/HangUpButton.kt @@ -59,7 +59,10 @@ fun HangUpButton( @Preview @Composable fun PreviewComposableHangUpButton() { - HangUpButton(modifier = Modifier - .width(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize) - .height(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize), onHangUpButtonClicked = { }) + HangUpButton( + modifier = Modifier + .width(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize) + .height(MaterialTheme.wireDimensions.outgoingCallHangUpButtonSize), + onHangUpButtonClicked = { } + ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt index 45862ae746c..2b43016bfb0 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallScreen.kt @@ -82,8 +82,7 @@ fun OutgoingCallScreen( onCallAccepted() } - OutgoingCallState.FlowState.Default -> { /* do nothing */ - } + OutgoingCallState.FlowState.Default -> { /* do nothing */ } } } with(sharedCallingViewModel) { diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt new file mode 100644 index 00000000000..98a89210eb3 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallState.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.calling.outgoing + +data class OutgoingCallState( + val flowState: FlowState = FlowState.Default +) { + sealed interface FlowState { + data object Default : FlowState + data object CallClosed : FlowState + data object CallEstablished : FlowState + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt index 9ffac1380c2..7a1b2fc2fba 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBar.kt @@ -69,6 +69,17 @@ fun CommonTopAppBar( } } +@Composable +fun getBackgroundColor(connectivityInfo: ConnectivityUIState): Color { + return when (connectivityInfo) { + is ConnectivityUIState.EstablishedCall, + is ConnectivityUIState.IncomingCall, + is ConnectivityUIState.OutgoingCall -> MaterialTheme.wireColorScheme.positive + ConnectivityUIState.Connecting, ConnectivityUIState.WaitingConnection -> MaterialTheme.wireColorScheme.primary + ConnectivityUIState.None -> MaterialTheme.wireColorScheme.background + } +} + @Composable private fun ConnectivityStatusBar( connectivityInfo: ConnectivityUIState, @@ -77,13 +88,7 @@ private fun ConnectivityStatusBar( onReturnToOutgoingCallClick: (ConnectivityUIState.OutgoingCall) -> Unit ) { val isVisible = connectivityInfo !is ConnectivityUIState.None - val backgroundColor = when (connectivityInfo) { - is ConnectivityUIState.EstablishedCall, - is ConnectivityUIState.IncomingCall, - is ConnectivityUIState.OutgoingCall -> MaterialTheme.wireColorScheme.positive - ConnectivityUIState.Connecting, ConnectivityUIState.WaitingConnection -> MaterialTheme.wireColorScheme.primary - ConnectivityUIState.None -> MaterialTheme.wireColorScheme.background - } + val backgroundColor = getBackgroundColor(connectivityInfo) if (!isVisible) { clearStatusBarColor() } @@ -102,13 +107,16 @@ private fun ConnectivityStatusBar( .height(MaterialTheme.wireDimensions.ongoingCallLabelHeight) .background(backgroundColor) .run { - when(connectivityInfo) { + when (connectivityInfo) { is ConnectivityUIState.EstablishedCall -> clickable(onClick = { onReturnToCallClick(connectivityInfo) }) + is ConnectivityUIState.IncomingCall -> clickable(onClick = { onReturnToIncomingCallClick(connectivityInfo) }) + is ConnectivityUIState.OutgoingCall -> clickable(onClick = { onReturnToOutgoingCallClick(connectivityInfo) }) + else -> this } } @@ -173,6 +181,7 @@ private fun IncomingCallContent(callerName: String?) { ) } } + @Composable private fun OutgoingCallContent(callerName: String?) { Row { diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index 22a2b94e3ae..b99e4e2aafc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -54,29 +54,32 @@ class CommonTopAppBarViewModel @Inject constructor( var state by mutableStateOf(CommonTopAppBarState()) private set - private suspend fun currentScreenFlow() = currentScreenManager.observeCurrentScreen(viewModelScope) + private suspend fun currentScreenFlow() = + currentScreenManager.observeCurrentScreen(viewModelScope) - private fun connectivityFlow(userId: UserId): Flow = coreLogic.sessionScope(userId) { - observeSyncState().map { - when (it) { - is SyncState.Failed, SyncState.Waiting -> Connectivity.WAITING_CONNECTION - SyncState.GatheringPendingEvents, SyncState.SlowSync -> Connectivity.CONNECTING - SyncState.Live -> Connectivity.CONNECTED + private fun connectivityFlow(userId: UserId): Flow = + coreLogic.sessionScope(userId) { + observeSyncState().map { + when (it) { + is SyncState.Failed, SyncState.Waiting -> Connectivity.WAITING_CONNECTION + SyncState.GatheringPendingEvents, SyncState.SlowSync -> Connectivity.CONNECTING + SyncState.Live -> Connectivity.CONNECTED + } } } - } - private suspend fun activeCallFlow(userId: UserId): Flow = coreLogic.sessionScope(userId) { - combine( - calls.establishedCall(), - calls.getIncomingCalls(), - calls.observeOutgoingCall(), - ) { establishedCall, incomingCalls, outgoingCalls -> - incomingCalls + outgoingCalls + establishedCall - }.map { calls -> - calls.firstOrNull() - }.distinctUntilChanged() - } + private suspend fun activeCallFlow(userId: UserId): Flow = + coreLogic.sessionScope(userId) { + combine( + calls.establishedCall(), + calls.getIncomingCalls(), + calls.observeOutgoingCall(), + ) { establishedCall, incomingCalls, outgoingCalls -> + incomingCalls + outgoingCalls + establishedCall + }.map { calls -> + calls.firstOrNull() + }.distinctUntilChanged() + } init { viewModelScope.launch { @@ -84,7 +87,9 @@ class CommonTopAppBarViewModel @Inject constructor( session.currentSessionFlow().flatMapLatest { when (it) { is CurrentSessionResult.Failure.Generic, - is CurrentSessionResult.Failure.SessionNotFound -> flowOf(ConnectivityUIState.None) + is CurrentSessionResult.Failure.SessionNotFound -> flowOf( + ConnectivityUIState.None + ) is CurrentSessionResult.Success -> { val userId = it.accountInfo.userId @@ -123,9 +128,9 @@ class CommonTopAppBarViewModel @Inject constructor( val canDisplayConnectivityIssues = currentScreen !is CurrentScreen.AuthRelated if (activeCall != null) { - return if(activeCall.status == CallStatus.INCOMING) { + return if (activeCall.status == CallStatus.INCOMING) { ConnectivityUIState.IncomingCall(activeCall.conversationId, activeCall.callerName) - } else if(activeCall.status == CallStatus.STARTED) { + } else if (activeCall.status == CallStatus.STARTED) { ConnectivityUIState.OutgoingCall(activeCall.conversationId, activeCall.callerName) } else { ConnectivityUIState.EstablishedCall(activeCall.conversationId, activeCall.isMuted) From 08f193deb60112bc8354b6b451de33e83c7cf746 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 18 Apr 2024 18:15:15 +0200 Subject: [PATCH 16/29] chore: revert changes --- .../sketch/tools/DrawingToolsConfig.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt new file mode 100644 index 00000000000..9c56240a6cc --- /dev/null +++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/tools/DrawingToolsConfig.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.feature.sketch.tools + +import androidx.compose.ui.graphics.Color +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Configuration for the drawing tools. + * ie. colors, stroke width, etc. + */ +data class DrawingToolsConfig( + val colors: ImmutableList = persistentListOf(Color.Blue), + // todo. later add more configurations, like stroke width, etc. +) From c5ab04b2b7391f48af3c7ec9b46ad84622845185 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 19 Apr 2024 11:10:57 +0200 Subject: [PATCH 17/29] chore: cover CommonTopAppBarViewModel with test --- .../topappbar/CommonTopAppBarViewModelTest.kt | 273 ++++++++++++------ 1 file changed, 182 insertions(+), 91 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt index 7f5b25832c5..1b8102c7b65 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt @@ -25,10 +25,14 @@ import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.GlobalKaliumScope import com.wire.kalium.logic.data.auth.AccountInfo 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.sync.SyncState import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.sync.ObserveSyncStateUseCase import io.mockk.MockKAnnotations @@ -53,78 +57,72 @@ import org.junit.jupiter.api.extension.ExtendWith class CommonTopAppBarViewModelTest { @Test - fun givenNoActiveCallAndHomeScreenAndSlowSync_whenGettingState_thenShouldHaveConnectingInfo() = runTest { - val (_, commonTopAppBarViewModel) = Arrangement() - .withCurrentSessionExist() - .withoutActiveCall() - .withCurrentScreen(CurrentScreen.Home) - .withSyncState(SyncState.SlowSync) - .arrange() - - advanceUntilIdle() - val state = commonTopAppBarViewModel.state - - val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.Connecting::class - } - - @Test - fun givenNoActiveCallAndHomeScreenAndGathering_whenGettingState_thenShouldHaveConnectingInfo() = runTest { - val (_, commonTopAppBarViewModel) = Arrangement() - .withCurrentSessionExist() - .withoutActiveCall() - .withCurrentScreen(CurrentScreen.Home) - .withSyncState(SyncState.GatheringPendingEvents) - .arrange() - - advanceUntilIdle() - val state = commonTopAppBarViewModel.state - - val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.Connecting::class - } + fun givenNoCallAndHomeScreenAndSlowSync_whenGettingState_thenShouldHaveConnectingInfo() = + runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withoutOngoingCall() + .withoutOutgoingCall() + .withoutIncomingCall() + .withCurrentScreen(CurrentScreen.Home) + .withSyncState(SyncState.SlowSync) + .arrange() + + advanceUntilIdle() + val state = commonTopAppBarViewModel.state + + val info = state.connectivityState + info shouldBeInstanceOf ConnectivityUIState.Connecting::class + } @Test - fun givenActiveCallAndHomeScreenAndConnectivityIssues_whenGettingState_thenShouldHaveActiveCallInfo() = runTest { - val (arrangement, commonTopAppBarViewModel) = Arrangement() - .withCurrentSessionExist() - .withActiveCall() - .withCurrentScreen(CurrentScreen.Home) - .withSyncState(SyncState.Waiting) - .arrange() - - advanceUntilIdle() - val state = commonTopAppBarViewModel.state - - val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class - info as ConnectivityUIState.EstablishedCall - info.conversationId shouldBeEqualTo arrangement.activeCall.conversationId - } + fun givenNoCallAndHomeScreenAndGathering_whenGettingState_thenShouldHaveConnectingInfo() = + runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withoutOngoingCall() + .withoutOutgoingCall() + .withoutIncomingCall() + .withCurrentScreen(CurrentScreen.Home) + .withSyncState(SyncState.GatheringPendingEvents) + .arrange() + + advanceUntilIdle() + val state = commonTopAppBarViewModel.state + + val info = state.connectivityState + info shouldBeInstanceOf ConnectivityUIState.Connecting::class + } @Test - fun givenActiveCallAndConnectivityIssues_whenGettingState_thenShouldHaveConnectivityInfo() = runTest { - val (_, commonTopAppBarViewModel) = Arrangement() - .withCurrentSessionExist() - .withActiveCall() - .withCurrentScreen(CurrentScreen.Home) - .withSyncState(SyncState.Waiting) - .arrange() - - advanceUntilIdle() - val state = commonTopAppBarViewModel.state - - val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class - } + fun givenAnOngoingCallAndHomeScreenAndConnectivityIssues_whenGettingState_thenShouldHaveActiveCallInfo() = + runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withOngoingCall() + .withoutOutgoingCall() + .withoutIncomingCall() + .withCurrentScreen(CurrentScreen.Home) + .withSyncState(SyncState.Waiting) + .arrange() + + advanceUntilIdle() + val state = commonTopAppBarViewModel.state + + val info = state.connectivityState + info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class + info as ConnectivityUIState.EstablishedCall + info.conversationId shouldBeEqualTo ongoingCall.conversationId + } @Test - fun givenActiveCallAndCallIsMuted_whenGettingState_thenShouldHaveMutedCallInfo() = runTest { + fun givenAnOngoingCallAndCallIsMuted_whenGettingState_thenShouldHaveMutedCallInfo() = runTest { val (_, commonTopAppBarViewModel) = Arrangement() .withCurrentSessionExist() - .withActiveCall() + .withOngoingCall(isMuted = true) + .withoutOutgoingCall() + .withoutIncomingCall() .withCurrentScreen(CurrentScreen.Conversation(mockk())) - .withCallMuted(true) .withSyncState(SyncState.Waiting) .arrange() @@ -138,12 +136,53 @@ class CommonTopAppBarViewModelTest { } @Test - fun givenActiveCallAndCallIsNotMuted_whenGettingState_thenShouldNotHaveMutedCallInfo() = runTest { + fun givenAnOngoingCallAndCallIsNotMuted_whenGettingState_thenShouldNotHaveMutedCallInfo() = + runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withOngoingCall(isMuted = false) + .withoutOutgoingCall() + .withoutIncomingCall() + .withCurrentScreen(CurrentScreen.Conversation(mockk())) + .withSyncState(SyncState.Waiting) + .arrange() + + advanceUntilIdle() + val state = commonTopAppBarViewModel.state + + val info = state.connectivityState + info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class + info as ConnectivityUIState.EstablishedCall + info.isMuted shouldBe false + } + + @Test + fun givenAnOngoingCallAndConnectivityIssueAndSomeOtherScreen_whenGettingState_thenShouldHaveActiveCallInfo() = + runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withOngoingCall(isMuted = false) + .withoutOutgoingCall() + .withoutIncomingCall() + .withCurrentScreen(CurrentScreen.SomeOther) + .withSyncState(SyncState.Waiting) + .arrange() + + advanceUntilIdle() + val state = commonTopAppBarViewModel.state + + val info = state.connectivityState + info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class + } + + @Test + fun givenAnIncomingCallAnd_whenGettingState_thenShouldHaveIncomingCallInfo() = runTest { val (_, commonTopAppBarViewModel) = Arrangement() .withCurrentSessionExist() - .withActiveCall() - .withCurrentScreen(CurrentScreen.Conversation(mockk())) - .withCallMuted(false) + .withIncomingCall() + .withoutOngoingCall() + .withoutOutgoingCall() + .withCurrentScreen(CurrentScreen.Home) .withSyncState(SyncState.Waiting) .arrange() @@ -151,26 +190,25 @@ class CommonTopAppBarViewModelTest { val state = commonTopAppBarViewModel.state val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class - info as ConnectivityUIState.EstablishedCall - info.isMuted shouldBe false + info shouldBeInstanceOf ConnectivityUIState.IncomingCall::class } @Test - fun givenActiveCallAndConnectivityIssueAndSomeOtherScreen_whenGettingState_thenShouldHaveActiveCallInfo() = runTest { + fun givenAnOutgoingCallAnd_whenGettingState_thenShouldHaveOutgoingCallInfo() = runTest { val (_, commonTopAppBarViewModel) = Arrangement() .withCurrentSessionExist() - .withActiveCall() - .withCurrentScreen(CurrentScreen.SomeOther) - .withCallMuted(false) - .withSyncState(SyncState.Waiting) + .withOutgoingCall() + .withoutIncomingCall() + .withoutOngoingCall() + .withCurrentScreen(CurrentScreen.Home) + .withSyncState(SyncState.Live) .arrange() advanceUntilIdle() val state = commonTopAppBarViewModel.state val info = state.connectivityState - info shouldBeInstanceOf ConnectivityUIState.EstablishedCall::class + info shouldBeInstanceOf ConnectivityUIState.OutgoingCall::class } @Test @@ -187,13 +225,14 @@ class CommonTopAppBarViewModelTest { private class Arrangement { - val activeCall: Call = mockk() - val conversationId: ConversationId = mockk() + @MockK + private lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase - private var isCallMuted = true + @MockK + private lateinit var incomingCalls: GetIncomingCallsUseCase @MockK - private lateinit var establishedCalls: ObserveEstablishedCallsUseCase + private lateinit var observeOutgoingCall: ObserveOutgoingCallUseCase @MockK private lateinit var currentScreenManager: CurrentScreenManager @@ -209,7 +248,6 @@ class CommonTopAppBarViewModelTest { init { MockKAnnotations.init(this) - every { activeCall.conversationId } returns conversationId every { coreLogic.sessionScope(any()) { observeSyncState @@ -220,7 +258,19 @@ class CommonTopAppBarViewModelTest { coreLogic.sessionScope(any()) { calls.establishedCall } - } returns establishedCalls + } returns observeEstablishedCalls + + every { + coreLogic.sessionScope(any()) { + calls.getIncomingCalls + } + } returns incomingCalls + + every { + coreLogic.sessionScope(any()) { + calls.observeOutgoingCall + } + } returns observeOutgoingCall every { coreLogic.getGlobalScope() @@ -231,27 +281,44 @@ class CommonTopAppBarViewModelTest { } returns emptyFlow() withSyncState(SyncState.Live) - withoutActiveCall() + withoutOngoingCall() } private val commonTopAppBarViewModel by lazy { - every { activeCall.isMuted } returns isCallMuted CommonTopAppBarViewModel( currentScreenManager, coreLogic ) } - fun withCallMuted(isMuted: Boolean) = apply { - isCallMuted = isMuted + fun withOngoingCall(isMuted: Boolean = false) = apply { + coEvery { observeEstablishedCalls() } returns flowOf( + listOf(ongoingCall.copy(isMuted = isMuted)) + ) } - fun withActiveCall() = apply { - coEvery { establishedCalls() } returns flowOf(listOf(activeCall)) + fun withoutOngoingCall() = apply { + coEvery { observeEstablishedCalls() } returns flowOf(listOf()) } - fun withoutActiveCall() = apply { - coEvery { establishedCalls() } returns flowOf(listOf()) + fun withoutOutgoingCall() = apply { + coEvery { observeOutgoingCall() } returns flowOf(listOf()) + } + + fun withOutgoingCall() = apply { + coEvery { observeOutgoingCall() } returns flowOf( + listOf(outgoingCall) + ) + } + + fun withoutIncomingCall() = apply { + coEvery { incomingCalls() } returns flowOf(listOf()) + } + + fun withIncomingCall() = apply { + coEvery { incomingCalls() } returns flowOf( + listOf(incomingCall) + ) } fun withSyncState(syncState: SyncState) = apply { @@ -273,9 +340,33 @@ class CommonTopAppBarViewModelTest { } fun withCurrentScreen(currentScreen: CurrentScreen) = apply { - coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(currentScreen) + coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow( + currentScreen + ) } fun arrange() = this to commonTopAppBarViewModel } + + companion object { + val conversationId = ConversationId("first", "domain") + val ongoingCall = Call( + conversationId, + CallStatus.ESTABLISHED, + true, + false, + false, + "caller-id", + "ONE_ON_ONE Name", + Conversation.Type.ONE_ON_ONE, + "otherUsername", + "team1" + ) + val outgoingCall = ongoingCall.copy( + status = CallStatus.STARTED + ) + val incomingCall = ongoingCall.copy( + status = CallStatus.INCOMING + ) + } } From 8564f2dd2abfbd8872124ece38099d397e19098b Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 19 Apr 2024 11:26:45 +0200 Subject: [PATCH 18/29] chore: cover WireNotificationManager with test --- .../WireNotificationManagerTest.kt | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt index 5d8c0f2d868..d25ad98bbba 100644 --- a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt @@ -47,6 +47,7 @@ import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.call.CallsScope import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveOutgoingCallUseCase import com.wire.kalium.logic.feature.connection.MarkConnectionRequestAsNotifiedUseCase import com.wire.kalium.logic.feature.conversation.ConversationScope import com.wire.kalium.logic.feature.message.GetNotificationsUseCase @@ -146,6 +147,7 @@ class WireNotificationManagerTest { fun givenSomeIncomingCalls_whenObserving_thenCallNotificationShowed() = runTestWithCancellation(dispatcherProvider.main()) { val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.SomeOther) .withCurrentUserSession(CurrentSessionResult.Success(AccountInfo.Valid(provideUserId()))) @@ -177,6 +179,25 @@ class WireNotificationManagerTest { verify(exactly = 1) { arrangement.callNotificationManager.handleIncomingCallNotifications(incomingCalls, user2) } } + @Test + fun givenOutgoingCall_whenCurrentUserIsDifferentFromCallReceiver_thenCallNotificationIsShown() = + runTestWithCancellation(dispatcherProvider.main()) { + val user1 = provideUserId("user1") + val user2 = provideUserId("user2") + val outgoingCalls = listOf(provideCall().copy(status = CallStatus.STARTED)) + val (arrangement, manager) = Arrangement() + .withSpecificUserSession(userId = user1) + .withSpecificUserSession(userId = user2, outgoingCalls = outgoingCalls) + .withMessageNotifications(listOf()) + .withCurrentScreen(CurrentScreen.SomeOther) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(user1.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(user1, user2), this) + runCurrent() + + verify(exactly = 1) { arrangement.callNotificationManager.handleOutgoingCallNotifications(outgoingCalls, user2) } + } @Test fun givenSomeNotifications_whenAppIsInForegroundAndNoUserLoggedIn_thenMessageNotificationNotShowed() = @@ -240,6 +261,7 @@ class WireNotificationManagerTest { val (arrangement, manager) = Arrangement() .withMessageNotifications(messageNotifications) .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.SomeOther) .arrange() @@ -265,7 +287,9 @@ class WireNotificationManagerTest { messages = listOf(provideLocalNotificationMessage()) ) ) - ).withIncomingCalls(listOf()) + ) + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.Conversation(conversationId)) .arrange() @@ -302,6 +326,7 @@ class WireNotificationManagerTest { ) .withEstablishedCall(listOf()) .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withSelfUser(selfUserFlow) .arrange() @@ -322,6 +347,7 @@ class WireNotificationManagerTest { val (arrangement, manager) = Arrangement() .withMessageNotifications(listOf()) .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.Conversation(conversationId)) .arrange() @@ -338,7 +364,8 @@ class WireNotificationManagerTest { val (arrangement, manager) = Arrangement() .withMessageNotifications(listOf()) .withSession(GetAllSessionsResult.Success(listOf(TEST_AUTH_TOKEN))) - .withIncomingCalls(listOf()).withCurrentScreen(CurrentScreen.InBackground) + .withIncomingCalls(listOf()) + .withCurrentScreen(CurrentScreen.InBackground) .arrange() coEvery { arrangement.connectionPolicyManager.handleConnectionOnPushNotification(userId, any()) } coAnswers { @@ -413,6 +440,7 @@ class WireNotificationManagerTest { ) ) .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.Conversation(id = conversationId)) .arrange() @@ -434,6 +462,7 @@ class WireNotificationManagerTest { val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withEstablishedCall(listOf(call)) @@ -453,6 +482,7 @@ class WireNotificationManagerTest { val userId = provideUserId() val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withEstablishedCall(listOf()) @@ -476,6 +506,7 @@ class WireNotificationManagerTest { .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withEstablishedCall(listOf(call)) + .withOutgoingCalls(listOf()) .withCurrentUserSession(CurrentSessionResult.Success(provideInvalidAccountInfo(userId.value))) .arrange() @@ -492,6 +523,7 @@ class WireNotificationManagerTest { val userId = provideUserId() val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withEstablishedCall(listOf()) @@ -512,6 +544,7 @@ class WireNotificationManagerTest { val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.Home) .withEstablishedCall(listOf(call)) @@ -531,6 +564,7 @@ class WireNotificationManagerTest { val userId = provideUserId() val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.Home) .withEstablishedCall(listOf()) @@ -551,6 +585,7 @@ class WireNotificationManagerTest { val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.Home) .withEstablishedCall(listOf(call)) @@ -570,6 +605,7 @@ class WireNotificationManagerTest { val userId = provideUserId() val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.Home) .withEstablishedCall(listOf()) @@ -653,6 +689,7 @@ class WireNotificationManagerTest { val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.InBackground) .withEstablishedCall(listOf(call)) @@ -680,7 +717,9 @@ class WireNotificationManagerTest { messages = listOf(provideLocalNotificationMessage()) ) ) - ).withIncomingCalls(listOf()) + ) + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) .withCurrentScreen(CurrentScreen.SomeOther) .withObserveE2EIRequired(E2EIRequiredResult.NoGracePeriod.Create) .arrange() @@ -792,6 +831,9 @@ class WireNotificationManagerTest { @MockK lateinit var getIncomingCallsUseCase: GetIncomingCallsUseCase + @MockK + lateinit var observeOutgoingCall: ObserveOutgoingCallUseCase + @MockK lateinit var establishedCall: ObserveEstablishedCallsUseCase @@ -848,6 +890,7 @@ class WireNotificationManagerTest { coEvery { messageNotificationManager.handleNotification(any(), any(), any()) } returns Unit coEvery { callsScope.getIncomingCalls } returns getIncomingCallsUseCase coEvery { callsScope.establishedCall } returns establishedCall + coEvery { callsScope.observeOutgoingCall } returns observeOutgoingCall coEvery { callNotificationManager.handleIncomingCallNotifications(any(), any()) } returns Unit coEvery { callNotificationManager.hideIncomingCallNotification() } returns Unit coEvery { callNotificationManager.builder.getNotificationTitle(any()) } returns "Test title" @@ -866,6 +909,7 @@ class WireNotificationManagerTest { private fun mockSpecificUserSession( incomingCalls: List = emptyList(), establishedCalls: List = emptyList(), + outgoingCalls: List = emptyList(), notifications: List = emptyList(), selfUser: SelfUser = TestUser.SELF_USER, userId: MockKMatcherScope.() -> UserId, @@ -878,6 +922,7 @@ class WireNotificationManagerTest { coEvery { calls } returns mockk { coEvery { establishedCall() } returns flowOf(establishedCalls) coEvery { getIncomingCalls() } returns flowOf(incomingCalls) + coEvery { observeOutgoingCall() } returns flowOf(outgoingCalls) } coEvery { messages } returns mockk { coEvery { getNotifications() } returns flowOf(notifications) @@ -909,6 +954,10 @@ class WireNotificationManagerTest { coEvery { getIncomingCallsUseCase() } returns flowOf(calls) return this } + fun withOutgoingCalls(calls: List): Arrangement { + coEvery { observeOutgoingCall() } returns flowOf(calls) + return this + } fun withEstablishedCall(calls: List): Arrangement { coEvery { establishedCall() } returns flowOf(calls) @@ -919,10 +968,11 @@ class WireNotificationManagerTest { userId: UserId, incomingCalls: List = emptyList(), establishedCalls: List = emptyList(), + outgoingCalls: List = emptyList(), notifications: List = emptyList(), selfUser: SelfUser = TestUser.SELF_USER, ): Arrangement = apply { - mockSpecificUserSession(incomingCalls, establishedCalls, notifications, selfUser) { eq(userId) } + mockSpecificUserSession(incomingCalls, establishedCalls, outgoingCalls, notifications, selfUser) { eq(userId) } } fun withCurrentScreen(screen: CurrentScreen): Arrangement { From 2812f5bcbc44d099abbc21394212b811e878e030 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 19 Apr 2024 12:02:11 +0200 Subject: [PATCH 19/29] chore: detekt --- .../notification/WireNotificationManagerTest.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt index d25ad98bbba..b9e39880786 100644 --- a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt @@ -177,8 +177,14 @@ class WireNotificationManagerTest { manager.observeNotificationsAndCallsWhileRunning(listOf(user1, user2), this) runCurrent() - verify(exactly = 1) { arrangement.callNotificationManager.handleIncomingCallNotifications(incomingCalls, user2) } + verify(exactly = 1) { + arrangement.callNotificationManager.handleIncomingCallNotifications( + incomingCalls, + user2 + ) + } } + @Test fun givenOutgoingCall_whenCurrentUserIsDifferentFromCallReceiver_thenCallNotificationIsShown() = runTestWithCancellation(dispatcherProvider.main()) { @@ -906,13 +912,14 @@ class WireNotificationManagerTest { coEvery { globalKaliumScope.doesValidSessionExist.invoke(any()) } returns DoesValidSessionExistResult.Success(true) } + @Suppress("LongParameterList") private fun mockSpecificUserSession( incomingCalls: List = emptyList(), establishedCalls: List = emptyList(), outgoingCalls: List = emptyList(), notifications: List = emptyList(), selfUser: SelfUser = TestUser.SELF_USER, - userId: MockKMatcherScope.() -> UserId, + userId: MockKMatcherScope.() -> UserId ) { coEvery { coreLogic.getSessionScope(userId()) } returns mockk { coEvery { syncManager } returns this@Arrangement.syncManager @@ -964,6 +971,7 @@ class WireNotificationManagerTest { return this } + @Suppress("LongParameterList") fun withSpecificUserSession( userId: UserId, incomingCalls: List = emptyList(), From 741becca784f2732e7c7a337a7de365fdf0a1799 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 22 Apr 2024 11:41:04 +0200 Subject: [PATCH 20/29] chore: cover CallNotificationManager with unit test --- .../notification/CallNotificationManager.kt | 1 - .../CallNotificationManagerTest.kt | 60 ++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index d9240012287..c84b49e308a 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -173,7 +173,6 @@ class CallNotificationBuilder @Inject constructor( conversationName: String ): Notification { val conversationIdString = conversationId.toString() - val userIdString = userId.toString() val channelId = NotificationConstants.getOutgoingChannelId(userId) return NotificationCompat.Builder(context, channelId) diff --git a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt index 0bb3c0542d9..fa1591085ed 100644 --- a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt @@ -55,8 +55,29 @@ class CallNotificationManagerTest { callNotificationManager.handleIncomingCallNotifications(listOf(), TEST_USER_ID1) advanceUntilIdle() // then - verify(exactly = 0) { arrangement.notificationManager.notify(any(), any()) } - verify(exactly = 1) { arrangement.notificationManager.cancel(any()) } + verify(exactly = 0) { + arrangement.notificationManager.notify(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID, any()) + } + verify(exactly = 1) { + arrangement.notificationManager.cancel(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID) + } + } + + @Test + fun `given no outgoing calls, when handling notifications, then hide outgoing call notification`() = + runTest(dispatcherProvider.main()) { + // given + val (arrangement, callNotificationManager) = Arrangement() + .arrange() + callNotificationManager.handleOutgoingCallNotifications(listOf(), TEST_USER_ID1) + advanceUntilIdle() + // then + verify(exactly = 0) { + arrangement.notificationManager.notify(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID, any()) + } + verify(exactly = 1) { + arrangement.notificationManager.cancel(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID) + } } @Test @@ -74,6 +95,25 @@ class CallNotificationManagerTest { verify(exactly = 1) { arrangement.notificationManager.notify(any(), notification) } verify(exactly = 0) { arrangement.notificationManager.cancel(any()) } } + @Test + fun `given an outgoing call for one user, when handling notifications,, then show notification for that call`() = + runTest(dispatcherProvider.main()) { + val notification = mockk() + val (arrangement, callNotificationManager) = Arrangement() + .withOutgoingNotificationForUserAndCall(notification, TEST_USER_ID1, TEST_CALL1.conversationId, TEST_CALL1.conversationName!!) + .arrange() + + arrangement.clearRecordedCallsForNotificationManager() // clear first empty list recorded call + callNotificationManager.handleOutgoingCallNotifications(listOf(TEST_CALL1), TEST_USER_ID1) + advanceUntilIdle() + + verify(exactly = 1) { + arrangement.notificationManager.notify(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID, notification) + } + verify(exactly = 0) { + arrangement.notificationManager.cancel(NotificationConstants.CALL_OUTGOING_NOTIFICATION_ID) + } + } @Test fun `given incoming calls for two users, then show notification for the first call`() = @@ -155,7 +195,7 @@ class CallNotificationManagerTest { callNotificationManager.handleIncomingCallNotifications(listOf(), TEST_USER_ID1) // then verify(exactly = 0) { arrangement.notificationManager.notify(any(), notification) } - verify(exactly = 1) { arrangement.notificationManager.cancel(any()) } + verify(exactly = 1) { arrangement.notificationManager.cancel(NotificationConstants.CALL_INCOMING_NOTIFICATION_ID) } } @Test @@ -210,6 +250,20 @@ class CallNotificationManagerTest { fun withIncomingNotificationForUserAndCall(notification: Notification, forUser: UserId, forCall: Call) = apply { every { callNotificationBuilder.getIncomingCallNotification(eq(forCall), eq(forUser)) } returns notification } + fun withOutgoingNotificationForUserAndCall( + notification: Notification, + forUser: UserId, + conversationId: ConversationId, + conversationName: String + ) = apply { + every { + callNotificationBuilder.getOutgoingCallNotification( + eq(conversationId), + eq(forUser), + eq(conversationName) + ) + } returns notification + } fun arrange() = this to callNotificationManager } From f1961fe026b576d168730fccd6a13f3c5767e6f4 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 22 Apr 2024 13:38:04 +0200 Subject: [PATCH 21/29] chore: detekt --- .../android/notification/CallNotificationManagerTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt index fa1591085ed..898c7a4a2d9 100644 --- a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt @@ -95,12 +95,18 @@ class CallNotificationManagerTest { verify(exactly = 1) { arrangement.notificationManager.notify(any(), notification) } verify(exactly = 0) { arrangement.notificationManager.cancel(any()) } } + @Test fun `given an outgoing call for one user, when handling notifications,, then show notification for that call`() = runTest(dispatcherProvider.main()) { val notification = mockk() val (arrangement, callNotificationManager) = Arrangement() - .withOutgoingNotificationForUserAndCall(notification, TEST_USER_ID1, TEST_CALL1.conversationId, TEST_CALL1.conversationName!!) + .withOutgoingNotificationForUserAndCall( + notification, + TEST_USER_ID1, + TEST_CALL1.conversationId, + TEST_CALL1.conversationName!! + ) .arrange() arrangement.clearRecordedCallsForNotificationManager() // clear first empty list recorded call From 52e4186bf22a89dbd439367d0a4dd97cf446fea0 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 25 Apr 2024 16:31:09 +0200 Subject: [PATCH 22/29] chore: address comments --- .../notification/CallNotificationManager.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index c84b49e308a..3122134d56e 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -54,7 +54,7 @@ class CallNotificationManager @Inject constructor( private val notificationManager = NotificationManagerCompat.from(context) private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.default()) private val incomingCallsForUsers = MutableStateFlow>>(emptyList()) - private val outgoingCallForUsers = MutableStateFlow>>(emptyList()) + private val outgoingCallForUsers = MutableStateFlow>(mapOf()) init { scope.launch { @@ -77,8 +77,12 @@ class CallNotificationManager @Inject constructor( if (it.isEmpty()) { hideOutgoingCallNotification() } else { - it.first().let { (userId, call) -> - showOutgoingCallNotification(call.conversationId, userId, call.conversationName ?: "Name") + it[it.keys.first()]?.let { call -> + showOutgoingCallNotification( + conversationId = call.conversationId, + userId = it.keys.first(), + conversationName = call.conversationName ?: context.getString(R.string.calling_participant_tile_default_user_name) + ) } } } @@ -98,9 +102,9 @@ class CallNotificationManager @Inject constructor( } fun handleOutgoingCallNotifications(calls: List, userId: UserId) { if (calls.isEmpty()) { - outgoingCallForUsers.update { it.filter { it.first != userId } } + outgoingCallForUsers.update { it.filter { it.key != userId } } } else { - outgoingCallForUsers.update { it.filter { it.first != userId } + (userId to calls.first()) } + outgoingCallForUsers.update { it.filter { it.key != userId } + (userId to calls.first()) } } } From 0ca1f7a004d69a1a82b76be27f22faa2d76b95ee Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 25 Apr 2024 17:02:17 +0200 Subject: [PATCH 23/29] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index 272c815fea7..b6bdf965dc8 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit 272c815fea702682cea046b2f724a4f7a4b3df61 +Subproject commit b6bdf965dc8142e01e29d54843125c08b0a47694 From a478a5ad1e04e04283d51b80ad90122f90c958af Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 25 Apr 2024 17:02:28 +0200 Subject: [PATCH 24/29] chore: resolve conflicts --- .../com/wire/android/notification/CallNotificationManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index 3122134d56e..6689de82063 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -187,7 +187,6 @@ class CallNotificationBuilder @Inject constructor( .setContentText(context.getString(R.string.notification_outgoing_call_tap_to_return)) .setAutoCancel(false) .setOngoing(true) - .setVibrate(VIBRATE_PATTERN) .setFullScreenIntent( outgoingCallPendingIntent(context, conversationIdString), true From 2544ef7b3dc9a1838271aafddcec0f7735477a22 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 25 Apr 2024 17:40:57 +0200 Subject: [PATCH 25/29] chore: detekt --- .../notification/CallNotificationManager.kt | 7 ++++--- .../com/wire/android/ui/calling/CallActivity.kt | 14 +++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index 6689de82063..7d0c388c535 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -81,7 +81,8 @@ class CallNotificationManager @Inject constructor( showOutgoingCallNotification( conversationId = call.conversationId, userId = it.keys.first(), - conversationName = call.conversationName ?: context.getString(R.string.calling_participant_tile_default_user_name) + conversationName = call.conversationName + ?: context.getString(R.string.calling_participant_tile_default_user_name) ) } } @@ -102,9 +103,9 @@ class CallNotificationManager @Inject constructor( } fun handleOutgoingCallNotifications(calls: List, userId: UserId) { if (calls.isEmpty()) { - outgoingCallForUsers.update { it.filter { it.key != userId } } + outgoingCallForUsers.update { it.filter { it.key != userId } } } else { - outgoingCallForUsers.update { it.filter { it.key != userId } + (userId to calls.first()) } + outgoingCallForUsers.update { it.filter { it.key != userId } + (userId to calls.first()) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt index 5f48259876f..47aa28d554f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/CallActivity.kt @@ -64,15 +64,13 @@ class CallActivity : AppCompatActivity() { private val qualifiedIdMapper = QualifiedIdMapperImpl(null) + @Suppress("LongMethod") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setUpScreenShootPreventionFlag() setUpCallingFlags() - appLogger.i("$TAG Initializing proximity sensor..") - proximitySensorManager.initialize() - WindowCompat.setDecorFitsSystemWindows(window, false) val conversationId = intent.extras?.getString(EXTRA_CONVERSATION_ID) @@ -112,7 +110,11 @@ class CallActivity : AppCompatActivity() { conversationId?.let { when (screenType) { CallScreenType.Outgoing.name -> { - OutgoingCallScreen(conversationId = qualifiedIdMapper.fromStringToQualifiedID(it)) { + OutgoingCallScreen( + conversationId = qualifiedIdMapper.fromStringToQualifiedID( + it + ) + ) { currentCallScreenType = CallScreenType.Ongoing.name } } @@ -129,9 +131,7 @@ class CallActivity : AppCompatActivity() { } } } - } ?: run { - finish() - } + } ?: run { finish() } } } } From ecd27de043a3a16d8c3d4d3709146640f7bfb8f0 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Thu, 25 Apr 2024 18:21:54 +0200 Subject: [PATCH 26/29] chore: kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index b6bdf965dc8..70e54372181 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b6bdf965dc8142e01e29d54843125c08b0a47694 +Subproject commit 70e54372181f210ef102b4ea0c89b88062e17687 From 6ffb9404b066d13c12310f99f68206529a84457c Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Fri, 26 Apr 2024 12:56:53 +0200 Subject: [PATCH 27/29] Update app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt Co-authored-by: Vitor Hugo Schwaab --- .../wire/android/notification/CallNotificationManagerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt index 898c7a4a2d9..0c34123f0e8 100644 --- a/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/CallNotificationManagerTest.kt @@ -97,7 +97,7 @@ class CallNotificationManagerTest { } @Test - fun `given an outgoing call for one user, when handling notifications,, then show notification for that call`() = + fun `given an outgoing call for one user, when handling notifications, then show notification for that call`() = runTest(dispatcherProvider.main()) { val notification = mockk() val (arrangement, callNotificationManager) = Arrangement() From 528e030a096d64e9f46d369294cb2baaee2ee344 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 29 Apr 2024 10:48:07 +0200 Subject: [PATCH 28/29] chore: missing unit test --- .../notification/CallNotificationManager.kt | 15 +++++++------ .../calling/outgoing/OutgoingCallViewModel.kt | 6 +++++- .../topappbar/CommonTopAppBarViewModel.kt | 6 ++++-- .../topappbar/CommonTopAppBarViewModelTest.kt | 21 ++++++++++++++++++- 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index 7d0c388c535..cf7e8e553a1 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -77,14 +77,13 @@ class CallNotificationManager @Inject constructor( if (it.isEmpty()) { hideOutgoingCallNotification() } else { - it[it.keys.first()]?.let { call -> - showOutgoingCallNotification( - conversationId = call.conversationId, - userId = it.keys.first(), - conversationName = call.conversationName - ?: context.getString(R.string.calling_participant_tile_default_user_name) - ) - } + val call = it.values.first() + showOutgoingCallNotification( + conversationId = call.conversationId, + userId = it.keys.first(), + conversationName = call.conversationName + ?: context.getString(R.string.calling_participant_tile_default_user_name) + ) } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt index b7073910165..0e6d5fa24ae 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/outgoing/OutgoingCallViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.R +import com.wire.android.appLogger import com.wire.android.media.CallRinger import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.usecase.EndCallUseCase @@ -116,7 +117,10 @@ class OutgoingCallViewModel @AssistedInject constructor( isIncomingCall = false ) - StartCallUseCase.Result.SyncFailure -> {} // TODO: handle case where start call fails + StartCallUseCase.Result.SyncFailure -> { + // TODO: handle case where start call fails + appLogger.i("Failed to start call") + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt index b99e4e2aafc..8f0db156acb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModel.kt @@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject @HiltViewModel @@ -68,14 +69,15 @@ class CommonTopAppBarViewModel @Inject constructor( } } - private suspend fun activeCallFlow(userId: UserId): Flow = + @VisibleForTesting + internal suspend fun activeCallFlow(userId: UserId): Flow = coreLogic.sessionScope(userId) { combine( calls.establishedCall(), calls.getIncomingCalls(), calls.observeOutgoingCall(), ) { establishedCall, incomingCalls, outgoingCalls -> - incomingCalls + outgoingCalls + establishedCall + establishedCall + incomingCalls + outgoingCalls }.map { calls -> calls.firstOrNull() }.distinctUntilChanged() diff --git a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt index 1b8102c7b65..5c0fb7acc07 100644 --- a/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/common/topappbar/CommonTopAppBarViewModelTest.kt @@ -223,6 +223,24 @@ class CommonTopAppBarViewModelTest { state.connectivityState shouldBeInstanceOf ConnectivityUIState.None::class } + @Test + fun givenEstablishedAndIncomingCall_whenActiveCallFlowIsCalled_thenEmitEstablishedCallOnly() = runTest { + val (_, commonTopAppBarViewModel) = Arrangement() + .withCurrentSessionExist() + .withOngoingCall(isMuted = true) + .withIncomingCall() + .withOutgoingCall() + .withCurrentScreen(CurrentScreen.Home) + .withSyncState(SyncState.Waiting) + .arrange() + + val flow = commonTopAppBarViewModel.activeCallFlow(userId) + + flow.collect { + it shouldBeEqualTo ongoingCall + } + } + private class Arrangement { @MockK @@ -334,7 +352,7 @@ class CommonTopAppBarViewModelTest { fun withCurrentSessionExist() = apply { every { coreLogic.globalScope { session.currentSessionFlow() } } returns flowOf( CurrentSessionResult.Success( - AccountInfo.Valid(UserId("userId", "domain")) + AccountInfo.Valid(userId) ) ) } @@ -349,6 +367,7 @@ class CommonTopAppBarViewModelTest { } companion object { + val userId = UserId("userId", "domain") val conversationId = ConversationId("first", "domain") val ongoingCall = Call( conversationId, From 5f515ed0b970399530261060464e6e33c97e3f27 Mon Sep 17 00:00:00 2001 From: Oussama Hassine Date: Mon, 29 Apr 2024 11:09:20 +0200 Subject: [PATCH 29/29] chore: update kalium reference --- kalium | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kalium b/kalium index b1625f7fb8e..bbf74b111bd 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit b1625f7fb8e5d8f757e4930a8398b8300fa7f3ae +Subproject commit bbf74b111bde930b8acb3639563ef0c2c32f56a4