diff options
Diffstat (limited to 'android/app/src/main')
3 files changed, 121 insertions, 33 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt index 63f17c780b..9bf8b8bf3b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/WireguardCustomPortDialog.kt @@ -3,12 +3,13 @@ package net.mullvad.mullvadvpn.compose.dialog import android.os.Parcelable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.result.EmptyResultBackNavigator @@ -18,11 +19,15 @@ import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.test.CUSTOM_PORT_DIALOG_INPUT_TEST_TAG import net.mullvad.mullvadvpn.compose.textfield.CustomPortTextField +import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.PortRange import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.util.asString -import net.mullvad.mullvadvpn.util.inAnyOf +import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogSideEffect +import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogUiState +import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel +import org.koin.androidx.compose.koinViewModel @Preview @Composable @@ -47,39 +52,44 @@ data class WireguardCustomPortNavArgs( @Destination<RootGraph>(style = DestinationStyle.Dialog::class) @Composable fun WireguardCustomPort( - navArg: WireguardCustomPortNavArgs, + @Suppress("UNUSED_PARAMETER") navArg: WireguardCustomPortNavArgs, backNavigator: ResultBackNavigator<Port?>, ) { + val viewModel = koinViewModel<WireguardCustomPortDialogViewModel>() + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + CollectSideEffectWithLifecycle(viewModel.uiSideEffect) { + when (it) { + is WireguardCustomPortDialogSideEffect.Success -> backNavigator.navigateBack(it.port) + } + } + WireguardCustomPortDialog( - initialPort = navArg.customPort, - allowedPortRanges = navArg.allowedPortRanges, - onSave = { port -> backNavigator.navigateBack(port) }, - onDismiss = backNavigator::navigateBack, + uiState, + onInputChanged = viewModel::onInputChanged, + onSavePort = viewModel::onSaveClick, + onResetPort = viewModel::onResetClick, + onDismiss = dropUnlessResumed { backNavigator.navigateBack() }, ) } @Composable fun WireguardCustomPortDialog( - initialPort: Port?, - allowedPortRanges: List<PortRange>, - onSave: (Port?) -> Unit, + state: WireguardCustomPortDialogUiState, + onInputChanged: (String) -> Unit, + onSavePort: (String) -> Unit, + onResetPort: () -> Unit, onDismiss: () -> Unit, ) { - val port = remember { mutableStateOf(initialPort?.value?.toString() ?: "") } - val isValidPort = port.value.toPortOrNull()?.inAnyOf(allowedPortRanges) ?: false - InputDialog( title = stringResource(id = R.string.custom_port_dialog_title), input = { CustomPortTextField( - value = port.value, - onSubmit = { input -> - if (isValidPort) { - onSave(input.toPortOrNull()) - } - }, - onValueChanged = { input -> port.value = input }, - isValidValue = isValidPort, + value = state.portInput, + onValueChanged = onInputChanged, + onSubmit = onSavePort, + isValidValue = state.isValidInput, maxCharLength = 5, modifier = Modifier.testTag(CUSTOM_PORT_DIALOG_INPUT_TEST_TAG).fillMaxWidth(), ) @@ -87,20 +97,13 @@ fun WireguardCustomPortDialog( message = stringResource( id = R.string.custom_port_dialog_valid_ranges, - allowedPortRanges.asString(), + state.allowedPortRanges.asString(), ), - confirmButtonEnabled = isValidPort, + confirmButtonEnabled = state.isValidInput, confirmButtonText = stringResource(id = R.string.custom_port_dialog_submit), onResetButtonText = stringResource(R.string.custom_port_dialog_remove), onBack = onDismiss, - onReset = - if (initialPort != null) { - { onSave(null) } - } else { - null - }, - onConfirm = { onSave(port.value.toPortOrNull()) }, + onReset = if (state.showResetToDefault) onResetPort else null, + onConfirm = { onSavePort(state.portInput) }, ) } - -private fun String.toPortOrNull() = toIntOrNull()?.let { Port(it) } 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 6b909d394d..32fad5614b 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 @@ -83,6 +83,7 @@ import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel import net.mullvad.mullvadvpn.viewmodel.VpnPermissionViewModel import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel +import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel import org.apache.commons.validator.routines.InetAddressValidator import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext @@ -182,6 +183,7 @@ val uiModule = module { viewModel { DeviceRevokedViewModel(get(), get()) } viewModel { MtuDialogViewModel(get(), get()) } viewModel { DnsDialogViewModel(get(), get(), get()) } + viewModel { WireguardCustomPortDialogViewModel(get()) } viewModel { LoginViewModel(get(), get(), get()) } viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) } viewModel { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt new file mode 100644 index 0000000000..b98801612e --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WireguardCustomPortDialogViewModel.kt @@ -0,0 +1,83 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.WireguardCustomPortDestination +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.lib.model.Port +import net.mullvad.mullvadvpn.lib.model.PortRange +import net.mullvad.mullvadvpn.util.inAnyOf + +class WireguardCustomPortDialogViewModel( + savedStateHandle: SavedStateHandle, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +) : ViewModel() { + private val navArgs = WireguardCustomPortDestination.argsFrom(savedStateHandle).navArg + + private val _portInput = MutableStateFlow(navArgs.customPort?.value?.toString() ?: "") + private val _isValidPort = MutableStateFlow(_portInput.value.isValidPort()) + + val uiState: StateFlow<WireguardCustomPortDialogUiState> = + combine(_portInput, _isValidPort, ::createState) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + createState(_portInput.value, _isValidPort.value), + ) + + private val _uiSideEffect = Channel<WireguardCustomPortDialogSideEffect>() + val uiSideEffect = _uiSideEffect.receiveAsFlow() + + private fun createState(portInput: String, isValidPortInput: Boolean) = + WireguardCustomPortDialogUiState( + portInput = portInput, + isValidInput = isValidPortInput, + allowedPortRanges = navArgs.allowedPortRanges, + showResetToDefault = navArgs.customPort != null, + ) + + fun onInputChanged(value: String) { + _portInput.value = value + _isValidPort.value = value.isValidPort() + } + + fun onSaveClick(portValue: String) = + viewModelScope.launch(dispatcher) { + val port = portValue.parseValidPort() ?: return@launch + _uiSideEffect.send(WireguardCustomPortDialogSideEffect.Success(port)) + } + + fun onResetClick() { + viewModelScope.launch(dispatcher) { + _uiSideEffect.send(WireguardCustomPortDialogSideEffect.Success(null)) + } + } + + private fun String.isValidPort(): Boolean = parseValidPort() != null + + private fun String.parseValidPort(): Port? = + Port.fromString(this).getOrNull()?.takeIf { port -> + port.inAnyOf(navArgs.allowedPortRanges) + } +} + +sealed interface WireguardCustomPortDialogSideEffect { + data class Success(val port: Port?) : WireguardCustomPortDialogSideEffect +} + +data class WireguardCustomPortDialogUiState( + val portInput: String, + val isValidInput: Boolean, + val allowedPortRanges: List<PortRange>, + val showResetToDefault: Boolean, +) |
