From 82a5be4dce8f75c79b323c2f6d52edd1b1f390b7 Mon Sep 17 00:00:00 2001 From: MaryamShaghaghi <122574719+MaryamShaghaghi@users.noreply.github.com> Date: Wed, 29 Nov 2023 11:23:47 +0100 Subject: [PATCH 1/3] Check network availability --- .../mullvadvpn/compose/screen/LoginScreen.kt | 1 + .../mullvadvpn/compose/state/LoginUiState.kt | 2 ++ .../net/mullvad/mullvadvpn/di/UiModule.kt | 4 +++- .../mullvadvpn/usecase/ConnectivityUseCase.kt | 17 +++++++++++++++++ .../mullvadvpn/viewmodel/LoginViewModel.kt | 14 +++++++++++++- .../resource/src/main/res/values/strings.xml | 1 + 6 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ConnectivityUseCase.kt diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index b4287aaeef90..113ef4b020b5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -295,6 +295,7 @@ private fun LoginState.supportingText(): String? { when (loginError) { LoginError.InvalidCredentials -> R.string.login_fail_description LoginError.UnableToCreateAccount -> R.string.failed_to_create_account + LoginError.NoInternetConnection -> R.string.no_internet_connection is LoginError.Unknown -> R.string.error_occurred null -> return null } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt index bcbc181b85ab..82f69e538008 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/LoginUiState.kt @@ -37,4 +37,6 @@ sealed class LoginError { data object InvalidCredentials : LoginError() data class Unknown(val reason: String) : LoginError() + + data object NoInternetConnection : LoginError() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 220097a7313a..9e35e67823a6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase +import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase import net.mullvad.mullvadvpn.usecase.EmptyPaymentUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.usecase.PaymentUseCase @@ -100,6 +101,7 @@ val uiModule = module { single { NewDeviceNotificationUseCase(get()) } single { PortRangeUseCase(get()) } single { RelayListUseCase(get(), get()) } + single { ConnectivityUseCase(get()) } single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } @@ -130,7 +132,7 @@ val uiModule = module { viewModel { ConnectViewModel(get(), get(), get(), get(), get(), get(), get()) } viewModel { DeviceListViewModel(get(), get()) } viewModel { DeviceRevokedViewModel(get(), get()) } - viewModel { LoginViewModel(get(), get(), get()) } + viewModel { LoginViewModel(get(), get(), get(), get()) } viewModel { PrivacyDisclaimerViewModel(get()) } viewModel { SelectLocationViewModel(get(), get(), get()) } viewModel { SettingsViewModel(get(), get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ConnectivityUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ConnectivityUseCase.kt new file mode 100644 index 000000000000..35983a86c0e9 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ConnectivityUseCase.kt @@ -0,0 +1,17 @@ +package net.mullvad.mullvadvpn.usecase + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities + +class ConnectivityUseCase(val context: Context) { + fun isInternetAvailable(): Boolean { + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + val network = connectivityManager.activeNetwork + val capabilities = connectivityManager.getNetworkCapabilities(network) + + return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt index 87174f2063bc..34648f1d539b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModel.kt @@ -17,7 +17,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.LoginError import net.mullvad.mullvadvpn.compose.state.LoginState -import net.mullvad.mullvadvpn.compose.state.LoginState.* +import net.mullvad.mullvadvpn.compose.state.LoginState.Idle +import net.mullvad.mullvadvpn.compose.state.LoginState.Loading +import net.mullvad.mullvadvpn.compose.state.LoginState.Success import net.mullvad.mullvadvpn.compose.state.LoginUiState import net.mullvad.mullvadvpn.constant.LOGIN_TIMEOUT_MILLIS import net.mullvad.mullvadvpn.model.AccountCreationResult @@ -25,6 +27,7 @@ import net.mullvad.mullvadvpn.model.AccountToken import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import net.mullvad.mullvadvpn.util.awaitWithTimeoutOrNull @@ -42,6 +45,7 @@ class LoginViewModel( private val accountRepository: AccountRepository, private val deviceRepository: DeviceRepository, private val newDeviceNotificationUseCase: NewDeviceNotificationUseCase, + private val connectivityUseCase: ConnectivityUseCase, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ViewModel() { private val _loginState = MutableStateFlow(LoginUiState.INITIAL.loginState) @@ -75,6 +79,10 @@ class LoginViewModel( } fun login(accountToken: String) { + if (!isInternetAvailable()) { + _loginState.value = Idle(LoginError.NoInternetConnection) + return + } _loginState.value = Loading.LoggingIn viewModelScope.launch(dispatcher) { // Ensure we always take at least MINIMUM_LOADING_SPINNER_TIME_MILLIS to show the @@ -135,4 +143,8 @@ class LoginViewModel( Idle(LoginError.UnableToCreateAccount) } } + + private fun isInternetAvailable(): Boolean { + return connectivityUseCase.isInternetAvailable() + } } diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 5b33b45f575e..0b690671ace7 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ Voucher code has already been used. An error occurred. Settings + No internet connection Account less than a day left less than a minute ago From f0bc888b3ced3f3ae48fdc15ae7dee851aa71de0 Mon Sep 17 00:00:00 2001 From: Jonatan Rhodin Date: Fri, 1 Dec 2023 10:30:25 +0100 Subject: [PATCH 2/3] Update translations --- gui/locales/messages.pot | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot index 08ecb96c0470..79d43d44b82e 100644 --- a/gui/locales/messages.pot +++ b/gui/locales/messages.pot @@ -1765,6 +1765,9 @@ msgstr "" msgid "Mullvad services unavailable" msgstr "" +msgid "No internet connection" +msgstr "" + msgid "Preferences" msgstr "" From 12b27e55131f3a534fabb5622895dfcd0193f0e2 Mon Sep 17 00:00:00 2001 From: MaryamShaghaghi <122574719+MaryamShaghaghi@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:26:14 +0100 Subject: [PATCH 3/3] Add unit test in the login view model test Co-Authored-By: Boban Sijuk <49131853+Boki91@users.noreply.github.com> --- .../viewmodel/LoginViewModelTest.kt | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt index 2ada5bf767b7..7eb35404d077 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/LoginViewModelTest.kt @@ -14,7 +14,9 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import net.mullvad.mullvadvpn.compose.state.LoginError -import net.mullvad.mullvadvpn.compose.state.LoginState.* +import net.mullvad.mullvadvpn.compose.state.LoginState.Idle +import net.mullvad.mullvadvpn.compose.state.LoginState.Loading +import net.mullvad.mullvadvpn.compose.state.LoginState.Success import net.mullvad.mullvadvpn.compose.state.LoginUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.model.AccountCreationResult @@ -24,6 +26,7 @@ import net.mullvad.mullvadvpn.model.DeviceListEvent import net.mullvad.mullvadvpn.model.LoginResult import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository +import net.mullvad.mullvadvpn.usecase.ConnectivityUseCase import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase import org.junit.Assert.assertEquals import org.junit.Before @@ -33,6 +36,7 @@ import org.junit.Test class LoginViewModelTest { @get:Rule val testCoroutineRule = TestCoroutineRule() + @MockK private lateinit var connectivityUseCase: ConnectivityUseCase @MockK private lateinit var mockedAccountRepository: AccountRepository @MockK private lateinit var mockedDeviceRepository: DeviceRepository @MockK private lateinit var mockedNewDeviceNotificationUseCase: NewDeviceNotificationUseCase @@ -42,9 +46,10 @@ class LoginViewModelTest { @Before fun setup() { + Dispatchers.setMain(UnconfinedTestDispatcher()) MockKAnnotations.init(this, relaxUnitFun = true) - + every { connectivityUseCase.isInternetAvailable() } returns true every { mockedAccountRepository.accountHistory } returns accountHistoryTestEvents every { mockedNewDeviceNotificationUseCase.newDeviceCreated() } returns Unit @@ -53,10 +58,32 @@ class LoginViewModelTest { mockedAccountRepository, mockedDeviceRepository, mockedNewDeviceNotificationUseCase, + connectivityUseCase, UnconfinedTestDispatcher() ) } + @Test + fun testIsInternetAvailableWithoutInternet() = runTest { + turbineScope { + // Arrange + every { connectivityUseCase.isInternetAvailable() } returns false + val uiStates = loginViewModel.uiState.testIn(backgroundScope) + + // Act + loginViewModel.login("") + + // Discard default item + uiStates.awaitItem() + + // Assert + assertEquals( + Idle(loginError = LoginError.NoInternetConnection), + uiStates.awaitItem().loginState + ) + } + } + @Test fun testDefaultState() = runTest { loginViewModel.uiState.test { assertEquals(LoginUiState.INITIAL, awaitItem()) }