summaryrefslogtreecommitdiffhomepage
path: root/android/app
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
commitd69c346accf92301e5c6f8db30854f2cd28113e3 (patch)
tree721ae7ea730eebb962ad74c10a69c65251c249d8 /android/app
parent6e15ab43e1620a5fa291322860127818b8de0cbe (diff)
downloadmullvadvpn-d69c346accf92301e5c6f8db30854f2cd28113e3.tar.xz
mullvadvpn-d69c346accf92301e5c6f8db30854f2cd28113e3.zip
Add composified advanced settings view
Diffstat (limited to 'android/app')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomDnsComposeCell.kt99
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DnsCell.kt82
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt98
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt71
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt187
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt175
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt234
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/DnsTextField.kt31
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/MtuTextField.kt30
-rw-r--r--android/app/src/main/res/values/strings.xml7
10 files changed, 1014 insertions, 0 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomDnsComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomDnsComposeCell.kt
new file mode 100644
index 0000000000..d5fa79fe09
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CustomDnsComposeCell.kt
@@ -0,0 +1,99 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.CellSwitch
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+
+@Preview
+@Composable
+private fun PreviewDnsComposeCell() {
+ CustomDnsComposeCell(
+ checkboxDefaultState = true,
+ onToggle = {}
+ )
+}
+
+@Composable
+fun CustomDnsComposeCell(
+ checkboxDefaultState: Boolean,
+ onToggle: (Boolean) -> Unit
+) {
+ val titleModifier = Modifier
+ val bodyViewModifier = Modifier
+ val subtitleModifier = Modifier
+
+ BaseCell(
+ title = { CustomDnsCellTitle(modifier = titleModifier) },
+ bodyView = {
+ CustomDnsCellView(
+ switchTriggered = {
+ onToggle(it)
+ },
+ isToggled = checkboxDefaultState,
+ modifier = bodyViewModifier
+ )
+ },
+ onCellClicked = { onToggle(!checkboxDefaultState) },
+ subtitleModifier = subtitleModifier
+ )
+}
+
+@Composable
+fun CustomDnsCellTitle(
+ modifier: Modifier
+) {
+ val textSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ Text(
+ text = stringResource(R.string.enable_custom_dns),
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.Bold,
+ fontSize = textSize,
+ color = MullvadWhite,
+ modifier = modifier
+ .wrapContentWidth(align = Alignment.End)
+ .wrapContentHeight()
+ )
+}
+
+@Composable
+fun CustomDnsCellView(
+ switchTriggered: (Boolean) -> Unit,
+ isToggled: Boolean,
+ modifier: Modifier
+) {
+ Row(
+ modifier = modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ ) {
+ CellSwitch(
+ isChecked = isToggled,
+ onCheckedChange = null
+ )
+ }
+}
+
+@Composable
+fun CustomDnsCellSubtitle(modifier: Modifier) {
+ val textSize = dimensionResource(id = R.dimen.text_small).value.sp
+ Text(
+ text = stringResource(R.string.custom_dns_footer),
+ fontSize = textSize,
+ color = MullvadWhite60,
+ modifier = modifier
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DnsCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DnsCell.kt
new file mode 100644
index 0000000000..238ecb8d8e
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/DnsCell.kt
@@ -0,0 +1,82 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.theme.MullvadHelmetYellow
+
+@Preview
+@Composable
+private fun PreviewDnsCell() {
+ DnsCell(
+ address = "0.0.0.0",
+ isUnreachableLocalDnsWarningVisible = true,
+ onClick = {}
+ )
+}
+
+@Composable
+fun DnsCell(
+ address: String,
+ isUnreachableLocalDnsWarningVisible: Boolean,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val titleModifier = Modifier
+ val startPadding = 54.dp
+
+ BaseCell(
+ title = {
+ DnsTitle(
+ address = address,
+ modifier = titleModifier
+ )
+ },
+ bodyView = {
+ if (isUnreachableLocalDnsWarningVisible) {
+ Icon(
+ painter = painterResource(id = R.drawable.icon_alert),
+ contentDescription = stringResource(id = R.string.confirm_local_dns),
+ tint = MullvadHelmetYellow
+ )
+ }
+ },
+ onCellClicked = { onClick.invoke() },
+ background = colorResource(id = R.color.blue20),
+ startPadding = startPadding,
+ modifier = modifier
+ )
+}
+
+@Composable
+private fun DnsTitle(
+ address: String,
+ modifier: Modifier = Modifier
+) {
+ val textSize = dimensionResource(id = R.dimen.text_medium).value.sp
+ Text(
+ text = address,
+ color = Color.White,
+ fontSize = textSize,
+ fontStyle = FontStyle.Normal,
+ textAlign = TextAlign.Start,
+ modifier = modifier
+ .wrapContentWidth(align = Alignment.End)
+ .wrapContentHeight()
+ )
+}
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
new file mode 100644
index 0000000000..5be7f04d3b
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/MtuComposeCell.kt
@@ -0,0 +1,98 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth as wrapContentWidth1
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+import net.mullvad.mullvadvpn.constant.MTU_MAX_VALUE
+import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE
+
+@Preview
+@Composable
+fun MtuComposeCellPreview() {
+ MtuComposeCell(
+ mtuValue = "1300",
+ onEditMtu = {}
+ )
+}
+
+@Composable
+fun MtuComposeCell(
+ mtuValue: String,
+ onEditMtu: () -> Unit,
+) {
+ val titleModifier = Modifier
+ val subtitleModifier = Modifier
+
+ BaseCell(
+ title = { MtuTitle(modifier = titleModifier) },
+ bodyView = {
+ MtuBodyView(
+ mtuValue = mtuValue,
+ modifier = titleModifier
+ )
+ },
+ subtitle = { MtuSubtitle(subtitleModifier) },
+ subtitleModifier = subtitleModifier,
+ onCellClicked = {
+ onEditMtu.invoke()
+ }
+ )
+}
+
+@Composable
+private fun MtuTitle(
+ modifier: Modifier
+) {
+ val textSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ Text(
+ text = stringResource(R.string.wireguard_mtu),
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.Bold,
+ fontSize = textSize,
+ color = Color.White,
+ modifier = modifier
+ .wrapContentWidth1(align = Alignment.End)
+ .wrapContentHeight()
+ )
+}
+
+@Composable
+private fun MtuBodyView(
+ mtuValue: String,
+ modifier: Modifier
+) {
+ Row(
+ modifier = modifier
+ .wrapContentWidth1()
+ .wrapContentHeight()
+ ) {
+ Text(
+ text = mtuValue.ifEmpty { stringResource(id = R.string.hint_default) },
+ color = Color.White
+ )
+ }
+}
+
+@Composable
+private fun MtuSubtitle(modifier: Modifier) {
+ val textSize = dimensionResource(id = R.dimen.text_small).value.sp
+ Text(
+ text = stringResource(R.string.wireguard_mtu_footer, MTU_MIN_VALUE, MTU_MAX_VALUE),
+ fontSize = textSize,
+ color = MullvadWhite60,
+ modifier = modifier
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt
new file mode 100644
index 0000000000..b4e05ebbd3
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt
@@ -0,0 +1,71 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+
+@Preview
+@Composable
+private fun PreviewNavigationCell() {
+ NavigationComposeCell(
+ title = "Navigation sample",
+ onClick = {}
+ )
+}
+
+@Composable
+fun NavigationComposeCell(
+ title: String,
+ modifier: Modifier = Modifier,
+ bodyView: @Composable () -> Unit = {
+ DefaultNavigationView(chevronContentDescription = title)
+ },
+ onClick: () -> Unit
+) {
+ BaseCell(
+ onCellClicked = onClick,
+ title = { NavigationTitleView(title = title, modifier = modifier) },
+ bodyView = {
+ bodyView()
+ },
+ subtitle = null,
+ )
+}
+
+@Composable
+private fun NavigationTitleView(
+ title: String,
+ modifier: Modifier = Modifier
+) {
+ val textMediumSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ Text(
+ text = title,
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.Bold,
+ fontSize = textMediumSize,
+ color = Color.White,
+ modifier = modifier
+ .wrapContentWidth(align = Alignment.End)
+ .wrapContentHeight()
+ )
+}
+
+@Composable
+private fun DefaultNavigationView(chevronContentDescription: String) {
+ Image(
+ painter = painterResource(id = R.drawable.icon_chevron),
+ contentDescription = chevronContentDescription
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt
new file mode 100644
index 0000000000..b70aa2fddc
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt
@@ -0,0 +1,187 @@
+package net.mullvad.mullvadvpn.compose.dialog
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.textfield.DnsTextField
+import net.mullvad.mullvadvpn.compose.theme.MullvadBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadRed
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite20
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+import net.mullvad.mullvadvpn.viewmodel.StagedDns
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun DnsDialog(
+ stagedDns: StagedDns,
+ isAllowLanEnabled: Boolean,
+ onIpAddressChanged: (String) -> Unit,
+ onAttemptToSave: () -> Unit,
+ onRemove: () -> Unit,
+ onDismiss: () -> Unit
+) {
+ val buttonSize = dimensionResource(id = R.dimen.button_height)
+ val mediumPadding = dimensionResource(id = R.dimen.medium_padding)
+ val textMediumSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ val textFieldFocusRequester = FocusRequester()
+
+ val textSmallSize = dimensionResource(id = R.dimen.text_small).value.sp
+ val textBigSize = dimensionResource(id = R.dimen.text_big).value.sp
+ val dialogPadding = 20.dp
+ val midPadding = 10.dp
+ val smallPadding = 5.dp
+
+ Dialog(
+ // Fix for https://issuetracker.google.com/issues/221643630
+ properties = DialogProperties(usePlatformDefaultWidth = false),
+ onDismissRequest = {
+ onDismiss()
+ },
+ content = {
+ Column(
+ Modifier
+ // Related to the fix for https://issuetracker.google.com/issues/221643630
+ .fillMaxWidth(0.8f)
+ .background(color = MullvadDarkBlue)
+ .padding(dialogPadding)
+ ) {
+ Text(
+ text = if (stagedDns is StagedDns.NewDns) {
+ stringResource(R.string.add_dns_server_dialog_title)
+ } else {
+ stringResource(R.string.update_dns_server_dialog_title)
+ },
+ color = Color.White,
+ fontSize = textBigSize
+ )
+
+ Box(
+ Modifier
+ .wrapContentSize()
+ .clickable { textFieldFocusRequester.requestFocus() }
+ ) {
+ DnsTextField(
+ value = stagedDns.item.address,
+ isValidValue = stagedDns.isValid(),
+ onValueChanged = { newMtuValue ->
+ onIpAddressChanged(newMtuValue)
+ },
+ onFocusChanges = {},
+ onSubmit = { onAttemptToSave() },
+ isEnabled = true,
+ placeholderText = stringResource(R.string.enter_value_placeholder),
+ modifier = Modifier
+ .padding(top = midPadding)
+ .focusRequester(textFieldFocusRequester)
+ )
+ }
+
+ val errorMessage = when {
+ stagedDns.validationResult is StagedDns.ValidationResult.DuplicateAddress -> {
+ stringResource(R.string.duplicate_address_warning)
+ }
+ stagedDns.item.isLocal && isAllowLanEnabled.not() -> {
+ stringResource(id = R.string.confirm_local_dns)
+ }
+ else -> {
+ null
+ }
+ }
+
+ if (errorMessage != null) {
+ Text(
+ text = errorMessage,
+ fontSize = textSmallSize,
+ color = MullvadRed,
+ modifier = Modifier.padding(top = smallPadding)
+ )
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .defaultMinSize(minHeight = buttonSize)
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = MullvadWhite,
+ disabledContentColor = MullvadWhite60,
+ disabledBackgroundColor = MullvadWhite20
+ ),
+ onClick = { onAttemptToSave() },
+ enabled = stagedDns.isValid()
+ ) {
+ Text(
+ text = stringResource(id = R.string.submit_button),
+ fontSize = textMediumSize
+ )
+ }
+
+ if (stagedDns is StagedDns.EditDns) {
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .defaultMinSize(minHeight = buttonSize)
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = MullvadWhite
+ ),
+ onClick = { onRemove() }
+ ) {
+ Text(
+ text = stringResource(id = R.string.remove_button),
+ fontSize = textMediumSize
+ )
+ }
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .defaultMinSize(minHeight = buttonSize)
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = Color.White
+ ),
+ onClick = {
+ onDismiss()
+ }
+ ) {
+ Text(
+ text = stringResource(id = R.string.cancel),
+ fontSize = textMediumSize
+ )
+ }
+ }
+ }
+ )
+}
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
new file mode 100644
index 0000000000..e4df8af467
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt
@@ -0,0 +1,175 @@
+package net.mullvad.mullvadvpn.compose.dialog
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Dialog
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.textfield.MtuTextField
+import net.mullvad.mullvadvpn.compose.theme.MullvadBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite20
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+import net.mullvad.mullvadvpn.constant.MTU_MAX_VALUE
+import net.mullvad.mullvadvpn.constant.MTU_MIN_VALUE
+import net.mullvad.mullvadvpn.util.isValidMtu
+
+@Composable
+fun MtuDialog(
+ mtuValue: String,
+ onMtuValueChanged: (String) -> Unit,
+ onSave: () -> Unit,
+ onRestoreDefaultValue: () -> Unit,
+ onDismiss: () -> Unit,
+) {
+ val buttonSize = dimensionResource(id = R.dimen.button_height)
+ val mediumPadding = dimensionResource(id = R.dimen.medium_padding)
+ val textMediumSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ val isValidMtu = mtuValue.toIntOrNull()?.isValidMtu() == true
+ val textFieldFocusRequester = FocusRequester()
+
+ val textSmallSize = dimensionResource(id = R.dimen.text_small).value.sp
+ val dialogPadding = 10.dp
+ val smallPadding = 5.dp
+
+ Dialog(
+ onDismissRequest = {
+ onDismiss()
+ },
+ content = {
+ Column(
+ Modifier
+ .background(color = MullvadDarkBlue)
+ .padding(dialogPadding)
+ ) {
+ Text(
+ text = stringResource(id = R.string.wireguard_mtu),
+ color = Color.White,
+ fontSize = textMediumSize
+ )
+
+ Box(
+ Modifier
+ .wrapContentSize()
+ .clickable { textFieldFocusRequester.requestFocus() }
+ .padding(top = dialogPadding)
+ ) {
+ MtuTextField(
+ value = mtuValue,
+ onValueChanged = { newMtuValue ->
+ onMtuValueChanged(newMtuValue)
+ },
+ onFocusChange = {},
+ onSubmit = { newMtuValue ->
+ if (newMtuValue.toIntOrNull()?.isValidMtu() == true) {
+ onSave()
+ }
+ },
+ isEnabled = true,
+ placeholderText = stringResource(R.string.enter_value_placeholder),
+ maxCharLength = 4,
+ isValidValue = isValidMtu,
+ modifier = Modifier
+ .focusRequester(textFieldFocusRequester)
+ )
+ }
+
+ Text(
+ text = stringResource(
+ id = R.string.wireguard_mtu_footer,
+ MTU_MIN_VALUE,
+ MTU_MAX_VALUE
+ ),
+ fontSize = textSmallSize,
+ color = MullvadWhite60,
+ modifier = Modifier.padding(top = smallPadding)
+ )
+
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = MullvadWhite,
+ disabledContentColor = MullvadWhite60,
+ disabledBackgroundColor = MullvadWhite20
+ ),
+ enabled = isValidMtu,
+ onClick = {
+ onSave()
+ }
+ ) {
+ Text(
+ text = stringResource(R.string.submit_button),
+ fontSize = textMediumSize
+ )
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .defaultMinSize(
+ minHeight = buttonSize
+ )
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = MullvadWhite
+ ),
+ onClick = {
+ onRestoreDefaultValue()
+ }
+ ) {
+ Text(
+ text = stringResource(R.string.reset_to_default_button),
+ fontSize = textMediumSize
+ )
+ }
+
+ Button(
+ modifier = Modifier
+ .padding(top = mediumPadding)
+ .height(buttonSize)
+ .defaultMinSize(
+ minHeight = buttonSize
+ )
+ .fillMaxWidth(),
+ colors = ButtonDefaults.buttonColors(
+ backgroundColor = MullvadBlue,
+ contentColor = Color.White
+ ),
+ onClick = {
+ onDismiss()
+ }
+ ) {
+ Text(
+ text = stringResource(R.string.cancel),
+ fontSize = textMediumSize
+ )
+ }
+ }
+ }
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt
new file mode 100644
index 0000000000..6d67718f1a
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AdvancedSettingScreen.kt
@@ -0,0 +1,234 @@
+package net.mullvad.mullvadvpn.compose.screen
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import me.onebone.toolbar.ScrollStrategy
+import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.cell.BaseCell
+import net.mullvad.mullvadvpn.compose.cell.CustomDnsCellSubtitle
+import net.mullvad.mullvadvpn.compose.cell.CustomDnsComposeCell
+import net.mullvad.mullvadvpn.compose.cell.DnsCell
+import net.mullvad.mullvadvpn.compose.cell.MtuComposeCell
+import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell
+import net.mullvad.mullvadvpn.compose.component.CollapsableAwareToolbarScaffold
+import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar
+import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
+import net.mullvad.mullvadvpn.compose.dialog.DnsDialog
+import net.mullvad.mullvadvpn.compose.dialog.MtuDialog
+import net.mullvad.mullvadvpn.compose.state.AdvancedSettingsUiState
+import net.mullvad.mullvadvpn.compose.theme.CollapsingToolbarTheme
+import net.mullvad.mullvadvpn.compose.theme.MullvadBlue20
+import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
+import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
+
+@OptIn(ExperimentalMaterialApi::class)
+@Preview
+@Composable
+private fun PreviewAdvancedSettings() {
+ AdvancedSettingScreen(
+ uiState = AdvancedSettingsUiState.DefaultUiState(
+ mtu = "1337",
+ isCustomDnsEnabled = true,
+ customDnsItems = listOf(
+ CustomDnsItem("0.0.0.0", false)
+ )
+ ),
+ onMtuCellClick = {},
+ onMtuInputChange = {},
+ onSaveMtuClick = {},
+ onRestoreMtuClick = {},
+ onCancelMtuDialogClicked = {},
+ onSplitTunnelingNavigationClick = {},
+ onToggleDnsClick = {},
+ onDnsClick = {},
+ onDnsInputChange = {},
+ onSaveDnsClick = {},
+ onRemoveDnsClick = {},
+ onCancelDnsDialogClick = {},
+ onBackClick = {},
+ )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@ExperimentalMaterialApi
+@Composable
+fun AdvancedSettingScreen(
+ uiState: AdvancedSettingsUiState,
+ onMtuCellClick: () -> Unit,
+ onMtuInputChange: (String) -> Unit,
+ onSaveMtuClick: () -> Unit,
+ onRestoreMtuClick: () -> Unit,
+ onCancelMtuDialogClicked: () -> Unit,
+ onSplitTunnelingNavigationClick: () -> Unit,
+ onToggleDnsClick: (Boolean) -> Unit,
+ onDnsClick: (index: Int?) -> Unit,
+ onDnsInputChange: (String) -> Unit,
+ onSaveDnsClick: () -> Unit,
+ onRemoveDnsClick: () -> Unit,
+ onCancelDnsDialogClick: () -> Unit,
+ onBackClick: () -> Unit
+) {
+ val cellVerticalSpacing = dimensionResource(id = R.dimen.cell_label_vertical_padding)
+ val cellHorizontalSpacing = dimensionResource(id = R.dimen.cell_left_padding)
+
+ when (uiState) {
+ is AdvancedSettingsUiState.MtuDialogUiState -> {
+ MtuDialog(
+ mtuValue = uiState.mtuEditValue,
+ onMtuValueChanged = { onMtuInputChange(it) },
+ onSave = { onSaveMtuClick() },
+ onRestoreDefaultValue = { onRestoreMtuClick() },
+ onDismiss = { onCancelMtuDialogClicked() }
+ )
+ }
+ is AdvancedSettingsUiState.DnsDialogUiState -> {
+ DnsDialog(
+ stagedDns = uiState.stagedDns,
+ isAllowLanEnabled = uiState.isAllowLanEnabled,
+ onIpAddressChanged = { onDnsInputChange(it) },
+ onAttemptToSave = { onSaveDnsClick() },
+ onRemove = { onRemoveDnsClick() },
+ onDismiss = { onCancelDnsDialogClick() },
+ )
+ }
+ else -> {
+ // NOOP
+ }
+ }
+
+ val lazyListState = rememberLazyListState()
+ val biggerPadding = 54.dp
+ val topPadding = 6.dp
+
+ CollapsingToolbarTheme {
+
+ val state = rememberCollapsingToolbarScaffoldState()
+ val progress = state.toolbarState.progress
+
+ CollapsableAwareToolbarScaffold(
+ modifier = Modifier
+ .background(MullvadDarkBlue)
+ .fillMaxSize(),
+ state = state,
+ scrollStrategy = ScrollStrategy.ExitUntilCollapsed,
+ isEnabledWhenCollapsable = true,
+ toolbar = {
+ val scaffoldModifier = Modifier
+ .road(
+ whenCollapsed = Alignment.TopCenter,
+ whenExpanded = Alignment.BottomStart
+ )
+ CollapsingTopBar(
+ backgroundColor = MullvadDarkBlue,
+ onBackClicked = {
+ onBackClick()
+ },
+ title = stringResource(id = R.string.settings_advanced),
+ progress = progress,
+ modifier = scaffoldModifier,
+ backTitle = stringResource(id = R.string.settings),
+ )
+ }
+ ) {
+ LazyColumn(
+ modifier = Modifier
+ .drawVerticalScrollbar(lazyListState)
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .animateContentSize(),
+ state = lazyListState
+
+ ) {
+ item {
+ MtuComposeCell(
+ mtuValue = uiState.mtu,
+ onEditMtu = { onMtuCellClick() }
+ )
+ }
+
+ item {
+ NavigationComposeCell(
+ title = stringResource(id = R.string.split_tunneling),
+ onClick = {
+ onSplitTunnelingNavigationClick.invoke()
+ }
+ )
+ Divider()
+ }
+
+ item {
+ CustomDnsComposeCell(
+ checkboxDefaultState = uiState.isCustomDnsEnabled,
+ onToggle = { newValue ->
+ onToggleDnsClick(newValue)
+ }
+ )
+ Divider()
+ }
+
+ if (uiState.isCustomDnsEnabled) {
+ itemsIndexed(uiState.customDnsItems) { index, item ->
+ DnsCell(
+ address = item.address,
+ isUnreachableLocalDnsWarningVisible = item.isLocal &&
+ uiState.isAllowLanEnabled.not(),
+ onClick = { onDnsClick(index) },
+ modifier = Modifier.animateItemPlacement(),
+ )
+ Divider()
+ }
+
+ item {
+ BaseCell(
+ onCellClicked = { onDnsClick(null) },
+ title = {
+ Text(
+ text = stringResource(id = R.string.add_a_server),
+ color = Color.White
+ )
+ },
+ bodyView = { },
+ subtitle = null,
+ background = MullvadBlue20,
+ startPadding = biggerPadding
+ )
+ Divider()
+ }
+ }
+
+ item {
+ CustomDnsCellSubtitle(
+ Modifier
+ .background(MullvadDarkBlue)
+ .padding(
+ start = cellHorizontalSpacing,
+ top = topPadding,
+ end = cellHorizontalSpacing,
+ bottom = cellVerticalSpacing
+ )
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/DnsTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/DnsTextField.kt
new file mode 100644
index 0000000000..198bb59159
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/DnsTextField.kt
@@ -0,0 +1,31 @@
+package net.mullvad.mullvadvpn.compose.textfield
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+
+@Composable
+fun DnsTextField(
+ value: String,
+ isValidValue: Boolean,
+ modifier: Modifier = Modifier,
+ onValueChanged: (String) -> Unit = { },
+ onFocusChanges: (Boolean) -> Unit = { },
+ onSubmit: (String) -> Unit = { },
+ placeholderText: String = "",
+ isEnabled: Boolean = true
+) {
+ CustomTextField(
+ value = value,
+ modifier = modifier,
+ onValueChanged = onValueChanged,
+ onFocusChange = onFocusChanges,
+ onSubmit = onSubmit,
+ isEnabled = isEnabled,
+ placeholderText = placeholderText,
+ maxCharLength = Int.MAX_VALUE,
+ isValidValue = isValidValue,
+ isDigitsOnlyAllowed = false,
+ textAlign = TextAlign.Start
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/MtuTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/MtuTextField.kt
new file mode 100644
index 0000000000..c44c16911c
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/MtuTextField.kt
@@ -0,0 +1,30 @@
+package net.mullvad.mullvadvpn.compose.textfield
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+
+@Composable
+fun MtuTextField(
+ value: String,
+ isValidValue: Boolean,
+ modifier: Modifier = Modifier,
+ onValueChanged: (String) -> Unit = { },
+ onFocusChange: (Boolean) -> Unit = { },
+ onSubmit: (String) -> Unit = { },
+ isEnabled: Boolean = true,
+ placeholderText: String = "",
+ maxCharLength: Int
+) {
+ CustomTextField(
+ value = value,
+ modifier = modifier,
+ onValueChanged = onValueChanged,
+ onFocusChange = onFocusChange,
+ onSubmit = onSubmit,
+ isEnabled = isEnabled,
+ placeholderText = placeholderText,
+ maxCharLength = maxCharLength,
+ isValidValue = isValidValue,
+ isDigitsOnlyAllowed = true
+ )
+}
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 2e4c5f0275..f655e99901 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -203,4 +203,11 @@
then the app queries your system for a list of all installed applications. This list is only
retrieved in the split tunneling view. The list of installed applications is never sent from
the device.</string>
+ <string name="submit_button">Submit</string>
+ <string name="remove_button">Remove</string>
+ <string name="enter_value_placeholder">Enter…</string>
+ <string name="reset_to_default_button">Reset to default</string>
+ <string name="add_dns_server_dialog_title">Add DNS server</string>
+ <string name="update_dns_server_dialog_title">Update DNS server</string>
+ <string name="duplicate_address_warning">This address has already been entered.</string>
</resources>