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()) }