Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Check network availability #5537

Merged
merged 3 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ sealed class LoginError {
data object InvalidCredentials : LoginError()

data class Unknown(val reason: String) : LoginError()

data object NoInternetConnection : LoginError()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()) }

Expand Down Expand Up @@ -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()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ 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
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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -135,4 +143,8 @@ class LoginViewModel(
Idle(LoginError.UnableToCreateAccount)
}
}

private fun isInternetAvailable(): Boolean {
return connectivityUseCase.isInternetAvailable()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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()) }
Expand Down
1 change: 1 addition & 0 deletions android/lib/resource/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<string name="voucher_already_used">Voucher code has already been used.</string>
<string name="error_occurred">An error occurred.</string>
<string name="settings">Settings</string>
<string name="no_internet_connection">No internet connection</string>
<string name="settings_account">Account</string>
<string name="less_than_a_day_left">less than a day left</string>
<string name="less_than_a_minute_ago">less than a minute ago</string>
Expand Down
3 changes: 3 additions & 0 deletions gui/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -1765,6 +1765,9 @@ msgstr ""
msgid "Mullvad services unavailable"
msgstr ""

msgid "No internet connection"
msgstr ""

msgid "Preferences"
msgstr ""

Expand Down
Loading