summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorsaber safavi <saber.safavi@codic.se>2023-03-17 15:15:49 +0100
committerAlbin <albin@mullvad.net>2023-03-20 16:21:29 +0100
commit6e15ab43e1620a5fa291322860127818b8de0cbe (patch)
tree0eb2ca4454f40975914d55cc237d9cd28a9d14c6 /android
parenta168948dbd2348461da429dfc23e5a1e0f086bd2 (diff)
downloadmullvadvpn-6e15ab43e1620a5fa291322860127818b8de0cbe.tar.xz
mullvadvpn-6e15ab43e1620a5fa291322860127818b8de0cbe.zip
Add advanced settings vm and repo
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AdvancedSettingsUiState.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/MtuConstant.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt51
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/IntegerExtension.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModel.kt249
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AdvancedSettingsViewModelState.kt100
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
+ )
+ }
+ }
+}