diff options
| author | saber safavi <saber.safavi@codic.se> | 2023-03-17 15:15:49 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2023-03-20 16:21:29 +0100 |
| commit | 6e15ab43e1620a5fa291322860127818b8de0cbe (patch) | |
| tree | 0eb2ca4454f40975914d55cc237d9cd28a9d14c6 /android | |
| parent | a168948dbd2348461da429dfc23e5a1e0f086bd2 (diff) | |
| download | mullvadvpn-6e15ab43e1620a5fa291322860127818b8de0cbe.tar.xz mullvadvpn-6e15ab43e1620a5fa291322860127818b8de0cbe.zip | |
Add advanced settings vm and repo
Diffstat (limited to 'android')
7 files changed, 451 insertions, 3 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt new file mode 100644 index 0000000000..ce554115d2 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt @@ -0,0 +1,34 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem +import net.mullvad.mullvadvpn.viewmodel.StagedDns + +sealed interface AdvancedSettingsUiState { + val mtu: String + val isCustomDnsEnabled: Boolean + val customDnsItems: List<CustomDnsItem> + val isAllowLanEnabled: Boolean + + data class DefaultUiState( + override val mtu: String = "", + override val isCustomDnsEnabled: Boolean = false, + override val isAllowLanEnabled: Boolean = false, + override val customDnsItems: List<CustomDnsItem> = listOf() + ) : AdvancedSettingsUiState + + data class MtuDialogUiState( + override val mtu: String, + override val isCustomDnsEnabled: Boolean, + override val isAllowLanEnabled: Boolean, + override val customDnsItems: List<CustomDnsItem>, + val mtuEditValue: String + ) : AdvancedSettingsUiState + + data class DnsDialogUiState( + override val mtu: String, + override val isCustomDnsEnabled: Boolean, + override val isAllowLanEnabled: Boolean, + override val customDnsItems: List<CustomDnsItem>, + val stagedDns: StagedDns, + ) : AdvancedSettingsUiState +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/MtuConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/MtuConstant.kt new file mode 100644 index 0000000000..b6d04b87b5 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/MtuConstant.kt @@ -0,0 +1,4 @@ +package net.mullvad.mullvadvpn.constant + +const val MTU_MIN_VALUE = 1280 +const val MTU_MAX_VALUE = 1420 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 bbe12baf2e..4a95b2046c 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 @@ -13,6 +13,7 @@ import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.ui.notification.AccountExpiryNotification import net.mullvad.mullvadvpn.ui.notification.TunnelStateNotification import net.mullvad.mullvadvpn.ui.notification.VersionInfoNotification @@ -20,6 +21,7 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling import net.mullvad.mullvadvpn.util.ChangelogDataProvider import net.mullvad.mullvadvpn.util.IChangelogDataProvider +import net.mullvad.mullvadvpn.viewmodel.AdvancedSettingsViewModel import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel @@ -27,6 +29,7 @@ import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel +import org.apache.commons.validator.routines.InetAddressValidator import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel @@ -56,6 +59,7 @@ val uiModule = module { } single { ServiceConnectionManager(androidContext()) } + single { InetAddressValidator.getInstance() } single { androidContext().resources } single { androidContext().assets } @@ -75,6 +79,7 @@ val uiModule = module { ) ) } + single { SettingsRepository(get()) } single<IChangelogDataProvider> { ChangelogDataProvider(get()) } @@ -91,6 +96,12 @@ val uiModule = module { ) } viewModel { PrivacyDisclaimerViewModel(get()) } + viewModel { + AdvancedSettingsViewModel( + repository = get(), + inetAddressValidator = get() + ) + } } const val APPS_SCOPE = "APPS_SCOPE" 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 926c3543d3..59d42cf476 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 @@ -1,13 +1,58 @@ package net.mullvad.mullvadvpn.repository +import java.net.InetAddress +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +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.Settings import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.customDns +import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener +import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier +import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault class SettingsRepository( - private val serviceConnectionManager: ServiceConnectionManager + private val serviceConnectionManager: ServiceConnectionManager, + dispatcher: CoroutineDispatcher = Dispatchers.IO ) { - fun setDnsOptions(dnsOptions: DnsOptions) { - serviceConnectionManager.customDns()?.setDnsOptions(dnsOptions) + val settingsUpdates: StateFlow<Settings?> = serviceConnectionManager.connectionState + .flatMapReadyConnectionOrDefault(flowOf()) { state -> + callbackFlowFromNotifier(state.container.settingsListener.settingsNotifier) + } + .onStart { serviceConnectionManager.settingsListener()?.settingsNotifier?.latestEvent } + .stateIn( + CoroutineScope(dispatcher), + SharingStarted.WhileSubscribed(), + null + ) + + fun setDnsOptions( + isCustomDnsEnabled: Boolean, + dnsList: List<InetAddress> + ) { + serviceConnectionManager.customDns()?.setDnsOptions( + dnsOptions = DnsOptions( + state = if (isCustomDnsEnabled) DnsState.Custom else DnsState.Default, + customOptions = CustomDnsOptions(ArrayList(dnsList)), + defaultOptions = DefaultDnsOptions() + ) + ) + } + + fun isLocalNetworkSharingEnabled(): Boolean { + return serviceConnectionManager.settingsListener()?.allowLan ?: false + } + + fun setWireguardMtu(value: Int?) { + serviceConnectionManager.settingsListener()?.wireguardMtu = value } } 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 new file mode 100644 index 0000000000..a1a1d54b36 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt @@ -0,0 +1,5 @@ +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/AdvancedSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModel.kt new file mode 100644 index 0000000000..7d456fb680 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModel.kt @@ -0,0 +1,249 @@ +package net.mullvad.mullvadvpn.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import java.net.InetAddress +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.AdvancedSettingsUiState +import net.mullvad.mullvadvpn.model.DnsState +import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.repository.SettingsRepository +import net.mullvad.mullvadvpn.util.isValidMtu +import org.apache.commons.validator.routines.InetAddressValidator + +class AdvancedSettingsViewModel( + private val repository: SettingsRepository, + private val inetAddressValidator: InetAddressValidator, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : ViewModel() { + + private val dialogState = + MutableStateFlow<AdvancedSettingsDialogState>(AdvancedSettingsDialogState.NoDialog) + + private val vmState = combine( + repository.settingsUpdates, + dialogState + ) { settings, interaction -> + AdvancedSettingsViewModelState( + mtuValue = settings?.mtuString() ?: "", + isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false, + customDnsList = settings?.addresses()?.asStringAddressList() ?: listOf(), + isAllowLanEnabled = settings?.allowLan ?: false, + dialogState = interaction + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + AdvancedSettingsViewModelState.default() + ) + + val uiState = vmState + .map(AdvancedSettingsViewModelState::toUiState) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + AdvancedSettingsUiState.DefaultUiState() + ) + + fun onMtuCellClick() { + dialogState.update { AdvancedSettingsDialogState.MtuDialog(vmState.value.mtuValue) } + } + + fun onMtuInputChange(value: String) { + dialogState.update { AdvancedSettingsDialogState.MtuDialog(value) } + } + + fun onSaveMtuClick() = viewModelScope.launch(dispatcher) { + val dialog = dialogState.value as? AdvancedSettingsDialogState.MtuDialog + dialog?.mtuEditValue?.toIntOrNull()?.takeIf { it.isValidMtu() }?.let { mtu -> + repository.setWireguardMtu(mtu) + } + hideDialog() + } + + fun onRestoreMtuClick() = viewModelScope.launch(dispatcher) { + repository.setWireguardMtu(null) + hideDialog() + } + + fun onCancelDialogClick() { + hideDialog() + } + + fun onDnsClick(index: Int? = null) { + val stagedDns = if (index == null) { + StagedDns.NewDns(CustomDnsItem.default()) + } else { + vmState.value.customDnsList.getOrNull(index)?.let { listItem -> + StagedDns.EditDns( + item = listItem, + index = index + ) + } + } + + if (stagedDns != null) { + dialogState.update { AdvancedSettingsDialogState.DnsDialog(stagedDns) } + } + } + + fun onDnsInputChange(ipAddress: String) { + dialogState.update { state -> + val dialog = state as? AdvancedSettingsDialogState.DnsDialog ?: return + + val error = when { + ipAddress.isBlank() || ipAddress.isValidIp().not() -> { + StagedDns.ValidationResult.InvalidAddress + } + ipAddress.isDuplicateDns((state.stagedDns as? StagedDns.EditDns)?.index) -> { + StagedDns.ValidationResult.DuplicateAddress + } + else -> StagedDns.ValidationResult.Success + } + + return@update AdvancedSettingsDialogState.DnsDialog( + stagedDns = if (dialog.stagedDns is StagedDns.EditDns) { + StagedDns.EditDns( + item = CustomDnsItem( + address = ipAddress, + isLocal = ipAddress.isLocalAddress() + ), + validationResult = error, + index = dialog.stagedDns.index + ) + } else { + StagedDns.NewDns( + item = CustomDnsItem( + address = ipAddress, + isLocal = ipAddress.isLocalAddress() + ), + validationResult = error + ) + } + ) + } + } + + fun onSaveDnsClick() = viewModelScope.launch(dispatcher) { + val dialog = + vmState.value.dialogState as? AdvancedSettingsDialogState.DnsDialog ?: return@launch + + if (dialog.stagedDns.isValid().not()) return@launch + + val updatedList = + vmState.value.customDnsList.toMutableList() + .map { it.address } + .toMutableList() + .let { activeList -> + if (dialog.stagedDns is StagedDns.EditDns) { + activeList + .apply { + set(dialog.stagedDns.index, dialog.stagedDns.item.address) + } + .asInetAddressList() + } else { + activeList + .apply { + add(dialog.stagedDns.item.address) + } + .asInetAddressList() + } + } + + repository.setDnsOptions( + isCustomDnsEnabled = true, + dnsList = updatedList + ) + + hideDialog() + } + + fun onToggleDnsClick(isEnabled: Boolean) = viewModelScope.launch(dispatcher) { + repository.setDnsOptions( + isEnabled, + dnsList = vmState.value.customDnsList + .map { it.address } + .asInetAddressList() + ) + } + + fun onRemoveDnsClick() = viewModelScope.launch(dispatcher) { + val dialog = vmState.value.dialogState as? AdvancedSettingsDialogState.DnsDialog + ?: return@launch + + val updatedList = vmState.value.customDnsList.toMutableList() + .filter { + it.address != dialog.stagedDns.item.address + } + .map { it.address } + .asInetAddressList() + + repository.setDnsOptions( + isCustomDnsEnabled = vmState.value.isCustomDnsEnabled && updatedList.isNotEmpty(), + dnsList = updatedList + ) + + hideDialog() + } + + private fun hideDialog() { + dialogState.update { AdvancedSettingsDialogState.NoDialog } + } + + private fun String.isDuplicateDns(stagedIndex: Int? = null): Boolean { + return vmState.value.customDnsList + .filterIndexed { index, listItem -> + index != stagedIndex && listItem.address == this + } + .isNotEmpty() + } + + private fun List<String>.asInetAddressList(): List<InetAddress> { + return try { + map { InetAddress.getByName(it) } + } catch (ex: Exception) { + Log.e("mullvad", "Error parsing the DNS address list.") + emptyList() + } + } + + private fun List<InetAddress>.asStringAddressList(): List<CustomDnsItem> { + return map { + CustomDnsItem( + address = it.hostAddress ?: EMPTY_STRING, + isLocal = it.isLocalAddress() + ) + } + } + + private fun Settings.mtuString() = tunnelOptions.wireguard.mtu?.toString() ?: EMPTY_STRING + + private fun Settings.isCustomDnsEnabled() = tunnelOptions.dnsOptions.state == DnsState.Custom + + private fun Settings.addresses() = tunnelOptions.dnsOptions.customOptions.addresses + + private fun String.isValidIp(): Boolean { + return inetAddressValidator.isValid(this) + } + + private fun String.isLocalAddress(): Boolean { + return isValidIp() && InetAddress.getByName(this).isLocalAddress() + } + + private fun InetAddress.isLocalAddress(): Boolean { + return isLinkLocalAddress || isSiteLocalAddress + } + + companion object { + private const val EMPTY_STRING = "" + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModelState.kt new file mode 100644 index 0000000000..4db0c012fd --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModelState.kt @@ -0,0 +1,100 @@ +package net.mullvad.mullvadvpn.viewmodel + +import net.mullvad.mullvadvpn.compose.state.AdvancedSettingsUiState + +data class AdvancedSettingsViewModelState( + val mtuValue: String, + val isCustomDnsEnabled: Boolean, + val isAllowLanEnabled: Boolean, + val customDnsList: List<CustomDnsItem>, + val dialogState: AdvancedSettingsDialogState +) { + fun toUiState(): AdvancedSettingsUiState { + return when (dialogState) { + is AdvancedSettingsDialogState.MtuDialog -> AdvancedSettingsUiState.MtuDialogUiState( + mtu = mtuValue, + isCustomDnsEnabled = isCustomDnsEnabled, + isAllowLanEnabled = isAllowLanEnabled, + customDnsItems = customDnsList, + mtuEditValue = dialogState.mtuEditValue, + ) + is AdvancedSettingsDialogState.DnsDialog -> AdvancedSettingsUiState.DnsDialogUiState( + mtu = mtuValue, + isCustomDnsEnabled = isCustomDnsEnabled, + isAllowLanEnabled = isAllowLanEnabled, + customDnsItems = customDnsList, + stagedDns = dialogState.stagedDns, + ) + else -> AdvancedSettingsUiState.DefaultUiState( + mtu = mtuValue, + isCustomDnsEnabled = isCustomDnsEnabled, + isAllowLanEnabled = isAllowLanEnabled, + customDnsItems = customDnsList, + ) + } + } + + companion object { + private const val EMPTY_STRING = "" + + fun default() = AdvancedSettingsViewModelState( + mtuValue = EMPTY_STRING, + isCustomDnsEnabled = false, + customDnsList = listOf(), + isAllowLanEnabled = false, + dialogState = AdvancedSettingsDialogState.NoDialog + ) + } +} + +sealed class AdvancedSettingsDialogState { + object NoDialog : AdvancedSettingsDialogState() + + data class MtuDialog( + val mtuEditValue: String + ) : AdvancedSettingsDialogState() + + data class DnsDialog( + val stagedDns: StagedDns + ) : AdvancedSettingsDialogState() +} + +sealed interface StagedDns { + val item: CustomDnsItem + val validationResult: ValidationResult + + data class NewDns( + override val item: CustomDnsItem, + override val validationResult: ValidationResult = ValidationResult.Success, + ) : StagedDns + + data class EditDns( + override val item: CustomDnsItem, + override val validationResult: ValidationResult = ValidationResult.Success, + val index: Int + ) : StagedDns + + sealed class ValidationResult { + object Success : ValidationResult() + object InvalidAddress : ValidationResult() + object DuplicateAddress : ValidationResult() + } + + fun isValid() = (validationResult is ValidationResult.Success) +} + +data class CustomDnsItem( + val address: String, + val isLocal: Boolean +) { + companion object { + private const val EMPTY_STRING = "" + + fun default(): CustomDnsItem { + return CustomDnsItem( + address = EMPTY_STRING, + isLocal = false + ) + } + } +} |
