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 7fd28ce87d1..9990a1673fe 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -23,6 +23,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.WindowManager +import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -51,6 +52,7 @@ import androidx.navigation.NavController import androidx.navigation.NavHostController import com.ramcosta.composedestinations.spec.Route import com.wire.android.BuildConfig +import com.wire.android.R import com.wire.android.appLogger import com.wire.android.config.CustomUiConfigurationProvider import com.wire.android.config.LocalCustomUiConfigurationProvider @@ -507,8 +509,29 @@ class WireActivity : AppCompatActivity() { viewModel.handleDeepLink( intent = intent, onResult = ::handleDeepLinkResult, - onOpenConversation = { navigate(NavigationCommand(ConversationScreenDestination(it), BackStackMode.CLEAR_TILL_START)) }, - onIsSharingIntent = { navigate(NavigationCommand(ImportMediaScreenDestination, BackStackMode.UPDATE_EXISTED)) } + onOpenConversation = { + navigate( + NavigationCommand( + ConversationScreenDestination(it), + BackStackMode.CLEAR_TILL_START + ) + ) + }, + onIsSharingIntent = { + navigate( + NavigationCommand( + ImportMediaScreenDestination, + BackStackMode.UPDATE_EXISTED + ) + ) + }, + onCannotLoginDuringACall = { + Toast.makeText( + this, + resources.getString(R.string.cant_switch_account_in_call), + Toast.LENGTH_SHORT + ).show() + } ) intent.putExtra(HANDLED_DEEPLINK_FLAG, true) } 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 79ae1d5e2fc..847977bbc20 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -60,6 +60,7 @@ import com.wire.kalium.logic.data.logout.LogoutReason import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase import com.wire.kalium.logic.feature.client.NewClientResult import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase @@ -117,6 +118,7 @@ class WireActivityViewModel @Inject constructor( private val globalDataStore: GlobalDataStore, private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory, private val workManager: WorkManager, + private val observeEstablishedCalls: ObserveEstablishedCallsUseCase ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -255,12 +257,15 @@ class WireActivityViewModel @Inject constructor( return intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE } + private suspend fun canLoginThroughDeepLinks() = observeEstablishedCalls().first().isEmpty() + @Suppress("ComplexMethod") fun handleDeepLink( intent: Intent?, onIsSharingIntent: () -> Unit, onOpenConversation: (ConversationId) -> Unit, - onResult: (DeepLinkResult) -> Unit + onResult: (DeepLinkResult) -> Unit, + onCannotLoginDuringACall: () -> Unit ) { if (shouldMigrate()) { // means User is Logged in, but didn't finish the migration yet. @@ -270,9 +275,21 @@ class WireActivityViewModel @Inject constructor( viewModelScope.launch { val result = intent?.data?.let { deepLinkProcessor(it) } when { - result is DeepLinkResult.SSOLogin -> onResult(result) + result is DeepLinkResult.SSOLogin -> { + if (canLoginThroughDeepLinks()) { + onResult(result) + } else { + onCannotLoginDuringACall() + } + } result is DeepLinkResult.MigrationLogin -> onResult(result) - result is DeepLinkResult.CustomServerConfig -> onCustomServerConfig(result) + result is DeepLinkResult.CustomServerConfig -> { + if (canLoginThroughDeepLinks()) { + onCustomServerConfig(result) + } else { + onCannotLoginDuringACall() + } + } isSharingIntent(intent) -> onIsSharingIntent() shouldLogIn() -> { // to handle the deepLinks above user needs to be Logged in 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 5065fa3fd71..e0c020d09d3 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 @@ -35,7 +35,7 @@ import javax.inject.Inject import javax.inject.Singleton sealed class DeepLinkResult { - object Unknown : DeepLinkResult() + data object Unknown : DeepLinkResult() data class CustomServerConfig(val url: String) : DeepLinkResult() @Serializable 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 41c7c7b5df1..c50e580728d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -36,6 +36,7 @@ import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.migration.MigrationManager import com.wire.android.services.ServicesManager +import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModelTest import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.CurrentScreen @@ -46,10 +47,14 @@ import com.wire.android.util.newServerConfig import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus +import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.appVersioning.ObserveIfAppUpdateRequiredUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearNewClientsForUserUseCase import com.wire.kalium.logic.feature.client.NewClientResult import com.wire.kalium.logic.feature.client.ObserveNewClientsUseCase @@ -97,7 +102,7 @@ class WireActivityViewModelTest { .withSomeCurrentSession() .arrange() - viewModel.handleDeepLink(null, {}, {}, {}) + viewModel.handleDeepLink(null, {}, {}, {}, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) } @@ -108,7 +113,7 @@ class WireActivityViewModelTest { .withNoCurrentSession() .arrange() - viewModel.handleDeepLink(null, {}, {}, {}) + viewModel.handleDeepLink(null, {}, {}, {}, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) } @@ -119,9 +124,10 @@ class WireActivityViewModelTest { val (arrangement, viewModel) = Arrangement() .withSomeCurrentSession() .withDeepLinkResult(result) + .withNoOngoingCall() .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) coVerify(exactly = 1) { arrangement.deepLinkProcessor.invoke(any()) } verify(exactly = 1) { arrangement.onDeepLinkResult(result) } } @@ -133,9 +139,10 @@ class WireActivityViewModelTest { val (arrangement, viewModel) = Arrangement() .withSomeCurrentSession() .withDeepLinkResult(result) + .withNoOngoingCall() .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } @@ -148,15 +155,30 @@ class WireActivityViewModelTest { val (arrangement, viewModel) = Arrangement() .withNoCurrentSession() .withDeepLinkResult(DeepLinkResult.CustomServerConfig("url")) + .withNoOngoingCall() .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } assertEquals(newServerConfig(1).links, viewModel.globalAppState.customBackendDialog!!.serverLinks) } + @Test + fun `given Intent with ServerConfig during an ongoing call, when handling deep links, then onCannotLoginDuringACall is called `() = runTest { + val result = DeepLinkResult.CustomServerConfig("url") + val (arrangement, viewModel) = Arrangement() + .withSomeCurrentSession() + .withDeepLinkResult(result) + .withOngoingCall() + .arrange() + + viewModel.handleDeepLink(mockedIntent(), {}, {}, {}, arrangement.onCannotLoginDuringACall) + + verify(exactly = 1) { arrangement.onCannotLoginDuringACall() } + } + @Test fun `given Intent with ServerConfig, when currentSession is absent and migration is required, then initialAppState is NOT_MIGRATED`() = runTest { @@ -167,7 +189,7 @@ class WireActivityViewModelTest { .withCurrentScreen(MutableStateFlow(CurrentScreen.Home)) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_MIGRATED, viewModel.initialAppState) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } @@ -180,23 +202,39 @@ class WireActivityViewModelTest { val (arrangement, viewModel) = Arrangement() .withSomeCurrentSession() .withDeepLinkResult(ssoLogin) + .withNoOngoingCall() .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(ssoLogin) } } + @Test + fun `given Intent with SSOLogin during an ongoing call, when handling deep links, then onCannotLoginDuringACall is called `() = runTest { + val ssoLogin = DeepLinkResult.SSOLogin.Success("cookie", "serverConfig") + val (arrangement, viewModel) = Arrangement() + .withSomeCurrentSession() + .withDeepLinkResult(ssoLogin) + .withOngoingCall() + .arrange() + + viewModel.handleDeepLink(mockedIntent(), {}, {}, {}, arrangement.onCannotLoginDuringACall) + + verify(exactly = 1) { arrangement.onCannotLoginDuringACall() } + } + @Test fun `given Intent with SSOLogin, when currentSession is absent, then initialAppState is NOT_LOGGED_IN and result SSOLogin`() = runTest { val ssoLogin = DeepLinkResult.SSOLogin.Success("cookie", "serverConfig") val (arrangement, viewModel) = Arrangement() .withNoCurrentSession() .withDeepLinkResult(ssoLogin) + .withNoOngoingCall() .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(ssoLogin) } @@ -211,7 +249,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(result) } @@ -226,7 +264,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(result) } @@ -241,7 +279,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(result) } @@ -255,7 +293,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } @@ -271,7 +309,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState) verify(exactly = 1) { arrangement.onDeepLinkResult(result) } @@ -285,7 +323,7 @@ class WireActivityViewModelTest { .withDeepLinkResult(result) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } @@ -297,7 +335,7 @@ class WireActivityViewModelTest { .withSomeCurrentSession() .arrange() - viewModel.handleDeepLink(null, {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(null, {}, {}, arrangement.onDeepLinkResult, {}) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } } @@ -333,7 +371,7 @@ class WireActivityViewModelTest { ) .arrange() - viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}) viewModel.globalAppState.conversationJoinedDialog `should be equal to` JoinConversationViaCodeState.Show( conversationName, @@ -366,7 +404,7 @@ class WireActivityViewModelTest { ) ).arrange() - viewModel.handleDeepLink(mockedIntent(), {}, arrangement.onSuccess, arrangement.onDeepLinkResult) + viewModel.handleDeepLink(mockedIntent(), {}, arrangement.onSuccess, arrangement.onDeepLinkResult, {}) viewModel.globalAppState.conversationJoinedDialog `should be equal to` null verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } @@ -651,9 +689,15 @@ class WireActivityViewModelTest { @MockK lateinit var workManager: WorkManager + @MockK + lateinit var observeEstablishedCalls: ObserveEstablishedCallsUseCase + @MockK(relaxed = true) lateinit var onDeepLinkResult: (DeepLinkResult) -> Unit + @MockK(relaxed = true) + lateinit var onCannotLoginDuringACall: () -> Unit + @MockK(relaxed = true) lateinit var onSuccess: (ConversationId) -> Unit @@ -678,7 +722,8 @@ class WireActivityViewModelTest { observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, globalDataStore = globalDataStore, observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory, - workManager = workManager + workManager = workManager, + observeEstablishedCalls = observeEstablishedCalls ) } @@ -700,6 +745,15 @@ class WireActivityViewModelTest { return this } + fun withNoOngoingCall(): Arrangement { + coEvery { observeEstablishedCalls() } returns flowOf(emptyList()) + return this + } + fun withOngoingCall(): Arrangement { + coEvery { observeEstablishedCalls() } returns flowOf(listOf(ongoingCall)) + return this + } + fun withAppUpdateRequired(result: Boolean): Arrangement = apply { coEvery { observeIfAppUpdateRequired(any()) } returns flowOf(result) } @@ -767,5 +821,17 @@ class WireActivityViewModelTest { every { it.action } returns null } } + val ongoingCall = Call( + CommonTopAppBarViewModelTest.conversationId, + CallStatus.ESTABLISHED, + true, + false, + false, + "caller-id", + "ONE_ON_ONE Name", + Conversation.Type.ONE_ON_ONE, + "otherUsername", + "team1" + ) } }