diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt index 0641998f9bd1..b295bfea3b0c 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialogTest.kt @@ -13,6 +13,7 @@ import io.mockk.mockk import io.mockk.verify import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension import net.mullvad.mullvadvpn.compose.setContentWithTheme +import net.mullvad.mullvadvpn.viewmodel.MtuDialogUiState import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension @@ -31,13 +32,17 @@ class MtuDialogTest { @SuppressLint("ComposableNaming") @Composable private fun testMtuDialog( - mtuInitial: Int? = null, - onSaveMtu: (Int) -> Unit = { _ -> }, + mtuInput: String = "", + isValidInput: Boolean = true, + showResetButton: Boolean = true, + onInputChanged: (String) -> Unit = { _ -> }, + onSaveMtu: (String) -> Unit = { _ -> }, onResetMtu: () -> Unit = {}, onDismiss: () -> Unit = {}, ) { MtuDialog( - mtuInitial = mtuInitial, + MtuDialogUiState(mtuInput, isValidInput, showResetButton), + onInputChanged = onInputChanged, onSaveMtu = onSaveMtu, onResetMtu = onResetMtu, onDismiss = onDismiss @@ -60,12 +65,12 @@ class MtuDialogTest { // Arrange setContentWithTheme { testMtuDialog( - mtuInitial = VALID_DUMMY_MTU_VALUE, + mtuInput = VALID_DUMMY_MTU_VALUE, ) } // Assert - onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists() + onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() } @Test @@ -74,22 +79,22 @@ class MtuDialogTest { // Arrange setContentWithTheme { testMtuDialog( - null, + "", ) } // Act - onNodeWithText(EMPTY_STRING).performTextInput(VALID_DUMMY_MTU_VALUE.toString()) + onNodeWithText(EMPTY_STRING).performTextInput(VALID_DUMMY_MTU_VALUE) // Assert - onNodeWithText(VALID_DUMMY_MTU_VALUE.toString()).assertExists() + onNodeWithText(VALID_DUMMY_MTU_VALUE).assertExists() } @Test fun testMtuDialogSubmitOfValidValue() = composeExtension.use { // Arrange - val mockedSubmitHandler: (Int) -> Unit = mockk(relaxed = true) + val mockedSubmitHandler: (String) -> Unit = mockk(relaxed = true) setContentWithTheme { testMtuDialog( VALID_DUMMY_MTU_VALUE, @@ -125,7 +130,7 @@ class MtuDialogTest { val mockedClickHandler: () -> Unit = mockk(relaxed = true) setContentWithTheme { testMtuDialog( - mtuInitial = VALID_DUMMY_MTU_VALUE, + mtuInput = VALID_DUMMY_MTU_VALUE, onResetMtu = mockedClickHandler, ) } @@ -157,7 +162,7 @@ class MtuDialogTest { companion object { private const val EMPTY_STRING = "" - private const val VALID_DUMMY_MTU_VALUE = 1337 - private const val INVALID_DUMMY_MTU_VALUE = 1111 + private const val VALID_DUMMY_MTU_VALUE = "1337" + private const val INVALID_DUMMY_MTU_VALUE = "1111" } } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt index 471e39c38f28..c7bf129ff673 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt @@ -21,6 +21,7 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_NUMBE import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_TEST_TAG import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG import net.mullvad.mullvadvpn.model.Constraint +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.model.Port import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.QuantumResistantState @@ -49,7 +50,7 @@ class VpnSettingsScreenTest { ) } - apply { onNodeWithText("Auto-connect").assertExists() } + onNodeWithText("Auto-connect").assertExists() onNodeWithTag(LAZY_LIST_TEST_TAG) .performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) @@ -67,7 +68,10 @@ class VpnSettingsScreenTest { // Arrange setContentWithTheme { VpnSettingsScreen( - state = VpnSettingsUiState.createDefault(mtu = VALID_DUMMY_MTU_VALUE), + state = + VpnSettingsUiState.createDefault( + mtu = Mtu.fromString(VALID_DUMMY_MTU_VALUE).getOrNull()!! + ), ) } @@ -360,7 +364,7 @@ class VpnSettingsScreenTest { fun testMtuClick() = composeExtension.use { // Arrange - val mockedClickHandler: (Int?) -> Unit = mockk(relaxed = true) + val mockedClickHandler: (Mtu?) -> Unit = mockk(relaxed = true) setContentWithTheme { VpnSettingsScreen( state = VpnSettingsUiState.createDefault(), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt index d949f2a70827..094914afe93b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt @@ -13,16 +13,17 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.constant.MTU_MAX_VALUE import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.model.Mtu @Preview @Composable private fun PreviewMtuComposeCell() { - AppTheme { MtuComposeCell(mtuValue = "1300", onEditMtu = {}) } + AppTheme { MtuComposeCell(mtuValue = Mtu(1300), onEditMtu = {}) } } @Composable fun MtuComposeCell( - mtuValue: String, + mtuValue: Mtu?, onEditMtu: () -> Unit, ) { val titleModifier = Modifier @@ -45,10 +46,10 @@ private fun MtuTitle(modifier: Modifier) { } @Composable -private fun MtuBodyView(mtuValue: String, modifier: Modifier) { +private fun MtuBodyView(mtuValue: Mtu?, modifier: Modifier) { Row(modifier = modifier.wrapContentWidth1().wrapContentHeight()) { Text( - text = mtuValue.ifEmpty { stringResource(id = R.string.hint_default) }, + text = mtuValue?.value?.toString() ?: stringResource(id = R.string.hint_default), color = MaterialTheme.colorScheme.onPrimary ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt index 9c094ffab4d4..35df5c002e26 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt @@ -8,8 +8,8 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -27,22 +27,25 @@ import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaDescription -import net.mullvad.mullvadvpn.util.isValidMtu +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.viewmodel.MtuDialogSideEffect +import net.mullvad.mullvadvpn.viewmodel.MtuDialogUiState import net.mullvad.mullvadvpn.viewmodel.MtuDialogViewModel import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf @Preview @Composable private fun PreviewMtuDialog() { - AppTheme { MtuDialog(mtuInitial = 1234, EmptyResultBackNavigator()) } + AppTheme { MtuDialog(mtuInitial = Mtu(1234), EmptyResultBackNavigator()) } } @Destination(style = DestinationStyle.Dialog::class) @Composable -fun MtuDialog(mtuInitial: Int?, navigator: ResultBackNavigator) { - val viewModel = koinViewModel() +fun MtuDialog(mtuInitial: Mtu?, navigator: ResultBackNavigator) { + val viewModel = koinViewModel(parameters = { parametersOf(mtuInitial) }) + val uiState by viewModel.uiState.collectAsState() LaunchedEffectCollect(viewModel.uiSideEffect) { when (it) { MtuDialogSideEffect.Complete -> navigator.navigateBack(result = true) @@ -50,24 +53,22 @@ fun MtuDialog(mtuInitial: Int?, navigator: ResultBackNavigator) { } } MtuDialog( - mtuInitial = mtuInitial, + uiState, + onInputChanged = viewModel::onInputChanged, onSaveMtu = viewModel::onSaveClick, onResetMtu = viewModel::onRestoreClick, - onDismiss = navigator::navigateBack + onDismiss = { navigator.navigateBack(true) } ) } @Composable fun MtuDialog( - mtuInitial: Int?, - onSaveMtu: (Int) -> Unit, + state: MtuDialogUiState, + onInputChanged: (String) -> Unit, + onSaveMtu: (String) -> Unit, onResetMtu: () -> Unit, onDismiss: () -> Unit, ) { - - val mtu = remember { mutableStateOf(mtuInitial?.toString() ?: "") } - val isValidMtu = mtu.value.toIntOrNull()?.isValidMtu() == true - AlertDialog( onDismissRequest = onDismiss, title = { @@ -79,18 +80,13 @@ fun MtuDialog( text = { Column { MtuTextField( - value = mtu.value, - onValueChanged = { newMtuValue -> mtu.value = newMtuValue }, - onSubmit = { newMtuValue -> - val mtuInt = newMtuValue.toIntOrNull() - if (mtuInt?.isValidMtu() == true) { - onSaveMtu(mtuInt) - } - }, + value = state.mtuInput, + onValueChanged = onInputChanged, + onSubmit = onSaveMtu, isEnabled = true, placeholderText = stringResource(R.string.enter_value_placeholder), maxCharLength = 4, - isValidValue = isValidMtu, + isValidValue = state.isValidInput, modifier = Modifier.fillMaxWidth() ) @@ -111,17 +107,12 @@ fun MtuDialog( Column(verticalArrangement = Arrangement.spacedBy(Dimens.buttonSpacing)) { PrimaryButton( modifier = Modifier.fillMaxWidth(), - isEnabled = isValidMtu, + isEnabled = state.isValidInput, text = stringResource(R.string.submit_button), - onClick = { - val mtuInt = mtu.value.toIntOrNull() - if (mtuInt?.isValidMtu() == true) { - onSaveMtu(mtuInt) - } - } + onClick = { onSaveMtu(state.mtuInput) } ) - if (mtuInitial != null) { + if (state.showResetToDefault) { NegativeButton( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.reset_to_default_button), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt index d8c66a021306..4d19095a6e59 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt index 0c5369b0fdb6..bdbd901789d1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt @@ -87,6 +87,7 @@ import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.model.Constraint +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.model.Port import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.QuantumResistantState @@ -107,7 +108,7 @@ private fun PreviewVpnSettings() { state = VpnSettingsUiState.createDefault( isAutoConnectEnabled = true, - mtu = "1337", + mtu = Mtu(1337), isCustomDnsEnabled = true, customDnsItems = listOf(CustomDnsItem("0.0.0.0", false)), ), @@ -287,7 +288,7 @@ fun VpnSettingsScreen( onToggleBlockAdultContent: (Boolean) -> Unit = {}, onToggleBlockGambling: (Boolean) -> Unit = {}, onToggleBlockSocialMedia: (Boolean) -> Unit = {}, - navigateToMtuDialog: (mtu: Int?) -> Unit = {}, + navigateToMtuDialog: (mtu: Mtu?) -> Unit = {}, navigateToDns: (index: Int?, address: String?) -> Unit = { _, _ -> }, onToggleDnsClick: (Boolean) -> Unit = {}, onBackClick: () -> Unit = {}, @@ -617,10 +618,7 @@ fun VpnSettingsScreen( } item { - MtuComposeCell( - mtuValue = state.mtu, - onEditMtu = { navigateToMtuDialog(state.mtu.toIntOrNull()) } - ) + MtuComposeCell(mtuValue = state.mtu, onEditMtu = { navigateToMtuDialog(state.mtu) }) } item { MtuSubtitle(modifier = Modifier.testTag(LAZY_LIST_LAST_ITEM_TEST_TAG)) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt index 4b07e330dbea..348d0f7632f9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.model.Constraint import net.mullvad.mullvadvpn.model.DefaultDnsOptions +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.model.Port import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.QuantumResistantState @@ -9,7 +10,7 @@ import net.mullvad.mullvadvpn.model.SelectedObfuscation import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem data class VpnSettingsUiState( - val mtu: String, + val mtu: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, val isCustomDnsEnabled: Boolean, @@ -25,7 +26,7 @@ data class VpnSettingsUiState( companion object { fun createDefault( - mtu: String = "", + mtu: Mtu? = null, isAutoConnectEnabled: Boolean = false, isLocalNetworkSharingEnabled: Boolean = false, isCustomDnsEnabled: Boolean = false, 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 2b78272d92ca..e3b335bf5bd2 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 @@ -175,7 +175,7 @@ val uiModule = module { } viewModel { parameters -> DeviceListViewModel(get(), parameters.get()) } viewModel { DeviceRevokedViewModel(get(), get()) } - viewModel { MtuDialogViewModel(get()) } + viewModel { parameters -> MtuDialogViewModel(get(), parameters.getOrNull()) } viewModel { parameters -> DnsDialogViewModel(get(), get(), parameters.getOrNull(), parameters.getOrNull()) } @@ -185,7 +185,7 @@ val uiModule = module { viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) } viewModel { SplashViewModel(get(), get()) } viewModel { VoucherDialogViewModel(get()) } - viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } + viewModel { VpnSettingsViewModel(get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) } viewModel { ReportProblemViewModel(get(), get()) } viewModel { ViewLogsViewModel(get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt index 78a5af715972..b52d67ca2b5d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt @@ -12,6 +12,7 @@ import net.mullvad.mullvadvpn.model.CustomDnsOptions import net.mullvad.mullvadvpn.model.DefaultDnsOptions import net.mullvad.mullvadvpn.model.DnsOptions import net.mullvad.mullvadvpn.model.DnsState +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.model.ObfuscationSettings import net.mullvad.mullvadvpn.model.QuantumResistantState import net.mullvad.mullvadvpn.model.Settings @@ -51,7 +52,7 @@ class SettingsRepository( suspend fun addCustomDns(address: InetAddress) = managementService.addCustomDns(address) - suspend fun setWireguardMtu(value: Int) = managementService.setWireguardMtu(value) + suspend fun setWireguardMtu(mtu: Mtu) = managementService.setWireguardMtu(mtu.value) suspend fun clearWireguardMtu() = managementService.setWireguardMtu(null) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt deleted file mode 100644 index a1a1d54b3671..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt +++ /dev/null @@ -1,5 +0,0 @@ -package net.mullvad.mullvadvpn.util - -fun Int.isValidMtu(): Boolean { - return this in 1280..1420 -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt index b31af3603024..f2a68a7277f4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt @@ -5,29 +5,56 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.repository.SettingsRepository -import net.mullvad.mullvadvpn.util.isValidMtu class MtuDialogViewModel( private val repository: SettingsRepository, + private val initialMtu: Mtu?, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ViewModel() { + private val _mtuInput = MutableStateFlow(initialMtu?.value?.toString() ?: "") + private val _isValidMtu = MutableStateFlow(true) + val uiState: StateFlow = + combine(_mtuInput, _isValidMtu, ::createState) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + createState(_mtuInput.value, _isValidMtu.value) + ) + private val _uiSideEffect = Channel() val uiSideEffect = _uiSideEffect.receiveAsFlow() - fun onSaveClick(mtuValue: Int) = + private fun createState(mtuInput: String, isValidMtuInput: Boolean) = + MtuDialogUiState( + mtuInput = mtuInput, + isValidInput = isValidMtuInput, + showResetToDefault = initialMtu != null + ) + + fun onInputChanged(value: String) { + _mtuInput.value = value + _isValidMtu.value = Mtu.fromString(value).isRight() + } + + fun onSaveClick(mtuValue: String) = viewModelScope.launch(dispatcher) { - if (mtuValue.isValidMtu()) { - repository - .setWireguardMtu(mtuValue) - .fold( - { _uiSideEffect.send(MtuDialogSideEffect.Error) }, - { _uiSideEffect.send(MtuDialogSideEffect.Complete) } - ) - } + val mtu = Mtu.fromString(mtuValue).getOrNull() ?: return@launch + repository + .setWireguardMtu(mtu) + .fold( + { _uiSideEffect.send(MtuDialogSideEffect.Error) }, + { _uiSideEffect.send(MtuDialogSideEffect.Complete) } + ) } fun onRestoreClick() = @@ -46,3 +73,9 @@ sealed interface MtuDialogSideEffect { data object Error : MtuDialogSideEffect } + +data class MtuDialogUiState( + val mtuInput: String, + val isValidInput: Boolean, + val showResetToDefault: Boolean +) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt index e0a799885dc7..e9401d42e8be 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModel.kt @@ -25,7 +25,8 @@ class SettingsViewModel( SettingsUiState( isLoggedIn = accountState is DeviceState.LoggedIn, appVersion = BuildConfig.VERSION_NAME, - isUpdateAvailable = versionInfo.let { it.isSupported.not() || it.isUpdateAvailable }, + isUpdateAvailable = + versionInfo.let { it.isSupported.not() || it.isUpdateAvailable }, isPlayBuild = isPlayBuild ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt index 598079dadba5..c06fd9603f10 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt @@ -62,7 +62,7 @@ class VpnSettingsViewModel( portRanges, customWgPort -> VpnSettingsViewModelState( - mtuValue = settings?.mtuString() ?: "", + mtuValue = settings?.tunnelOptions?.wireguard?.mtu, isAutoConnectEnabled = settings?.autoConnect ?: false, isLocalNetworkSharingEnabled = settings?.allowLan ?: false, isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false, @@ -271,8 +271,6 @@ class VpnSettingsViewModel( } } - private fun Settings.mtuString() = tunnelOptions.wireguard.mtu?.toString() ?: EMPTY_STRING - private fun Settings.quantumResistant() = tunnelOptions.wireguard.quantumResistant private fun Settings.isCustomDnsEnabled() = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt index f18b58d7ddc9..0622d4606008 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt @@ -3,13 +3,14 @@ package net.mullvad.mullvadvpn.viewmodel import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.model.Constraint import net.mullvad.mullvadvpn.model.DefaultDnsOptions +import net.mullvad.mullvadvpn.model.Mtu import net.mullvad.mullvadvpn.model.Port import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.QuantumResistantState import net.mullvad.mullvadvpn.model.SelectedObfuscation data class VpnSettingsViewModelState( - val mtuValue: String, + val mtuValue: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, val isCustomDnsEnabled: Boolean, @@ -39,11 +40,9 @@ data class VpnSettingsViewModelState( ) companion object { - private const val EMPTY_STRING = "" - fun default() = VpnSettingsViewModelState( - mtuValue = EMPTY_STRING, + mtuValue = null, isAutoConnectEnabled = false, isLocalNetworkSharingEnabled = false, isCustomDnsEnabled = false, diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt index 0266d46c2b2b..2d39d5ea2b9f 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/OutOfTimeUseCaseTest.kt @@ -9,7 +9,6 @@ import kotlin.time.Duration.Companion.days import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Mtu.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Mtu.kt index 5ba4f840736a..fe4ad59ccd7e 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Mtu.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Mtu.kt @@ -1,6 +1,32 @@ package net.mullvad.mullvadvpn.model import android.os.Parcelable +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure import kotlinx.parcelize.Parcelize -@JvmInline @Parcelize value class Mtu(val value: Int) : Parcelable +@JvmInline +@Parcelize +value class Mtu(val value: Int) : Parcelable { + companion object { + fun fromString(value: String): Either = either { + val number = value.toIntOrNull() ?: raise(ParseMtuError.NotANumber) + ensure(number in MIN_MTU..MAX_MTU) { ParseMtuError.OutOfRange(number) } + Mtu(number) + } + + private fun Int.isValidMtu(): Boolean { + return this in 1280..1420 + } + + const val MIN_MTU = 1280 + const val MAX_MTU = 1420 + } +} + +sealed interface ParseMtuError { + data object NotANumber : ParseMtuError + + data class OutOfRange(val number: Int) : ParseMtuError +} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt index 0e1c65787e62..8e117594b224 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/notifications/tunnelstate/TunnelStateNotificationProvider.kt @@ -1,6 +1,5 @@ package net.mullvad.mullvadvpn.service.notifications.tunnelstate -import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted