summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--android/app/build.gradle.kts3
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/BaseCell.kt87
-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/component/CollapsingTopBar.kt126
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt60
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt336
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Switch.kt99
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt)3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt)4
-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/screen/DeviceListScreen.kt2
-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/compose/textfield/CustomTextField.kt172
-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/kotlin/net/mullvad/mullvadvpn/compose/theme/Color.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt33
-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/ui/AdvancedFragment.kt283
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt302
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt89
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmDnsDialogFragment.kt66
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/MtuCell.kt85
-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
-rw-r--r--android/app/src/main/res/drawable/cell_input_background.xml5
-rw-r--r--android/app/src/main/res/drawable/cell_input_cursor.xml6
-rw-r--r--android/app/src/main/res/drawable/icon_add.xml7
-rw-r--r--android/app/src/main/res/drawable/icon_check.xml11
-rw-r--r--android/app/src/main/res/layout/add_custom_dns_server.xml31
-rw-r--r--android/app/src/main/res/layout/advanced.xml34
-rw-r--r--android/app/src/main/res/layout/advanced_header.xml32
-rw-r--r--android/app/src/main/res/layout/confirm_dns.xml31
-rw-r--r--android/app/src/main/res/layout/custom_dns_footer.xml14
-rw-r--r--android/app/src/main/res/layout/custom_dns_server.xml31
-rw-r--r--android/app/src/main/res/layout/edit_custom_dns_server.xml30
-rw-r--r--android/app/src/main/res/layout/mtu_edit_text.xml14
-rw-r--r--android/app/src/main/res/values-da/strings.xml2
-rw-r--r--android/app/src/main/res/values-de/strings.xml2
-rw-r--r--android/app/src/main/res/values-es/strings.xml2
-rw-r--r--android/app/src/main/res/values-fi/strings.xml2
-rw-r--r--android/app/src/main/res/values-fr/strings.xml2
-rw-r--r--android/app/src/main/res/values-it/strings.xml2
-rw-r--r--android/app/src/main/res/values-ja/strings.xml2
-rw-r--r--android/app/src/main/res/values-ko/strings.xml2
-rw-r--r--android/app/src/main/res/values-my/strings.xml2
-rw-r--r--android/app/src/main/res/values-nb/strings.xml2
-rw-r--r--android/app/src/main/res/values-nl/strings.xml2
-rw-r--r--android/app/src/main/res/values-pl/strings.xml2
-rw-r--r--android/app/src/main/res/values-pt/strings.xml2
-rw-r--r--android/app/src/main/res/values-ru/strings.xml2
-rw-r--r--android/app/src/main/res/values-sv/strings.xml2
-rw-r--r--android/app/src/main/res/values-th/strings.xml2
-rw-r--r--android/app/src/main/res/values-tr/strings.xml2
-rw-r--r--android/app/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--android/app/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--android/app/src/main/res/values/colors.xml1
-rw-r--r--android/app/src/main/res/values/dimensions.xml2
-rw-r--r--android/app/src/main/res/values/strings.xml9
-rw-r--r--android/buildSrc/src/main/kotlin/Dependencies.kt4
-rw-r--r--android/buildSrc/src/main/kotlin/Versions.kt2
-rw-r--r--android/gradle/verification-metadata.xml73
-rw-r--r--gui/locales/messages.pot21
78 files changed, 2531 insertions, 1158 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 02cb680b08..46b70c0607 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -206,12 +206,15 @@ dependencies {
implementation(Dependencies.AndroidX.lifecycleRuntimeKtx)
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
implementation(Dependencies.AndroidX.recyclerview)
+ implementation(Dependencies.Compose.composeCollapsingToolbar)
implementation(Dependencies.Compose.constrainLayout)
implementation(Dependencies.Compose.foundation)
implementation(Dependencies.Compose.viewModelLifecycle)
implementation(Dependencies.Compose.material)
+ implementation(Dependencies.Compose.material3)
implementation(Dependencies.Compose.uiController)
implementation(Dependencies.Compose.ui)
+ implementation(Dependencies.Compose.uiUtil)
implementation(Dependencies.jodaTime)
implementation(Dependencies.Koin.core)
implementation(Dependencies.Koin.coreExt)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt
index dab5bf0a60..4c94fced01 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ChangelogDialogTest.kt
@@ -11,7 +11,7 @@ import io.mockk.just
import io.mockk.verify
import kotlinx.coroutines.flow.MutableStateFlow
import net.mullvad.mullvadvpn.compose.component.AppTheme
-import net.mullvad.mullvadvpn.compose.component.ChangelogDialog
+import net.mullvad.mullvadvpn.compose.dialog.ChangelogDialog
import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState
import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import org.junit.Before
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/BaseCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/BaseCell.kt
new file mode 100644
index 0000000000..cd63483d45
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/BaseCell.kt
@@ -0,0 +1,87 @@
+package net.mullvad.mullvadvpn.compose.cell
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+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.unit.Dp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.theme.MullvadBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
+
+@Composable
+fun BaseCell(
+ title: @Composable () -> Unit,
+ bodyView: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ onCellClicked: () -> Unit = {},
+ subtitle: @Composable (() -> Unit)? = null,
+ subtitleModifier: Modifier = Modifier,
+ background: Color = MullvadBlue,
+ startPadding: Dp = dimensionResource(id = R.dimen.cell_left_padding),
+ endPadding: Dp = dimensionResource(id = R.dimen.cell_right_padding)
+) {
+ val cellHeight = dimensionResource(id = R.dimen.cell_height)
+ val cellVerticalSpacing = dimensionResource(id = R.dimen.cell_label_vertical_padding)
+ val subtitleVerticalSpacing = dimensionResource(id = R.dimen.cell_footer_top_padding)
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentHeight()
+ .background(background)
+ ) {
+
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
+ modifier = Modifier
+ .height(cellHeight)
+ .fillMaxWidth()
+ .clickable { onCellClicked.invoke() }
+ .padding(start = startPadding, end = endPadding)
+
+ ) {
+ title()
+
+ Spacer(modifier = Modifier.weight(1.0f))
+
+ Column(
+ modifier = modifier
+ .wrapContentWidth()
+ .wrapContentHeight()
+ ) {
+ bodyView()
+ }
+ }
+
+ if (subtitle != null) {
+ Row(
+ modifier = subtitleModifier
+ .background(MullvadDarkBlue)
+ .padding(
+ start = startPadding,
+ top = subtitleVerticalSpacing,
+ end = endPadding,
+ bottom = cellVerticalSpacing
+ )
+ .fillMaxWidth()
+ .wrapContentHeight()
+ ) {
+ subtitle()
+ }
+ }
+ }
+}
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/component/CollapsingTopBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CollapsingTopBar.kt
new file mode 100644
index 0000000000..b117f7cbf3
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/CollapsingTopBar.kt
@@ -0,0 +1,126 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.layout.wrapContentWidth
+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.graphics.Color
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+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.dp
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.theme.MullvadDarkBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite60
+
+@Preview
+@Composable
+private fun PreviewTopBar() {
+ CollapsingTopBar(
+ backgroundColor = MullvadDarkBlue,
+ onBackClicked = {},
+ title = "View title",
+ progress = 1.0f,
+ backTitle = "Back",
+ modifier = Modifier.height(102.dp)
+ )
+}
+
+@Composable
+fun CollapsingTopBar(
+ backgroundColor: Color,
+ onBackClicked: () -> Unit,
+ title: String,
+ progress: Float,
+ backTitle: String,
+ modifier: Modifier
+) {
+ val expandedToolbarHeight = dimensionResource(id = R.dimen.expanded_toolbar_height)
+ val iconSize = dimensionResource(id = R.dimen.icon_size)
+ val iconPadding = dimensionResource(id = R.dimen.small_padding)
+ val sideMargin = dimensionResource(id = R.dimen.side_margin)
+ val verticalMargin = dimensionResource(id = R.dimen.cell_label_vertical_padding)
+ val textSize = dimensionResource(id = R.dimen.text_small).value.sp
+ val maxTopPadding = 48
+ val minTopPadding = 14
+ val maxTitleSize = 30
+ val minTitleSize = 20
+
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(expandedToolbarHeight)
+ .background(backgroundColor)
+ )
+
+ Button(
+ modifier = Modifier
+ .wrapContentWidth()
+ .wrapContentHeight(),
+ onClick = onBackClicked,
+ colors = ButtonDefaults.buttonColors(
+ contentColor = Color.White,
+ backgroundColor = MullvadDarkBlue
+ )
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.icon_back),
+ contentDescription = stringResource(id = R.string.back),
+ modifier = Modifier
+ .width(iconSize)
+ .height(iconSize)
+ )
+ Spacer(
+ modifier = Modifier
+ .width(iconPadding)
+ .fillMaxHeight()
+ )
+ Text(
+ text = backTitle,
+ color = MullvadWhite60,
+ fontWeight = FontWeight.Bold,
+ fontSize = textSize
+ )
+ }
+
+ Text(
+ text = title,
+ style = TextStyle(
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.End
+ ),
+ modifier = modifier
+ .padding(
+ start = sideMargin,
+ end = sideMargin,
+ top = (minTopPadding + (maxTopPadding - minTopPadding) * progress).dp,
+ bottom = verticalMargin
+ ),
+ fontSize = topBarSize(
+ progress = progress,
+ minTitleSize = minTitleSize,
+ maxTitleSize = maxTitleSize
+ ).sp
+ )
+}
+
+private fun topBarSize(progress: Float, minTitleSize: Int, maxTitleSize: Int): Float {
+ return (minTitleSize + ((maxTitleSize - minTitleSize) * progress))
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
index 05cd60cba1..c6d32a4df9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt
@@ -1,10 +1,25 @@
package net.mullvad.mullvadvpn.compose.component
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import me.onebone.toolbar.CollapsingToolbarScaffold
+import me.onebone.toolbar.CollapsingToolbarScaffoldScope
+import me.onebone.toolbar.CollapsingToolbarScaffoldState
+import me.onebone.toolbar.CollapsingToolbarScope
+import me.onebone.toolbar.ExperimentalToolbarApi
+import me.onebone.toolbar.ScrollStrategy
@Composable
fun ScaffoldWithTopBar(
@@ -30,3 +45,48 @@ fun ScaffoldWithTopBar(
content = content
)
}
+
+@Composable
+@OptIn(ExperimentalToolbarApi::class)
+fun CollapsableAwareToolbarScaffold(
+ modifier: Modifier,
+ state: CollapsingToolbarScaffoldState,
+ scrollStrategy: ScrollStrategy,
+ isEnabledWhenCollapsable: Boolean = true,
+ toolbarModifier: Modifier = Modifier,
+ toolbar: @Composable CollapsingToolbarScope.() -> Unit,
+ body: @Composable CollapsingToolbarScaffoldScope.() -> Unit
+) {
+ var isCollapsable by remember { mutableStateOf(false) }
+
+ LaunchedEffect(isCollapsable) {
+ if (!isCollapsable) {
+ state.toolbarState.expand()
+ }
+ }
+
+ CollapsingToolbarScaffold(
+ modifier = modifier,
+ state = state,
+ scrollStrategy = scrollStrategy,
+ enabled = isEnabledWhenCollapsable && isCollapsable,
+ toolbarModifier = toolbarModifier,
+ toolbar = toolbar,
+ body = {
+ var bodyHeight by remember { mutableStateOf(0) }
+
+ BoxWithConstraints(
+ modifier = Modifier.onGloballyPositioned { bodyHeight = it.size.height }
+ ) {
+ val minMaxToolbarHeightDiff = with(state) {
+ toolbarState.maxHeight - toolbarState.minHeight
+ }
+ val isContentHigherThanCollapseThreshold = with(LocalDensity.current) {
+ bodyHeight > maxHeight.toPx() - minMaxToolbarHeightDiff
+ }
+ isCollapsable = isContentHigherThanCollapseThreshold
+ body()
+ }
+ }
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt
new file mode 100644
index 0000000000..1ca651a9a9
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt
@@ -0,0 +1,336 @@
+package net.mullvad.mullvadvpn.compose.component
+
+/*
+ * Code snippet taken from https://gist.github.com/mxalbert1996/33a360fcab2105a31e5355af98216f5a
+ *
+ * MIT License
+ *
+ * Copyright (c) 2022 Albert Chang
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import android.view.ViewConfiguration
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastSumBy
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collectLatest
+
+fun Modifier.drawHorizontalScrollbar(
+ state: ScrollState,
+ reverseScrolling: Boolean = false
+): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling)
+
+fun Modifier.drawVerticalScrollbar(
+ state: ScrollState,
+ reverseScrolling: Boolean = false
+): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling)
+
+private fun Modifier.drawScrollbar(
+ state: ScrollState,
+ orientation: Orientation,
+ reverseScrolling: Boolean
+): Modifier = drawScrollbar(
+ orientation, reverseScrolling
+) { reverseDirection, atEnd, color, alpha ->
+ if (state.maxValue > 0) {
+ val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height
+ val totalSize = canvasSize + state.maxValue
+ val thumbSize = canvasSize / totalSize * canvasSize
+ val startOffset = state.value / totalSize * canvasSize
+ drawScrollbar(
+ orientation, reverseDirection, atEnd, color, alpha, thumbSize, startOffset
+ )
+ }
+}
+
+fun Modifier.drawHorizontalScrollbar(
+ state: LazyListState,
+ reverseScrolling: Boolean = false
+): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling)
+
+fun Modifier.drawVerticalScrollbar(
+ state: LazyListState,
+ reverseScrolling: Boolean = false
+): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling)
+
+private fun Modifier.drawScrollbar(
+ state: LazyListState,
+ orientation: Orientation,
+ reverseScrolling: Boolean
+): Modifier = drawScrollbar(
+ orientation, reverseScrolling
+) { reverseDirection, atEnd, color, alpha ->
+ val layoutInfo = state.layoutInfo
+ val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
+ val items = layoutInfo.visibleItemsInfo
+ val itemsSize = items.fastSumBy { it.size }
+ if (items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize) {
+ val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size
+ val totalSize = estimatedItemSize * layoutInfo.totalItemsCount
+ val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height
+ val thumbSize = viewportSize / totalSize * canvasSize
+ val startOffset = if (items.isEmpty()) 0f else items.first().run {
+ (estimatedItemSize * index - offset) / totalSize * canvasSize
+ }
+ drawScrollbar(
+ orientation, reverseDirection, atEnd, color, alpha, thumbSize, startOffset
+ )
+ }
+}
+
+fun Modifier.drawVerticalScrollbar(
+ state: LazyGridState,
+ spanCount: Int,
+ reverseScrolling: Boolean = false
+): Modifier = drawScrollbar(
+ Orientation.Vertical, reverseScrolling
+) { reverseDirection, atEnd, color, alpha ->
+ val layoutInfo = state.layoutInfo
+ val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
+ val items = layoutInfo.visibleItemsInfo
+ val rowCount = (items.size + spanCount - 1) / spanCount
+ var itemsSize = 0
+ for (i in 0 until rowCount) {
+ itemsSize += items[i * spanCount].size.height
+ }
+ if (items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize) {
+ val estimatedItemSize = if (rowCount == 0) 0f else itemsSize.toFloat() / rowCount
+ val totalRow = (layoutInfo.totalItemsCount + spanCount - 1) / spanCount
+ val totalSize = estimatedItemSize * totalRow
+ val canvasSize = size.height
+ val thumbSize = viewportSize / totalSize * canvasSize
+ val startOffset = if (rowCount == 0) 0f else items.first().run {
+ val rowIndex = index / spanCount
+ (estimatedItemSize * rowIndex - offset.y) / totalSize * canvasSize
+ }
+ drawScrollbar(
+ Orientation.Vertical, reverseDirection, atEnd, color, alpha, thumbSize, startOffset
+ )
+ }
+}
+
+private fun DrawScope.drawScrollbar(
+ orientation: Orientation,
+ reverseDirection: Boolean,
+ atEnd: Boolean,
+ color: Color,
+ alpha: () -> Float,
+ thumbSize: Float,
+ startOffset: Float
+) {
+ val thicknessPx = Thickness.toPx()
+ val topLeft = if (orientation == Orientation.Horizontal) {
+ Offset(
+ if (reverseDirection) size.width - startOffset - thumbSize else startOffset,
+ if (atEnd) size.height - thicknessPx else 0f
+ )
+ } else {
+ Offset(
+ if (atEnd) size.width - thicknessPx else 0f,
+ if (reverseDirection) size.height - startOffset - thumbSize else startOffset
+ )
+ }
+ val size = if (orientation == Orientation.Horizontal) {
+ Size(thumbSize, thicknessPx)
+ } else {
+ Size(thicknessPx, thumbSize)
+ }
+
+ drawRect(
+ color = color,
+ topLeft = topLeft,
+ size = size,
+ alpha = alpha()
+ )
+}
+
+private fun Modifier.drawScrollbar(
+ orientation: Orientation,
+ reverseScrolling: Boolean,
+ onDraw: DrawScope.(
+ reverseDirection: Boolean,
+ atEnd: Boolean,
+ color: Color,
+ alpha: () -> Float
+ ) -> Unit
+): Modifier = composed {
+ val scrolled = remember {
+ MutableSharedFlow<Unit>(
+ extraBufferCapacity = 1,
+ onBufferOverflow = BufferOverflow.DROP_OLDEST
+ )
+ }
+ val nestedScrollConnection = remember(orientation, scrolled) {
+ object : NestedScrollConnection {
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val delta = if (orientation == Orientation.Horizontal) consumed.x else consumed.y
+ if (delta != 0f) scrolled.tryEmit(Unit)
+ return Offset.Zero
+ }
+ }
+ }
+
+ val alpha = remember { Animatable(0f) }
+ LaunchedEffect(scrolled, alpha) {
+ scrolled.collectLatest {
+ alpha.snapTo(1f)
+ delay(ViewConfiguration.getScrollDefaultDelay().toLong())
+ alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
+ }
+ }
+
+ val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
+ val reverseDirection = if (orientation == Orientation.Horizontal) {
+ if (isLtr) reverseScrolling else !reverseScrolling
+ } else reverseScrolling
+ val atEnd = if (orientation == Orientation.Vertical) isLtr else true
+
+ val color = BarColor
+
+ Modifier
+ .nestedScroll(nestedScrollConnection)
+ .drawWithContent {
+ drawContent()
+ onDraw(reverseDirection, atEnd, color, alpha::value)
+ }
+}
+
+private val BarColor: Color
+ @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
+
+private val Thickness = 4.dp
+private val FadeOutAnimationSpec =
+ tween<Float>(durationMillis = ViewConfiguration.getScrollBarFadeDuration())
+
+@Preview(widthDp = 400, heightDp = 400, showBackground = true)
+@Composable
+internal fun ScrollbarPreview() {
+ val state = rememberScrollState()
+ Column(
+ modifier = Modifier
+ .drawVerticalScrollbar(state)
+ .verticalScroll(state),
+ ) {
+ repeat(50) {
+ Text(
+ text = "Item ${it + 1}",
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 400, heightDp = 400, showBackground = true)
+@Composable
+internal fun LazyListScrollbarPreview() {
+ val state = rememberLazyListState()
+ LazyColumn(
+ modifier = Modifier.drawVerticalScrollbar(state),
+ state = state
+ ) {
+ items(50) {
+ Text(
+ text = "Item ${it + 1}",
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 400, showBackground = true)
+@Composable
+internal fun HorizontalScrollbarPreview() {
+ val state = rememberScrollState()
+ Row(
+ modifier = Modifier
+ .drawHorizontalScrollbar(state)
+ .horizontalScroll(state)
+ ) {
+ repeat(50) {
+ Text(
+ text = (it + 1).toString(),
+ modifier = Modifier
+ .padding(horizontal = 8.dp, vertical = 16.dp)
+ )
+ }
+ }
+}
+
+@Preview(widthDp = 400, showBackground = true)
+@Composable
+internal fun LazyListHorizontalScrollbarPreview() {
+ val state = rememberLazyListState()
+ LazyRow(
+ modifier = Modifier.drawHorizontalScrollbar(state),
+ state = state
+ ) {
+ items(50) {
+ Text(
+ text = (it + 1).toString(),
+ modifier = Modifier
+ .padding(horizontal = 8.dp, vertical = 16.dp)
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Switch.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Switch.kt
new file mode 100644
index 0000000000..b30ca26f22
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Switch.kt
@@ -0,0 +1,99 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import net.mullvad.mullvadvpn.compose.theme.MullvadGreen
+import net.mullvad.mullvadvpn.compose.theme.MullvadRed
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite
+
+@Preview
+@Composable
+private fun PreviewSwitch() {
+ CellSwitch(
+ isChecked = false,
+ onCheckedChange = null
+ )
+}
+
+@Composable
+fun CellSwitch(
+ isChecked: Boolean,
+ onCheckedChange: ((Boolean) -> Unit)?,
+ modifier: Modifier = Modifier,
+ scale: Float = 1f,
+ thumbCheckedTrackColor: Color = MullvadGreen,
+ thumbUncheckedTrackColor: Color = MullvadRed,
+ thumbColor: Color = MullvadWhite
+) {
+ val gapBetweenThumbAndTrackEdge: Dp = 2.dp
+ val width: Dp = 46.dp
+ val height: Dp = 28.dp
+ val thumbRadius = 11.dp
+
+ // To move the thumb, we need to calculate the position (along x axis)
+ val animatePosition = animateFloatAsState(
+ targetValue = if (isChecked)
+ with(LocalDensity.current) {
+ (width - thumbRadius - gapBetweenThumbAndTrackEdge - 1.dp).toPx()
+ }
+ else
+ with(LocalDensity.current) { (thumbRadius + gapBetweenThumbAndTrackEdge + 1.dp).toPx() }
+ )
+
+ Canvas(
+ modifier = modifier
+ .padding(1.dp)
+ .size(width = width, height = height)
+ .scale(scale = scale)
+ .pointerInput(Unit) {
+ if (onCheckedChange != null) {
+ detectTapGestures(
+ onTap = {
+ onCheckedChange(!isChecked)
+ }
+ )
+ }
+ }
+ ) {
+ // Track
+ drawRoundRect(
+ color = thumbColor,
+ cornerRadius = CornerRadius(x = 15.dp.toPx(), y = 15.dp.toPx()),
+ style = Stroke(
+ width = 2.dp.toPx(),
+ miter = 6.dp.toPx(),
+ cap = StrokeCap.Square,
+ ),
+ )
+
+ // Thumb
+ drawCircle(
+ color = if (isChecked) thumbCheckedTrackColor else thumbUncheckedTrackColor,
+ radius = thumbRadius.toPx(),
+ center = Offset(
+ x = animatePosition.value,
+ y = size.height / 2
+ )
+ )
+ }
+
+ Spacer(modifier = Modifier.height(18.dp))
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt
index 55d015a7db..a0a9d4dd90 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt
@@ -23,7 +23,7 @@ import net.mullvad.mullvadvpn.R
@Preview
@Composable
-fun PreviewTopBar() {
+private fun PreviewTopBar() {
TopBar(
backgroundColor = colorResource(R.color.blue),
onSettingsClicked = {}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt
index 9f2dabae5f..419f0fc7f7 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ChangelogDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.component
+package net.mullvad.mullvadvpn.compose.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
@@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.ChangeListItem
@Composable
fun ChangelogDialog(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt
index edfcebac67..228487e3cc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Dialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.component
+package net.mullvad.mullvadvpn.compose.dialog
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
@@ -25,6 +25,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.HtmlText
+import net.mullvad.mullvadvpn.compose.component.textResource
import net.mullvad.mullvadvpn.model.Device
import net.mullvad.mullvadvpn.util.capitalizeFirstCharOfEachWord
import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
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/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
index 99218a8837..d0fb48348e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt
@@ -30,7 +30,7 @@ import androidx.constraintlayout.compose.Dimension
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.ActionButton
import net.mullvad.mullvadvpn.compose.component.ListItem
-import net.mullvad.mullvadvpn.compose.component.ShowDeviceRemovalDialog
+import net.mullvad.mullvadvpn.compose.dialog.ShowDeviceRemovalDialog
import net.mullvad.mullvadvpn.util.capitalizeFirstCharOfEachWord
import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel
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/compose/textfield/CustomTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt
new file mode 100644
index 0000000000..3a263a6886
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/CustomTextField.kt
@@ -0,0 +1,172 @@
+package net.mullvad.mullvadvpn.compose.textfield
+
+import android.text.TextUtils
+import android.view.KeyEvent
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.theme.MullvadBlue
+import net.mullvad.mullvadvpn.compose.theme.MullvadWhite10
+
+private const val EMPTY_STRING = ""
+private const val NEWLINE_STRING = "\n"
+
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+fun CustomTextField(
+ value: String,
+ modifier: Modifier = Modifier,
+ onValueChanged: (String) -> Unit,
+ onFocusChange: (Boolean) -> Unit,
+ onSubmit: (String) -> Unit,
+ isEnabled: Boolean = true,
+ placeholderText: String = "",
+ placeHolderColor: Color = MullvadBlue,
+ maxCharLength: Int = Int.MAX_VALUE,
+ isValidValue: Boolean,
+ isDigitsOnlyAllowed: Boolean,
+ defaultTextColor: Color = Color.White,
+ textAlign: TextAlign = TextAlign.Start
+) {
+ val fontSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp
+ val shape = RoundedCornerShape(4.dp)
+ val textFieldHeight = 44.dp
+
+ val focusManager = LocalFocusManager.current
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ var isFocused by remember { mutableStateOf(false) }
+
+ val textColor = when {
+ isValidValue.not() -> Color.Red
+ isFocused -> MullvadBlue
+ else -> defaultTextColor
+ }
+
+ val placeholderTextColor = if (isFocused) {
+ placeHolderColor
+ } else {
+ Color.White
+ }
+
+ val backgroundColor = if (isFocused) {
+ Color.White
+ } else {
+ MullvadWhite10
+ }
+
+ fun triggerSubmit() {
+ keyboardController?.hide()
+ focusManager.moveFocus(FocusDirection.Previous)
+ onSubmit(value)
+ }
+
+ BasicTextField(
+ value = value,
+ onValueChange = { input ->
+ val isValidInput = if (isDigitsOnlyAllowed) TextUtils.isDigitsOnly(input) else true
+ if (input.length <= maxCharLength && isValidInput) {
+ // Remove any newline chars added by enter key clicks
+ onValueChanged(input.replace(NEWLINE_STRING, EMPTY_STRING))
+ }
+ },
+ textStyle = TextStyle(
+ color = textColor,
+ fontSize = fontSize,
+ textAlign = textAlign
+ ),
+ enabled = isEnabled,
+ singleLine = true,
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(
+ keyboardType = KeyboardType.Number,
+ imeAction = ImeAction.Done,
+ autoCorrect = false,
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = { triggerSubmit() }
+ ),
+ decorationBox = { decorationBox ->
+ Box(
+ modifier = Modifier
+ .padding(PaddingValues(12.dp, 10.dp))
+ .fillMaxWidth()
+ ) {
+ if (value.isBlank()) {
+ Text(
+ text = placeholderText,
+ color = placeholderTextColor,
+ fontSize = fontSize,
+ textAlign = textAlign,
+ modifier = Modifier.fillMaxWidth()
+ )
+ }
+ decorationBox()
+ }
+ },
+ cursorBrush = SolidColor(MullvadBlue),
+ modifier = modifier
+ .background(backgroundColor)
+ .clip(shape)
+ .onFocusChanged { focusState ->
+ isFocused = focusState.isFocused
+ onFocusChange(focusState.isFocused)
+ }
+ .height(textFieldHeight)
+ .onKeyEvent { keyEvent ->
+ return@onKeyEvent when (keyEvent.nativeKeyEvent.keyCode) {
+ KeyEvent.KEYCODE_ENTER -> {
+ triggerSubmit()
+ true
+ }
+ KeyEvent.KEYCODE_ESCAPE -> {
+ focusManager.clearFocus(force = true)
+ keyboardController?.hide()
+ true
+ }
+ KeyEvent.KEYCODE_DPAD_DOWN -> {
+ focusManager.moveFocus(FocusDirection.Down)
+ true
+ }
+ KeyEvent.KEYCODE_DPAD_UP -> {
+ focusManager.moveFocus(FocusDirection.Up)
+ true
+ }
+ else -> {
+ false
+ }
+ }
+ }
+ )
+}
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/kotlin/net/mullvad/mullvadvpn/compose/theme/Color.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Color.kt
new file mode 100644
index 0000000000..2c542951ab
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Color.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.compose.theme
+
+import androidx.compose.ui.graphics.Color
+
+val MullvadBeige = Color(0xFFFFCD86)
+val MullvadBlue = Color(0xFF294D73)
+val MullvadBlue60 = Color(0x99294D73)
+val MullvadBlue20 = Color(0x33294D73)
+val MullvadBrown = Color(0xFFD2943B)
+val MullvadDarkBlue = Color(0xFF192E45)
+val MullvadGreen = Color(0xFF44AD4D)
+val MullvadRed = Color(0xFFE34039)
+val MullvadYellow = Color(0xFFFFD524)
+val MullvadHelmetYellow = Color(0xFFFFD524)
+val MullvadWhite = Color(0xFFFFFFFF)
+val MullvadWhite10 = Color(0x1AFFFFFF)
+val MullvadWhite20 = Color(0x33FFFFFF)
+val MullvadWhite40 = Color(0x66FFFFFF)
+val MullvadWhite60 = Color(0x99FFFFFF)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt
new file mode 100644
index 0000000000..95e6d312a0
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/theme/Theme.kt
@@ -0,0 +1,33 @@
+package net.mullvad.mullvadvpn.compose.theme
+
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Shapes
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.dp
+
+private val MullvadColorPalette = lightColors(
+ primary = MullvadBlue,
+ primaryVariant = MullvadDarkBlue,
+ secondary = MullvadRed
+)
+
+val Shapes = Shapes(
+ small = RoundedCornerShape(4.dp),
+ medium = RoundedCornerShape(4.dp),
+ large = RoundedCornerShape(0.dp)
+)
+
+@Composable
+fun CollapsingToolbarTheme(
+ content: @Composable () -> Unit
+) {
+ val colors = MullvadColorPalette
+
+ MaterialTheme(
+ colors = colors,
+ shapes = Shapes,
+ content = content
+ )
+}
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/ui/AdvancedFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt
index 10a4f2b5d7..8abb712ff8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/AdvancedFragment.kt
@@ -4,268 +4,59 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
-import androidx.recyclerview.widget.LinearLayoutManager
-import java.net.InetAddress
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
-import kotlinx.coroutines.launch
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.platform.ComposeView
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.model.Settings
-import net.mullvad.mullvadvpn.ui.customdns.CustomDnsAdapter
-import net.mullvad.mullvadvpn.ui.extension.requireMainActivity
+import net.mullvad.mullvadvpn.compose.screen.AdvancedSettingScreen
import net.mullvad.mullvadvpn.ui.fragment.BaseFragment
-import net.mullvad.mullvadvpn.ui.fragment.ConfirmDnsDialogFragment
import net.mullvad.mullvadvpn.ui.fragment.SplitTunnelingFragment
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
-import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
-import net.mullvad.mullvadvpn.ui.serviceconnection.customDns
-import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener
-import net.mullvad.mullvadvpn.ui.widget.CellSwitch
-import net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView
-import net.mullvad.mullvadvpn.ui.widget.MtuCell
-import net.mullvad.mullvadvpn.ui.widget.NavigateCell
-import net.mullvad.mullvadvpn.ui.widget.ToggleCell
-import net.mullvad.mullvadvpn.util.AdapterWithHeader
-import net.mullvad.mullvadvpn.util.JobTracker
-import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
-import org.koin.android.ext.android.inject
+import net.mullvad.mullvadvpn.viewmodel.AdvancedSettingsViewModel
+import org.koin.androidx.viewmodel.ext.android.viewModel
-// TODO: Move as part of refactoring to compose.
class AdvancedFragment : BaseFragment() {
+ private val vm by viewModel<AdvancedSettingsViewModel>()
- // Injected dependencies
- private val serviceConnectionManager: ServiceConnectionManager by inject()
-
- private var isAllowLanEnabled = false
-
- // Both customDnsAdapter and customDnsToggle are nullable since onNewServiceConnection,
- // which sets up custom dns subscriptions, is called before onSafelyCreateView.
- private var customDnsAdapter: CustomDnsAdapter? = null
- private var customDnsToggle: ToggleCell? = null
-
- private lateinit var wireguardMtuInput: MtuCell
- private lateinit var titleController: CollapsibleTitleController
-
- @Deprecated("Refactor code to instead rely on Lifecycle.")
- private val jobTracker = JobTracker()
-
- val shared = serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .map {
- it.customDns
- }
- .shareIn(lifecycleScope, SharingStarted.WhileSubscribed())
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.RESUMED) {
- launch {
- serviceConnectionManager.connectionState
- .flatMapLatest { state ->
- if (state is ServiceConnectionState.ConnectedReady) {
- flowOf(state.container)
- } else {
- emptyFlow()
- }
- }
- .flatMapLatest {
- callbackFlowFromNotifier(it.settingsListener.settingsNotifier)
- }
- .collect { settings ->
- if (settings != null) {
- updateUi(settings)
- }
- }
- }
-
- launch {
- shared
- .flatMapLatest {
- callbackFlowFromNotifier(it.onEnabledChanged)
- }
- .collect { isEnabled ->
- customDnsAdapter?.updateState(isEnabled)
- jobTracker.newUiJob("updateEnabled") {
- if (isEnabled) {
- customDnsToggle?.state = CellSwitch.State.ON
- } else {
- customDnsToggle?.state = CellSwitch.State.OFF
- }
- }
- }
- }
-
- launch {
- shared
- .flatMapLatest {
- callbackFlowFromNotifier(it.onDnsServersChanged)
- }
- .collect { servers ->
- customDnsAdapter?.updateServers(servers)
- }
- }
- }
- }
- }
-
+ @OptIn(ExperimentalMaterialApi::class)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
- val view = inflater.inflate(R.layout.advanced, container, false)
-
- view.findViewById<View>(R.id.back).setOnClickListener {
- customDnsAdapter?.stopEditing()
- requireActivity().onBackPressed()
- }
-
- titleController = CollapsibleTitleController(view, R.id.contents)
-
- customDnsAdapter = CustomDnsAdapter(
- onAddServer = { address ->
- serviceConnectionManager.customDns()?.addDnsServer(address) ?: false
- },
- onRemoveDnsServer = { address ->
- serviceConnectionManager.customDns()?.removeDnsServer(address) ?: false
- },
- onSetCustomDnsEnabled = { isEnabled ->
- if (isEnabled) {
- serviceConnectionManager.customDns()?.enable()
- } else {
- serviceConnectionManager.customDns()?.disable()
- }
- },
- onReplaceDnsServer = { oldServer, newServer ->
- serviceConnectionManager.customDns()?.replaceDnsServer(
- oldServer,
- newServer
- ) ?: false
- }
- ).also { newCustomDnsAdapter ->
-
- newCustomDnsAdapter.confirmAddAddress = ::confirmAddAddress
-
- view.findViewById<CustomRecyclerView>(R.id.contents).apply {
- layoutManager = LinearLayoutManager(requireContext())
-
- adapter = AdapterWithHeader(newCustomDnsAdapter, R.layout.advanced_header).apply {
- onHeaderAvailable = { headerView ->
- configureHeader(headerView)
- titleController.expandedTitleView =
- headerView.findViewById(R.id.expanded_title)
- }
- }
-
- addItemDecoration(
- ListItemDividerDecoration(
- topOffset = resources.getDimensionPixelSize(R.dimen.list_item_divider)
- )
+ return inflater.inflate(R.layout.fragment_compose, container, false).apply {
+ findViewById<ComposeView>(R.id.compose_view).setContent {
+ val state = vm.uiState.collectAsState().value
+ AdvancedSettingScreen(
+ uiState = state,
+ onMtuCellClick = vm::onMtuCellClick,
+ onMtuInputChange = vm::onMtuInputChange,
+ onSaveMtuClick = vm::onSaveMtuClick,
+ onRestoreMtuClick = vm::onRestoreMtuClick,
+ onCancelMtuDialogClicked = vm::onCancelDialogClick,
+ onSplitTunnelingNavigationClick = ::openSplitTunnelingFragment,
+ onToggleDnsClick = vm::onToggleDnsClick,
+ onDnsClick = vm::onDnsClick,
+ onDnsInputChange = vm::onDnsInputChange,
+ onSaveDnsClick = vm::onSaveDnsClick,
+ onRemoveDnsClick = vm::onRemoveDnsClick,
+ onCancelDnsDialogClick = vm::onCancelDialogClick,
+ onBackClick = { activity?.onBackPressed() }
)
}
}
-
- attachBackButtonHandler()
-
- return view
- }
-
- override fun onDestroyView() {
- detachBackButtonHandler()
- customDnsAdapter?.onDestroy()
- titleController.onDestroy()
- super.onDestroyView()
- }
-
- private fun configureHeader(view: View) {
- wireguardMtuInput = view.findViewById<MtuCell>(R.id.wireguard_mtu).apply {
- onSubmit = { mtu ->
- serviceConnectionManager.settingsListener()?.wireguardMtu = mtu
- }
- value = serviceConnectionManager.settingsListener()?.let { settingsNotifier ->
- settingsNotifier.wireguardMtu
- }
- }
-
- view.findViewById<NavigateCell>(R.id.split_tunneling).apply {
- targetFragment = SplitTunnelingFragment::class
- }
-
- customDnsToggle = view.findViewById<ToggleCell>(R.id.enable_custom_dns).apply {
- state = serviceConnectionManager.customDns().let { customDns ->
- if (customDns?.isCustomDnsEnabled() == true) {
- CellSwitch.State.ON
- } else {
- CellSwitch.State.OFF
- }
- }
-
- listener = { state ->
- jobTracker.newBackgroundJob("toggleCustomDns") {
- if (state == CellSwitch.State.ON) {
- serviceConnectionManager.customDns()?.enable()
- } else {
- serviceConnectionManager.customDns()?.disable()
- }
- }
- }
- }
- }
-
- private fun updateUi(settings: Settings) {
- if (this::wireguardMtuInput.isInitialized && wireguardMtuInput.hasFocus == false) {
- wireguardMtuInput.value = settings.tunnelOptions.wireguard.mtu
- }
- }
-
- private suspend fun confirmAddAddress(address: InetAddress): Boolean {
- val isLocalAddress = address.isLinkLocalAddress() || address.isSiteLocalAddress()
-
- return !isLocalAddress || isAllowLanEnabled || showConfirmDnsServerDialog()
- }
-
- private suspend fun showConfirmDnsServerDialog(): Boolean {
- val confirmation = CompletableDeferred<Boolean>()
- val transaction = parentFragmentManager.beginTransaction()
-
- detachBackButtonHandler()
- transaction.addToBackStack(null)
-
- ConfirmDnsDialogFragment(confirmation)
- .show(transaction, null)
-
- val result = confirmation.await()
-
- attachBackButtonHandler()
-
- return result
}
- private fun attachBackButtonHandler() {
- requireMainActivity().backButtonHandler = {
- if (customDnsAdapter?.isEditing == true) {
- customDnsAdapter?.stopEditing()
- }
- false
+ private fun openSplitTunnelingFragment() {
+ parentFragmentManager.beginTransaction().apply {
+ setCustomAnimations(
+ R.anim.fragment_enter_from_right,
+ R.anim.fragment_exit_to_left,
+ R.anim.fragment_half_enter_from_left,
+ R.anim.fragment_exit_to_right
+ )
+ replace(R.id.main_fragment, SplitTunnelingFragment())
+ addToBackStack(null)
+ commitAllowingStateLoss()
}
}
-
- private fun detachBackButtonHandler() {
- requireMainActivity().backButtonHandler = null
- }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index 7401ed9f68..a0fa0e613e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -30,7 +30,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ChangelogDialog
+import net.mullvad.mullvadvpn.compose.dialog.ChangelogDialog
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.di.uiModule
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt
deleted file mode 100644
index 1d0f940d4b..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/AddCustomDnsServerHolder.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.view.View
-import net.mullvad.mullvadvpn.R
-
-class AddCustomDnsServerHolder(view: View, adapter: CustomDnsAdapter) : CustomDnsItemHolder(view) {
- init {
- view.findViewById<View>(R.id.add).setOnClickListener {
- adapter.newDnsServer()
- }
-
- view.findViewById<View>(R.id.click_area).setOnClickListener {
- adapter.newDnsServer()
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt
deleted file mode 100644
index 1d44ca3a50..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsAdapter.kt
+++ /dev/null
@@ -1,302 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.RecyclerView.Adapter
-import java.net.InetAddress
-import kotlin.properties.Delegates.observable
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.util.JobTracker
-import org.apache.commons.validator.routines.InetAddressValidator
-
-class CustomDnsAdapter(
- val onSetCustomDnsEnabled: (Boolean) -> Unit,
- val onAddServer: (InetAddress) -> Boolean,
- val onRemoveDnsServer: (InetAddress) -> Unit,
- val onReplaceDnsServer: (InetAddress, InetAddress) -> Boolean
-) : Adapter<CustomDnsItemHolder>() {
- private enum class ViewTypes {
- ADD_SERVER,
- EDIT_SERVER,
- SHOW_SERVER,
- FOOTER,
- }
-
- private val customDnsServersLock = Mutex()
- private val inetAddressValidator = InetAddressValidator.getInstance()
- private val jobTracker = JobTracker()
-
- private var editingPosition: Int? = null
-
- private var activeCustomDnsServers by observable<List<InetAddress>>(
- emptyList()
- ) { _, _, servers ->
- if (servers != cachedCustomDnsServers) {
- cachedCustomDnsServers = servers.toMutableList()
- notifyDataSetChanged()
- }
- }
-
- private var cachedCustomDnsServers = emptyList<InetAddress>().toMutableList()
-
- private var enabled by observable(false) { _, oldValue, newValue ->
- if (oldValue != newValue) {
- if (newValue == true) {
- notifyItemRangeInserted(0, cachedCustomDnsServers.size + 1)
- } else {
- notifyItemRangeRemoved(0, cachedCustomDnsServers.size + 1)
- editingPosition = null
- }
- }
- }
-
- val isEditing
- get() = editingPosition != null
-
- // By default, refuse the address so that the dialog can be recreated by the user if needed
- var confirmAddAddress: suspend (InetAddress) -> Boolean = { false }
-
- fun updateServers(servers: List<InetAddress>) {
- jobTracker.newBackgroundJob("toggleCustomDns") {
- if (servers.isEmpty()) {
- onSetCustomDnsEnabled(false)
- }
- }
-
- jobTracker.newUiJob("updateDnsServers") {
- customDnsServersLock.withLock {
- activeCustomDnsServers = servers
- }
- }
- }
-
- fun updateState(isEnabled: Boolean) {
- jobTracker.newUiJob("updateEnabled") {
- customDnsServersLock.withLock {
- enabled = isEnabled
- }
- }
- }
-
- override fun getItemCount() =
- if (enabled) {
- cachedCustomDnsServers.size + 2
- } else {
- 1
- }
-
- override fun getItemViewType(position: Int): Int {
- val count = getItemCount()
- val footer = count - 1
- val addServer = count - 2
-
- if (position == footer) {
- return ViewTypes.FOOTER.ordinal
- } else if (position == editingPosition) {
- return ViewTypes.EDIT_SERVER.ordinal
- } else if (position == addServer) {
- return ViewTypes.ADD_SERVER.ordinal
- } else {
- return ViewTypes.SHOW_SERVER.ordinal
- }
- }
-
- override fun onCreateViewHolder(parentView: ViewGroup, type: Int): CustomDnsItemHolder {
- val inflater = LayoutInflater.from(parentView.context)
- when (ViewTypes.values()[type]) {
- ViewTypes.FOOTER -> {
- val view = inflater.inflate(R.layout.custom_dns_footer, parentView, false)
- return CustomDnsFooterHolder(view)
- }
- ViewTypes.ADD_SERVER -> {
- val view = inflater.inflate(R.layout.add_custom_dns_server, parentView, false)
- return AddCustomDnsServerHolder(view, this)
- }
- ViewTypes.EDIT_SERVER -> {
- val view = inflater.inflate(R.layout.edit_custom_dns_server, parentView, false)
- return EditCustomDnsServerHolder(view, this)
- }
- ViewTypes.SHOW_SERVER -> {
- val view = inflater.inflate(R.layout.custom_dns_server, parentView, false)
- return CustomDnsServerHolder(view, this)
- }
- }
- }
-
- override fun onBindViewHolder(holder: CustomDnsItemHolder, position: Int) {
- if (holder is CustomDnsServerHolder) {
- holder.serverAddress = cachedCustomDnsServers[position]
- } else if (holder is EditCustomDnsServerHolder) {
- if (position >= cachedCustomDnsServers.size) {
- holder.serverAddress = null
- } else {
- holder.serverAddress = cachedCustomDnsServers[position]
- }
- }
- }
-
- fun onDestroy() {
- jobTracker.newBackgroundJob("toggleCustomDns") {
- if (cachedCustomDnsServers.isEmpty()) {
- onSetCustomDnsEnabled(false)
- }
- }
- }
-
- fun newDnsServer() {
- jobTracker.newUiJob("newDnsServer") {
- customDnsServersLock.withLock {
- if (enabled) {
- val count = getItemCount()
-
- editDnsServerAt(count - 2)
- }
- }
- }
- }
-
- fun saveDnsServer(address: String, errorCallback: () -> Unit) {
- jobTracker.newUiJob("saveDnsServer $address") {
- customDnsServersLock.withLock {
- editingPosition?.let { position ->
- var validAddress: Boolean
-
- if (position >= cachedCustomDnsServers.size) {
- validAddress = addDnsServer(address)
- } else {
- validAddress = replaceDnsServer(address, position)
- }
-
- if (!validAddress) {
- errorCallback()
- }
- }
- }
- }
- }
-
- fun editDnsServer(address: InetAddress) {
- jobTracker.newUiJob("editDnsServer $address") {
- customDnsServersLock.withLock {
- if (enabled) {
- val position = cachedCustomDnsServers.indexOf(address)
-
- editDnsServerAt(position)
- }
- }
- }
- }
-
- fun stopEditing() {
- jobTracker.newUiJob("stopEditing") {
- customDnsServersLock.withLock {
- if (enabled) {
- editDnsServerAt(null)
- }
- }
- }
- }
-
- fun stopEditing(address: InetAddress) {
- jobTracker.newUiJob("stopEditing $address") {
- customDnsServersLock.withLock {
- if (enabled) {
- editingPosition?.let { position ->
- if (cachedCustomDnsServers.getOrNull(position) == address) {
- editDnsServerAt(null)
- }
- }
- }
- }
- }
- }
-
- fun removeDnsServer(address: InetAddress) {
- jobTracker.newUiJob("removeDnsServer $address") {
- customDnsServersLock.withLock {
- val position = jobTracker.runOnBackground {
- val index = cachedCustomDnsServers.indexOf(address)
- cachedCustomDnsServers.removeAt(index)
- onRemoveDnsServer(address)
- index
- }
-
- // Immediately disable custom dns in the ui when the last server in the list has
- // been removed to avoid glitches with the ADD_SERVER view.
- if (cachedCustomDnsServers.size == 0) {
- enabled = false
- }
-
- notifyItemRemoved(position)
- }
- }
- }
-
- private suspend fun addDnsServer(addressText: String): Boolean {
- var added = false
-
- withValidAddress(addressText) { address ->
- if (onAddServer(address)) {
- cachedCustomDnsServers.add(address)
- added = true
- }
- }
-
- if (added) {
- editingPosition = null
-
- val count = getItemCount()
-
- notifyItemChanged(count - 3)
- notifyItemInserted(count - 2)
- }
-
- return added
- }
-
- private suspend fun replaceDnsServer(address: String, position: Int): Boolean {
- var replaced = false
-
- withValidAddress(address) { newAddress ->
- val oldAddress = cachedCustomDnsServers[position]
-
- if (onReplaceDnsServer(oldAddress, newAddress)) {
- cachedCustomDnsServers[position] = newAddress
- replaced = true
- }
- }
-
- if (replaced) {
- editingPosition = null
- notifyItemChanged(position)
- }
-
- return replaced
- }
-
- private fun editDnsServerAt(position: Int?) {
- editingPosition?.let { oldPosition ->
- notifyItemChanged(oldPosition)
- }
-
- editingPosition = position
-
- position?.let { newPosition ->
- notifyItemChanged(newPosition)
- }
- }
-
- private suspend fun withValidAddress(addressText: String, handler: (InetAddress) -> Unit) {
- jobTracker.runOnBackground {
- if (inetAddressValidator.isValid(addressText)) {
- val address = InetAddress.getByName(addressText)
-
- if (!address.isLoopbackAddress() && confirmAddAddress(address)) {
- handler(address)
- }
- }
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt
deleted file mode 100644
index d09beffbce..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsFooterHolder.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.view.View
-
-class CustomDnsFooterHolder(view: View) : CustomDnsItemHolder(view)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt
deleted file mode 100644
index cfaf9399cc..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsItemHolder.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView.ViewHolder
-
-abstract class CustomDnsItemHolder(view: View) : ViewHolder(view)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt
deleted file mode 100644
index 49efad9310..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/CustomDnsServerHolder.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.view.View
-import android.widget.TextView
-import java.net.InetAddress
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.R
-import net.mullvad.talpid.util.addressString
-
-class CustomDnsServerHolder(view: View, adapter: CustomDnsAdapter) : CustomDnsItemHolder(view) {
- private val label: TextView = view.findViewById(R.id.label)
-
- var serverAddress by observable<InetAddress?>(null) { _, _, address ->
- label.text = address?.addressString() ?: ""
- }
-
- init {
- view.findViewById<View>(R.id.click_area).setOnClickListener {
- serverAddress?.let { address ->
- adapter.editDnsServer(address)
- }
- }
-
- view.findViewById<View>(R.id.remove).setOnClickListener {
- serverAddress?.let { address ->
- adapter.removeDnsServer(address)
- }
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt
deleted file mode 100644
index 5e62f47209..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/customdns/EditCustomDnsServerHolder.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package net.mullvad.mullvadvpn.ui.customdns
-
-import android.text.Editable
-import android.text.TextWatcher
-import android.view.View
-import android.view.View.OnFocusChangeListener
-import android.widget.EditText
-import java.net.InetAddress
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.util.setOnEnterOrDoneAction
-import net.mullvad.talpid.util.addressString
-
-class EditCustomDnsServerHolder(
- view: View,
- val adapter: CustomDnsAdapter
-) : CustomDnsItemHolder(view) {
- private enum class State {
- Normal,
- Error,
- }
-
- private val errorColor = view.context.getColor(R.color.red)
- private val normalColor = view.context.getColor(R.color.blue)
-
- private val input: EditText = view.findViewById<EditText>(R.id.input).apply {
- onFocusChangeListener = OnFocusChangeListener { _, hasFocus ->
- if (!hasFocus) {
- serverAddress?.let { address ->
- adapter.stopEditing(address)
- }
- }
- }
-
- setOnEnterOrDoneAction(::saveDnsServer)
- }
-
- private val watcher: TextWatcher = object : TextWatcher {
- override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
-
- override fun afterTextChanged(text: Editable) {
- state = State.Normal
- }
-
- override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {}
- }
-
- private var state by observable(State.Normal) { _, oldState, newState ->
- if (oldState != newState) {
- input.apply {
- when (newState) {
- State.Normal -> {
- setTextColor(normalColor)
- removeTextChangedListener(watcher)
- }
- State.Error -> {
- setTextColor(errorColor)
- addTextChangedListener(watcher)
- }
- }
- }
- }
- }
-
- var serverAddress by observable<InetAddress?>(null) { _, _, address ->
- if (address != null) {
- val addressString = address.addressString()
-
- input.setText(addressString)
- input.setSelection(addressString.length)
- } else {
- input.setText("")
- }
-
- input.requestFocus()
- }
-
- init {
- view.findViewById<View>(R.id.save).setOnClickListener {
- saveDnsServer()
- }
- }
-
- private fun saveDnsServer() {
- val onFailCallback = { state = State.Error }
-
- adapter.saveDnsServer(input.text.toString(), onFailCallback)
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmDnsDialogFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmDnsDialogFragment.kt
deleted file mode 100644
index 0e26163ac2..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmDnsDialogFragment.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package net.mullvad.mullvadvpn.ui.fragment
-
-import android.app.Dialog
-import android.content.DialogInterface
-import android.graphics.drawable.ColorDrawable
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams
-import android.widget.Button
-import androidx.fragment.app.DialogFragment
-import kotlinx.coroutines.CompletableDeferred
-import net.mullvad.mullvadvpn.R
-
-class ConfirmDnsDialogFragment @JvmOverloads constructor(
- private var confirmation: CompletableDeferred<Boolean>? = null
-) : DialogFragment() {
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- val view = inflater.inflate(R.layout.confirm_dns, container, false)
-
- view.findViewById<Button>(R.id.back_button).setOnClickListener {
- activity?.onBackPressed()
- }
-
- view.findViewById<Button>(R.id.confirm_button).setOnClickListener {
- confirmation?.complete(true)
- confirmation = null
- dismiss()
- }
-
- return view
- }
-
- override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
- val dialog = super.onCreateDialog(savedInstanceState)
-
- dialog.window?.setBackgroundDrawable(ColorDrawable(android.R.color.transparent))
-
- return dialog
- }
-
- override fun onStart() {
- super.onStart()
-
- dialog?.window?.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
-
- if (confirmation == null) {
- dismiss()
- }
- }
-
- override fun onDismiss(dialogInterface: DialogInterface) {
- confirmation?.complete(false)
- }
-
- override fun onDestroy() {
- confirmation?.cancel()
-
- super.onDestroy()
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/MtuCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/MtuCell.kt
deleted file mode 100644
index 93daba0856..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/MtuCell.kt
+++ /dev/null
@@ -1,85 +0,0 @@
-package net.mullvad.mullvadvpn.ui.widget
-
-import android.content.Context
-import android.text.Editable
-import android.text.TextWatcher
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.EditText
-import android.widget.TextView
-import kotlin.properties.Delegates.observable
-import net.mullvad.mullvadvpn.R
-
-private const val MIN_MTU_VALUE = 1280
-private const val MAX_MTU_VALUE = 1420
-
-class MtuCell : Cell {
- private val input =
- (LayoutInflater.from(context).inflate(R.layout.mtu_edit_text, null) as EditText).apply {
- val width = resources.getDimensionPixelSize(R.dimen.cell_input_width)
- val height = resources.getDimensionPixelSize(R.dimen.cell_input_height)
-
- layoutParams = LayoutParams(width, height, 0.0f)
-
- addTextChangedListener(InputWatcher())
- setOnFocusChangeListener { _, newHasFocus -> hasFocus = newHasFocus }
- }
-
- private val validInputColor = context.getColor(R.color.white)
- private val invalidInputColor = context.getColor(R.color.red)
-
- var value: Int?
- get() = input.text.toString().trim().toIntOrNull()
- set(value) = input.setText(value?.toString() ?: "")
-
- var onSubmit: ((Int?) -> Unit)? = null
-
- var hasFocus by observable(false) { _, oldValue, newValue ->
- if (oldValue && !newValue) {
- val mtu = value
-
- if (mtu == null || (mtu in MIN_MTU_VALUE..MAX_MTU_VALUE)) {
- onSubmit?.invoke(mtu)
- }
- }
- }
-
- @JvmOverloads
- constructor(
- context: Context,
- attributes: AttributeSet? = null,
- defaultStyleAttribute: Int = 0,
- defaultStyleResource: Int = 0
- ) : super(
- context,
- attributes,
- defaultStyleAttribute,
- defaultStyleResource,
- TextView(context)
- ) {
- cell.apply {
- setEnabled(false)
- setFocusable(false)
- addView(input)
- }
-
- footer?.text =
- context.getString(R.string.wireguard_mtu_footer, MIN_MTU_VALUE, MAX_MTU_VALUE)
- }
-
- inner class InputWatcher : TextWatcher {
- override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
-
- override fun onTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {}
-
- override fun afterTextChanged(text: Editable) {
- val value = text.toString().trim().toIntOrNull()
-
- if (value != null && value >= MIN_MTU_VALUE && value <= MAX_MTU_VALUE) {
- input.setTextColor(validInputColor)
- } else {
- input.setTextColor(invalidInputColor)
- }
- }
- }
-}
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
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/res/drawable/cell_input_background.xml b/android/app/src/main/res/drawable/cell_input_background.xml
deleted file mode 100644
index 436b3adb6e..0000000000
--- a/android/app/src/main/res/drawable/cell_input_background.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <corners android:radius="4dp" />
- <solid android:color="@color/white10" />
-</shape>
diff --git a/android/app/src/main/res/drawable/cell_input_cursor.xml b/android/app/src/main/res/drawable/cell_input_cursor.xml
deleted file mode 100644
index 781c1d9b87..0000000000
--- a/android/app/src/main/res/drawable/cell_input_cursor.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@color/white" />
- <size android:width="1sp"
- android:height="24sp" />
-</shape>
diff --git a/android/app/src/main/res/drawable/icon_add.xml b/android/app/src/main/res/drawable/icon_add.xml
deleted file mode 100644
index f44a660a95..0000000000
--- a/android/app/src/main/res/drawable/icon_add.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<rotate xmlns:android="http://schemas.android.com/apk/res/android"
- android:fromDegrees="45"
- android:toDegrees="45"
- android:pivotX="50%"
- android:pivotY="50%"
- android:drawable="@drawable/icon_close" />
diff --git a/android/app/src/main/res/drawable/icon_check.xml b/android/app/src/main/res/drawable/icon_check.xml
deleted file mode 100644
index b5bbbc6dd2..0000000000
--- a/android/app/src/main/res/drawable/icon_check.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <group>
- <path android:fillColor="@color/colorPrimary"
- android:pathData="M12,24A12,12 0,0 1,3.515 3.515a12,12 0,1 1,16.97 16.97A11.922,11.922 0,0 1,12 24zM5.345,10.9a1.108,1.108 0,0 0,-0.785 0.322,1.095 1.095,0 0,0 0,1.556L9,17.177a1.115,1.115 0,0 0,1.569 0l8.874,-8.8a1.095,1.095 0,0 0,0 -1.556,1.116 1.116,0 0,0 -1.569,0l-8.092,8.024 -3.653,-3.623a1.106,1.106 0,0 0,-0.784 -0.322z" />
- </group>
-</vector>
diff --git a/android/app/src/main/res/layout/add_custom_dns_server.xml b/android/app/src/main/res/layout/add_custom_dns_server.xml
deleted file mode 100644
index 892b48a6fe..0000000000
--- a/android/app/src/main/res/layout/add_custom_dns_server.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@color/blue40"
- android:orientation="horizontal">
- <TextView android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginLeft="54dp"
- android:layout_marginVertical="14dp"
- android:background="?android:attr/selectableItemBackground"
- android:gravity="center_vertical"
- android:textColor="@color/white"
- android:textSize="@dimen/text_medium"
- android:text="@string/add_a_server" />
- <View android:id="@+id/click_area"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignParentLeft="true"
- android:layout_alignParentRight="true"
- android:focusable="true"
- android:clickable="true"
- android:background="?android:attr/selectableItemBackground" />
- <ImageButton android:id="@+id/add"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="right"
- android:paddingHorizontal="16dp"
- android:paddingVertical="14dp"
- android:background="?android:attr/selectableItemBackground"
- android:src="@drawable/icon_add" />
-</FrameLayout>
diff --git a/android/app/src/main/res/layout/advanced.xml b/android/app/src/main/res/layout/advanced.xml
deleted file mode 100644
index 42f94b7b7f..0000000000
--- a/android/app/src/main/res/layout/advanced.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:mullvad="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@color/darkBlue"
- android:gravity="left">
- <TextView android:id="@+id/title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/settings_advanced"
- style="@style/SettingsCollapsedHeader" />
- <LinearLayout android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
- <FrameLayout android:layout_width="match_parent"
- android:layout_height="wrap_content">
- <net.mullvad.mullvadvpn.ui.widget.BackButton android:id="@+id/back"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- mullvad:text="@string/settings" />
- <TextView android:id="@+id/collapsed_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginHorizontal="4dp"
- android:layout_gravity="center"
- android:text="@string/settings_advanced"
- style="@style/SettingsCollapsedHeader" />
- </FrameLayout>
- <net.mullvad.mullvadvpn.ui.widget.CustomRecyclerView android:id="@+id/contents"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scrollbars="vertical" />
- </LinearLayout>
-</FrameLayout>
diff --git a/android/app/src/main/res/layout/advanced_header.xml b/android/app/src/main/res/layout/advanced_header.xml
deleted file mode 100644
index 70a583ea7c..0000000000
--- a/android/app/src/main/res/layout/advanced_header.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:mullvad="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:gravity="left">
- <TextView android:id="@+id/expanded_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="2dp"
- android:layout_marginLeft="@dimen/side_margin"
- android:lines="1"
- android:text="@string/settings_advanced"
- style="@style/SettingsExpandedHeader" />
- <net.mullvad.mullvadvpn.ui.widget.MtuCell android:id="@+id/wireguard_mtu"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/vertical_space"
- android:focusable="true"
- android:focusableInTouchMode="true"
- mullvad:text="@string/wireguard_mtu" />
- <net.mullvad.mullvadvpn.ui.widget.NavigateCell android:id="@+id/split_tunneling"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/vertical_space"
- mullvad:text="@string/split_tunneling" />
- <net.mullvad.mullvadvpn.ui.widget.ToggleCell android:id="@+id/enable_custom_dns"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/vertical_space"
- mullvad:text="@string/enable_custom_dns" />
-</LinearLayout>
diff --git a/android/app/src/main/res/layout/confirm_dns.xml b/android/app/src/main/res/layout/confirm_dns.xml
deleted file mode 100644
index 6c7266eae9..0000000000
--- a/android/app/src/main/res/layout/confirm_dns.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:scrollbars="none">
- <LinearLayout android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:padding="30dp"
- android:background="@drawable/dialog_background"
- android:orientation="vertical"
- android:gravity="left">
- <ImageView android:layout_width="44dp"
- android:layout_height="44dp"
- android:layout_marginTop="8dp"
- android:layout_gravity="center"
- android:src="@drawable/icon_alert" />
- <TextView android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_weight="0"
- android:layout_marginTop="16dp"
- android:textColor="@color/white80"
- android:textSize="@dimen/text_small"
- android:text="@string/confirm_local_dns" />
- <Button android:id="@+id/confirm_button"
- android:layout_marginVertical="@dimen/button_separation"
- android:text="@string/add_anyway"
- style="@style/RedButton" />
- <Button android:id="@+id/back_button"
- android:text="@string/back"
- style="@style/BlueButton" />
- </LinearLayout>
-</ScrollView>
diff --git a/android/app/src/main/res/layout/custom_dns_footer.xml b/android/app/src/main/res/layout/custom_dns_footer.xml
deleted file mode 100644
index c939eebb7f..0000000000
--- a/android/app/src/main/res/layout/custom_dns_footer.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center">
- <TextView android:id="@+id/name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:paddingHorizontal="@dimen/cell_footer_horizontal_padding"
- android:paddingBottom="@dimen/screen_vertical_margin"
- android:paddingTop="@dimen/cell_footer_top_padding"
- android:textColor="@color/white60"
- android:textSize="@dimen/text_small"
- android:text="@string/custom_dns_footer" />
-</FrameLayout>
diff --git a/android/app/src/main/res/layout/custom_dns_server.xml b/android/app/src/main/res/layout/custom_dns_server.xml
deleted file mode 100644
index 54d7e9f01e..0000000000
--- a/android/app/src/main/res/layout/custom_dns_server.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@color/blue40"
- android:orientation="horizontal">
- <TextView android:id="@+id/label"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginLeft="54dp"
- android:layout_marginVertical="14dp"
- android:background="?android:attr/selectableItemBackground"
- android:gravity="center_vertical"
- android:textColor="@color/white"
- android:textSize="@dimen/text_medium" />
- <View android:id="@+id/click_area"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_alignParentLeft="true"
- android:layout_alignParentRight="true"
- android:focusable="true"
- android:clickable="true"
- android:background="?android:attr/selectableItemBackground" />
- <ImageButton android:id="@+id/remove"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="right"
- android:paddingHorizontal="16dp"
- android:paddingVertical="14dp"
- android:background="?android:attr/selectableItemBackground"
- android:src="@drawable/icon_close" />
-</FrameLayout>
diff --git a/android/app/src/main/res/layout/edit_custom_dns_server.xml b/android/app/src/main/res/layout/edit_custom_dns_server.xml
deleted file mode 100644
index 855504f077..0000000000
--- a/android/app/src/main/res/layout/edit_custom_dns_server.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@color/white"
- android:orientation="horizontal">
- <EditText android:id="@+id/input"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:layout_marginLeft="54dp"
- android:layout_marginVertical="14dp"
- android:gravity="center_vertical"
- android:background="@android:color/transparent"
- android:singleLine="true"
- android:imeOptions="flagNoPersonalizedLearning"
- android:textCursorDrawable="@drawable/text_input_cursor"
- android:textColorHint="@color/blue60"
- android:textColor="@color/blue"
- android:textSize="@dimen/text_medium"
- android:hint="@string/custom_dns_hint" />
- <ImageButton android:id="@+id/save"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_weight="0"
- android:layout_gravity="right"
- android:paddingHorizontal="16dp"
- android:paddingVertical="14dp"
- android:background="?android:attr/selectableItemBackground"
- android:src="@drawable/icon_check" />
-</LinearLayout>
diff --git a/android/app/src/main/res/layout/mtu_edit_text.xml b/android/app/src/main/res/layout/mtu_edit_text.xml
deleted file mode 100644
index 11334cf4c1..0000000000
--- a/android/app/src/main/res/layout/mtu_edit_text.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<EditText xmlns:android="http://schemas.android.com/apk/res/android"
- android:paddingHorizontal="8dp"
- android:paddingVertical="4dp"
- android:background="@drawable/cell_input_background"
- android:digits="0123456789"
- android:inputType="number"
- android:singleLine="true"
- android:imeOptions="flagNoPersonalizedLearning"
- android:textCursorDrawable="@drawable/cell_input_cursor"
- android:gravity="center"
- android:hint="@string/hint_default"
- android:textColorHint="@color/white80"
- android:textColor="@color/white"
- android:textSize="@dimen/text_medium_plus" />
diff --git a/android/app/src/main/res/values-da/strings.xml b/android/app/src/main/res/values-da/strings.xml
index b7ed47f668..41e79658f5 100644
--- a/android/app/src/main/res/values-da/strings.xml
+++ b/android/app/src/main/res/values-da/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Viser påmindelser, når kontotiden er ved at udløbe</string>
<string name="account_time_notification_channel_name">Påmindelser om kontotid</string>
<string name="add_a_server">Tilføj en server</string>
- <string name="add_anyway">Tilføj alligevel</string>
<string name="add_time_to_account">Køb enten kredit på vores hjemmeside, eller indløs en kupon.</string>
<string name="all_applications">Alle applikationer</string>
<string name="allow_lan_footer">Giver adgang til andre enheder på det samme netværk til deling, udskrivning osv.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">OPRETTER SIKKER FORBINDELSE</string>
<string name="critical_error">Kritisk fejl (som kræver din opmærksomhed)</string>
<string name="custom_dns_footer">Aktiver for at tilføje mindst én DNS-server.</string>
- <string name="custom_dns_hint">Indtast IP</string>
<string name="custom_tunnel_host_resolution_error">Kunne ikke fortolke værtsnavnet på den tilpassede server</string>
<string name="device_inactive_description">Du har fjernet denne enhed. For at oprette forbindelse igen skal du logge ind igen.</string>
<string name="device_inactive_title">Enheden er inaktiv</string>
diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml
index 476caab0bc..072de1fe8a 100644
--- a/android/app/src/main/res/values-de/strings.xml
+++ b/android/app/src/main/res/values-de/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Erinnerungen anzeigen, wenn die Kontozeit bald abläuft</string>
<string name="account_time_notification_channel_name">Erinnerungen an die Kontozeit</string>
<string name="add_a_server">Server hinzufügen</string>
- <string name="add_anyway">Trotzdem hinzufügen</string>
<string name="add_time_to_account">Kaufen Sie entweder Guthaben über unsere Seite oder lösen Sie einen Gutschein ein.</string>
<string name="all_applications">Alle Anwendungen</string>
<string name="allow_lan_footer">Ermöglicht den Zugriff auf andere Geräte im selben Netzwerk zum Teilen von Dateien, Drucken etc.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">SICHERE VERBINDUNG WIRD ERSTELLT</string>
<string name="critical_error">Kritischer Fehler (Ihre Aufmerksamkeit ist erforderlich)</string>
<string name="custom_dns_footer">Aktivieren, um mindestens einen DNS-Server hinzuzufügen.</string>
- <string name="custom_dns_hint">IP eingeben</string>
<string name="custom_tunnel_host_resolution_error">Der Hostname des benutzerdefinierten Servers konnte nicht aufgelöst werden</string>
<string name="device_inactive_description">Sie haben dieses Gerät entfernt. Um sich erneut zu verbinden, müssen Sie sich erneut anmelden.</string>
<string name="device_inactive_title">Gerät ist inaktiv</string>
diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml
index 13bdc1d505..52b5381c0d 100644
--- a/android/app/src/main/res/values-es/strings.xml
+++ b/android/app/src/main/res/values-es/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Muestra avisos cuando el tiempo de la cuenta está a punto de caducar</string>
<string name="account_time_notification_channel_name">Recordatorios de tiempo de la cuenta</string>
<string name="add_a_server">Añadir un servidor</string>
- <string name="add_anyway">Añadir de todos modos</string>
<string name="add_time_to_account">Compre crédito en nuestro sitio web o canjee un cupón.</string>
<string name="all_applications">Todas las aplicaciones</string>
<string name="allow_lan_footer">Permite el acceso a otros dispositivos de la misma red para compartir archivos, imprimir, etc.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">CREANDO CONEXIÓN SEGURA</string>
<string name="critical_error">Error crítico (precisa su atención)</string>
<string name="custom_dns_footer">Active esta opción para agregar como mínimo un servidor DNS.</string>
- <string name="custom_dns_hint">Escriba la IP</string>
<string name="custom_tunnel_host_resolution_error">No se puede resolver el nombre de host del servidor personalizado</string>
<string name="device_inactive_description">Ha quitado este dispositivo. Vuelva a iniciar la sesión para conectarse.</string>
<string name="device_inactive_title">El dispositivo está inactivo</string>
diff --git a/android/app/src/main/res/values-fi/strings.xml b/android/app/src/main/res/values-fi/strings.xml
index 5847fe930f..fa4d54f1d7 100644
--- a/android/app/src/main/res/values-fi/strings.xml
+++ b/android/app/src/main/res/values-fi/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Näyttää muistutuksia, kun tilin käyttöaika on umpeutumassa</string>
<string name="account_time_notification_channel_name">Muistutukset tilin käyttöajasta</string>
<string name="add_a_server">Lisää palvelin</string>
- <string name="add_anyway">Lisää silti</string>
<string name="add_time_to_account">Osta käyttöaikaa verkkosivustoltamme tai lunasta kuponki.</string>
<string name="all_applications">Kaikki sovellukset</string>
<string name="allow_lan_footer">Sallii jakamisen, tulostuksen ym. saman verkon muille laitteille.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">LUODAAN SUOJATTU YHTEYS</string>
<string name="critical_error">Vakava virhe (vaatii huomiotasi)</string>
<string name="custom_dns_footer">Ota käyttöön lisätäksesi vähintään yhden DNS-palvelimen.</string>
- <string name="custom_dns_hint">Anna IP-osoite</string>
<string name="custom_tunnel_host_resolution_error">Mukautetun palvelimen isäntänimen selvittäminen epäonnistui</string>
<string name="device_inactive_description">Olet poistanut tämän laitteen. Jos haluat muodostaa yhteyden uudelleen, sinun täytyy kirjautua takaisin sisään.</string>
<string name="device_inactive_title">Laite ei ole aktiivinen</string>
diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml
index 0c8ee02884..8c32c7ddc6 100644
--- a/android/app/src/main/res/values-fr/strings.xml
+++ b/android/app/src/main/res/values-fr/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Affiche des rappels lorsque le temps du compte va expirer</string>
<string name="account_time_notification_channel_name">Rappels de temps pour le compte</string>
<string name="add_a_server">Ajouter un serveur</string>
- <string name="add_anyway">Ajouter quand même</string>
<string name="add_time_to_account">Achetez du crédit sur notre site web ou échangez un bon.</string>
<string name="all_applications">Toutes les applications</string>
<string name="allow_lan_footer">Autorise l\'accès aux autres appareils sur le même réseau pour partager, imprimer, etc.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">CRÉATION D\'UNE CONNEXION SÉCURISÉE</string>
<string name="critical_error">Erreur critique (votre attention est requise)</string>
<string name="custom_dns_footer">Activez pour ajouter au moins un serveur DNS.</string>
- <string name="custom_dns_hint">Saisir l\'IP</string>
<string name="custom_tunnel_host_resolution_error">Échec de la résolution du nom d\'hôte du serveur personnalisé</string>
<string name="device_inactive_description">Vous avez supprimé cet appareil. Vous devrez vous reconnecter pour connecter cet appareil à nouveau.</string>
<string name="device_inactive_title">L\'appareil est inactif</string>
diff --git a/android/app/src/main/res/values-it/strings.xml b/android/app/src/main/res/values-it/strings.xml
index 51bcdaff09..be38a145fe 100644
--- a/android/app/src/main/res/values-it/strings.xml
+++ b/android/app/src/main/res/values-it/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Mostra promemoria quando il tempo dell\'account sta per scadere</string>
<string name="account_time_notification_channel_name">Promemoria temporali per l\'account</string>
<string name="add_a_server">Aggiungi un server</string>
- <string name="add_anyway">Aggiungi comunque</string>
<string name="add_time_to_account">Acquista credito sul nostro sito web o riscatta un voucher.</string>
<string name="all_applications">Tutte le applicazioni</string>
<string name="allow_lan_footer">Consenti l\'accesso ad altri dispositivi sulla stessa rete per condividere, stampare e altro.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">CREAZIONE CONNESSIONE PROTETTA</string>
<string name="critical_error">Errore critico (è necessario intervenire)</string>
<string name="custom_dns_footer">Abilita per aggiungere almeno un server DNS.</string>
- <string name="custom_dns_hint">Inserisci IP</string>
<string name="custom_tunnel_host_resolution_error">Impossibile risolvere il nome host del server personalizzato</string>
<string name="device_inactive_description">Hai rimosso questo dispositivo. Per riconnetterti, dovrai effettuare nuovamente il login.</string>
<string name="device_inactive_title">Il dispositivo è inattivo</string>
diff --git a/android/app/src/main/res/values-ja/strings.xml b/android/app/src/main/res/values-ja/strings.xml
index 8072f2ae86..d48abcd90a 100644
--- a/android/app/src/main/res/values-ja/strings.xml
+++ b/android/app/src/main/res/values-ja/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">アカウントの期限切れが迫っているときにリマインダーを表示します</string>
<string name="account_time_notification_channel_name">アカウント時間のリマインダー</string>
<string name="add_a_server">サーバーを追加</string>
- <string name="add_anyway">追加を続ける</string>
<string name="add_time_to_account">当社ウェブサイトでクレジットを購入するか、バウチャーを使用してください。</string>
<string name="all_applications">すべてのアプリケーション</string>
<string name="allow_lan_footer">共有や印刷などのため、同一ネットワーク上の他のデバイスへのアクセスを許可します。</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">セキュリティ保護接続を確立中</string>
<string name="critical_error">重大なエラー (ご注意ください)</string>
<string name="custom_dns_footer">1つ以上のDNSサーバーを追加するには有効にしてください。</string>
- <string name="custom_dns_hint">IPを入力</string>
<string name="custom_tunnel_host_resolution_error">カスタムサーバーのホスト名を解決できませんでした</string>
<string name="device_inactive_description">このデバイスを削除しました。再度接続するには、ログインし直す必要があります。</string>
<string name="device_inactive_title">デバイスが無効です</string>
diff --git a/android/app/src/main/res/values-ko/strings.xml b/android/app/src/main/res/values-ko/strings.xml
index e40286aa59..4b76cc5175 100644
--- a/android/app/src/main/res/values-ko/strings.xml
+++ b/android/app/src/main/res/values-ko/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">계정 시간이 만료되려고 할 때 알림 표시</string>
<string name="account_time_notification_channel_name">계정 시간 알림</string>
<string name="add_a_server">서버 추가</string>
- <string name="add_anyway">추가</string>
<string name="add_time_to_account">웹 사이트에서 크레딧을 구매하거나 바우처를 사용하세요.</string>
<string name="all_applications">모든 애플리케이션</string>
<string name="allow_lan_footer">공유, 인쇄 등을 위해 동일한 네트워크의 다른 장치에 액세스할 수 있습니다.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">보안 연결 생성 중</string>
<string name="critical_error">심각한 오류(주의가 필요함)</string>
<string name="custom_dns_footer">하나 이상의 DNS 서버를 추가하려면 활성화합니다.</string>
- <string name="custom_dns_hint">IP 입력</string>
<string name="custom_tunnel_host_resolution_error">사용자 지정 서버의 호스트 이름을 확인하지 못함</string>
<string name="device_inactive_description">이 장치를 제거했습니다. 다시 연결하려면 다시 로그인해야 합니다.</string>
<string name="device_inactive_title">장치가 비활성 상태입니다.</string>
diff --git a/android/app/src/main/res/values-my/strings.xml b/android/app/src/main/res/values-my/strings.xml
index cdf8a4f56c..1feff0574f 100644
--- a/android/app/src/main/res/values-my/strings.xml
+++ b/android/app/src/main/res/values-my/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">အကောင့်အချိန် သက်တမ်းကုန်ခါနီးချိန်၌ သတိပေးချက်များ ပြသပေးပါသည်</string>
<string name="account_time_notification_channel_name">အကောင့်အချိန် သတိပေးချက်များ</string>
<string name="add_a_server">ဆာဗာ ပေါင်းထည့်ရန်</string>
- <string name="add_anyway">မည်သို့ပင်ဖြစ်စေ ပေါင်းထည့်ရန်</string>
<string name="add_time_to_account">ကျွန်ုပ်တို့၏ ဝက်ဘ်ဆိုက်တွင် ခရက်ဒစ် ဝယ်ယူပါ သို့မဟုတ် ဘောက်ချာဖြင့် လဲယူပါ။</string>
<string name="all_applications">အပလီကေးရှင်း အားလုံး</string>
<string name="allow_lan_footer">ဝေမျှရန်၊ ပရင့်ထုတ်ရန်စသည်တို့အတွက် တူညီသည့် ကွန်ရက်ရှိ အခြားစက်များ ရယူသုံးစွဲခွင့်ပြုပေးပါသည်။</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">လုံခြုံသည့် ချိတ်ဆက်မှုကို ဖန်တီးနေပါသည်</string>
<string name="critical_error">အလွန်အရေးပါသည့် ချို့ယွင်းချက် (သင့်အာရုံစိုက်မှု လိုအပ်ပါသည်)</string>
<string name="custom_dns_footer">အနည်းဆုံး DNS ဆာဗာတစ်ခုကို ပေါင်းထည့်ပါ။</string>
- <string name="custom_dns_hint">IP ဖြည့်ပါ</string>
<string name="custom_tunnel_host_resolution_error">စိတ်ကြိုက် ဆာဗာ၏ Hostname ကို ဖြေရှင်း၍ မရနိုင်ပါ</string>
<string name="device_inactive_description">ဤစက်ကို ဖယ်ရှားပြီး ဖြစ်သည်။ ထပ်မံချိတ်ဆက်ရန်အတွက် ပြန်လည် ဝင်ရောက်ရန် လိုပါသည်။</string>
<string name="device_inactive_title">စက်သည် သက်ဝင်လုပ်ဆောင်မှု မရှိပါ</string>
diff --git a/android/app/src/main/res/values-nb/strings.xml b/android/app/src/main/res/values-nb/strings.xml
index 3fed35b942..07d6cbd657 100644
--- a/android/app/src/main/res/values-nb/strings.xml
+++ b/android/app/src/main/res/values-nb/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Viser påminnelser når tidsavbrudd for kontoen er i ferd med å inntreffe</string>
<string name="account_time_notification_channel_name">Påminnelser om tidsavbrudd for konto</string>
<string name="add_a_server">Legg til en server</string>
- <string name="add_anyway">Legg til likevel</string>
<string name="add_time_to_account">Du kan enten kjøpe kreditt på nettsiden vår eller løse inn en kupong.</string>
<string name="all_applications">Alle applikasjoner</string>
<string name="allow_lan_footer">Gir tilgang til andre enheter på samme nettverk for deling, utskrift osv.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">OPPRETTER SIKKER TILKOBLING</string>
<string name="critical_error">Kritisk feil (krever din oppmerksomhet)</string>
<string name="custom_dns_footer">Aktiver for å legge til minst én DNS-server.</string>
- <string name="custom_dns_hint">Angi IP</string>
<string name="custom_tunnel_host_resolution_error">Kunne ikke løse vertsnavnet til den egendefinerte serveren</string>
<string name="device_inactive_description">Du har fjernet denne enheten. For å koble til igjen, må du logge inn på nytt.</string>
<string name="device_inactive_title">Enheten er inaktiv</string>
diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml
index 442d0de4c3..dda01f6c11 100644
--- a/android/app/src/main/res/values-nl/strings.xml
+++ b/android/app/src/main/res/values-nl/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Toont herinneringen wanneer de accounttijd op het punt staat te verlopen</string>
<string name="account_time_notification_channel_name">Accounttijdherinneringen</string>
<string name="add_a_server">Server toevoegen</string>
- <string name="add_anyway">Toch toevoegen</string>
<string name="add_time_to_account">Koop krediet op onze website of wissel een voucher in.</string>
<string name="all_applications">Alle toepassingen</string>
<string name="allow_lan_footer">Biedt toegang tot andere apparaten op hetzelfde netwerk voor delen, afdrukken en dergelijke</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">BEVEILIGDE VERBINDING AANMAKEN</string>
<string name="critical_error">Kritieke fout (uw aandacht is vereist)</string>
<string name="custom_dns_footer">Schakel in om minimaal één DNS-server toe te voegen.</string>
- <string name="custom_dns_hint">Voer IP-adres in</string>
<string name="custom_tunnel_host_resolution_error">Kon de hostnaam van de aangepaste server niet omzetten</string>
<string name="device_inactive_description">U hebt dit apparaat verwijderd. U moet zich opnieuw aanmelden om het opnieuw te verbinden.</string>
<string name="device_inactive_title">Apparaat is niet actief</string>
diff --git a/android/app/src/main/res/values-pl/strings.xml b/android/app/src/main/res/values-pl/strings.xml
index d07c6eb5a7..2e94cb447c 100644
--- a/android/app/src/main/res/values-pl/strings.xml
+++ b/android/app/src/main/res/values-pl/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Pokazuje przypomnienia, gdy kończy się czas na koncie</string>
<string name="account_time_notification_channel_name">Przypomnienia o czasie na koncie</string>
<string name="add_a_server">Dodaj serwer</string>
- <string name="add_anyway">Mimo to dodaj</string>
<string name="add_time_to_account">Doładuj w naszej witrynie internetowej lub zrealizuj kupon.</string>
<string name="all_applications">Wszystkie aplikacje</string>
<string name="allow_lan_footer">Umożliwia dostęp do innych urządzeń w tej samej sieci w celu udostępniania, drukowania itd.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">TWORZENIE BEZPIECZNEGO POŁĄCZENIA</string>
<string name="critical_error">Błąd krytyczny (wymagana uwaga)</string>
<string name="custom_dns_footer">Włącz, aby dodać co najmniej jeden serwer DNS.</string>
- <string name="custom_dns_hint">Wprowadź adres IP</string>
<string name="custom_tunnel_host_resolution_error">Nie można rozpoznać nazwy hosta serwera niestandardowego</string>
<string name="device_inactive_description">Urządzenie usunięto. Aby połączyć się ponownie, musisz się ponownie zalogować.</string>
<string name="device_inactive_title">Urządzenie nieaktywne</string>
diff --git a/android/app/src/main/res/values-pt/strings.xml b/android/app/src/main/res/values-pt/strings.xml
index c99e0fc70c..50f688114a 100644
--- a/android/app/src/main/res/values-pt/strings.xml
+++ b/android/app/src/main/res/values-pt/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Mostra lembretes quando o tempo da conta está prestes a expirar</string>
<string name="account_time_notification_channel_name">Lembretes de tempo da conta</string>
<string name="add_a_server">Adicionar um servidor</string>
- <string name="add_anyway">Adicionar mesmo assim</string>
<string name="add_time_to_account">Compre crédito no nosso sítio da web ou reclame um voucher.</string>
<string name="all_applications">Todas as aplicações</string>
<string name="allow_lan_footer">Permite o acesso a outros dispositivos na mesma rede para partilha, impressão, etc.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">A CRIAR LIGAÇÃO SEGURA</string>
<string name="critical_error">Erro crítico (é necessária a sua atenção)</string>
<string name="custom_dns_footer">Ativar para adicionar pelo menos um servidor DNS.</string>
- <string name="custom_dns_hint">Introduzir IP</string>
<string name="custom_tunnel_host_resolution_error">Não foi possível resolver o nome do anfitrião do servidor personalizado</string>
<string name="device_inactive_description">Removeu este dispositivo. Para voltar a ligar o dispositivo, terá de voltar a iniciar a sessão.</string>
<string name="device_inactive_title">O dispositivo está desativado</string>
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index ffe73a4aae..2431264cd9 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Показывает уведомления, когда время на учетной записи скоро закончится</string>
<string name="account_time_notification_channel_name">Напоминания о времени на учетной записи</string>
<string name="add_a_server">Добавить сервер</string>
- <string name="add_anyway">Всё равно добавить</string>
<string name="add_time_to_account">Пополните баланс у нас на сайте или погасите ваучер.</string>
<string name="all_applications">Все приложения</string>
<string name="allow_lan_footer">Разрешить доступ к другим устройствам в той же сети для передачи данных, печати и т. д.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">СОЗДАНИЕ ЗАЩИЩЕННОГО ПОДКЛЮЧЕНИЯ</string>
<string name="critical_error">Критическая ошибка (требуется ваше участие)</string>
<string name="custom_dns_footer">Чтобы добавить как минимум один DNS-сервер, включите этот параметр.</string>
- <string name="custom_dns_hint">Введите IP-адрес</string>
<string name="custom_tunnel_host_resolution_error">Не удалось преобразовать имя узла пользовательского сервера</string>
<string name="device_inactive_description">Вы удалили это устройство. Чтобы снова подключиться, нужно будет выполнить вход.</string>
<string name="device_inactive_title">Устройство неактивно</string>
diff --git a/android/app/src/main/res/values-sv/strings.xml b/android/app/src/main/res/values-sv/strings.xml
index 116c34f7d4..f132492fb0 100644
--- a/android/app/src/main/res/values-sv/strings.xml
+++ b/android/app/src/main/res/values-sv/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Visar påminnelser när kontots tidsgräns uppnås</string>
<string name="account_time_notification_channel_name">Påminnelser om kontotid</string>
<string name="add_a_server">Lägg till en server</string>
- <string name="add_anyway">Lägg till ändå</string>
<string name="add_time_to_account">Du kan antingen köpa kredit på vår webbplats eller lösa in en kupong.</string>
<string name="all_applications">Alla applikationer</string>
<string name="allow_lan_footer">Tillåter åtkomst till andra enheter i samma nätverk för delning, utskrift etc.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">SKAPAR SÄKER ANSLUTNING</string>
<string name="critical_error">Kritiskt fel (kräver din uppmärksamhet)</string>
<string name="custom_dns_footer">Aktivera för att lägga till minst en DNS-server.</string>
- <string name="custom_dns_hint">Ange IP</string>
<string name="custom_tunnel_host_resolution_error">Det gick inte att lösa värdnamnet för den anpassade servern</string>
<string name="device_inactive_description">Du har tagit bort den här enheten. Du måste logga in igen för att återansluta.</string>
<string name="device_inactive_title">Enheten är inaktiv</string>
diff --git a/android/app/src/main/res/values-th/strings.xml b/android/app/src/main/res/values-th/strings.xml
index 4472175d61..c3e826cc5e 100644
--- a/android/app/src/main/res/values-th/strings.xml
+++ b/android/app/src/main/res/values-th/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">แสดงการแจ้งเตือน ในขณะที่เวลาบัญชีใกล้หมดอายุ</string>
<string name="account_time_notification_channel_name">การแจ้งเตือนเวลาบัญชี</string>
<string name="add_a_server">เพิ่มเซิร์ฟเวอร์</string>
- <string name="add_anyway">เพิ่มต่อไป</string>
<string name="add_time_to_account">ซื้อเครดิตบนเว็บไซต์ของเรา หรือแลกรับบัตรกำนัล</string>
<string name="all_applications">แอปพลิเคชันทั้งหมด</string>
<string name="allow_lan_footer">อนุญาตให้เข้าถึงอุปกรณ์อื่นๆ บนเครือข่ายเดียวกัน เพื่อแชร์ พิมพ์ ฯลฯ</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">กำลังสร้างการเชื่อมต่อที่ปลอดภัย</string>
<string name="critical_error">ข้อผิดพลาดร้ายแรง (คุณจำเป็นต้องตรวจสอบ)</string>
<string name="custom_dns_footer">เปิดเพื่อเพิ่มเซิร์ฟเวอร์ DNS อย่างน้อยหนึ่งรายการ</string>
- <string name="custom_dns_hint">ป้อน IP</string>
<string name="custom_tunnel_host_resolution_error">ไม่พบชื่อโฮสต์ของเซิร์ฟเวอร์แบบกำหนดเอง</string>
<string name="device_inactive_description">คุณได้ลบอุปกรณ์เครื่องนี้แล้ว หากต้องการเชื่อมต่ออีกครั้ง คุณจะต้องเข้าสู่ระบบใหม่อีกครั้ง</string>
<string name="device_inactive_title">อุปกรณ์ไม่ได้ใช้งาน</string>
diff --git a/android/app/src/main/res/values-tr/strings.xml b/android/app/src/main/res/values-tr/strings.xml
index 756baf5dbe..7acbbed720 100644
--- a/android/app/src/main/res/values-tr/strings.xml
+++ b/android/app/src/main/res/values-tr/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">Hesap süresinin dolmak üzere olduğunu bildiren hatırlatıcıları gösterir</string>
<string name="account_time_notification_channel_name">Hesap süresi hatırlatıcıları</string>
<string name="add_a_server">Sunucu ekle</string>
- <string name="add_anyway">Yine de ekle</string>
<string name="add_time_to_account">Web sitemizden kredi satın alın veya kupon kullanın.</string>
<string name="all_applications">Tüm uygulamalar</string>
<string name="allow_lan_footer">Paylaşım, yazdırma vb. için aynı ağdaki diğer cihazlara erişime izin verir.</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">GÜVENLİ BAĞLANTI OLUŞTURULUYOR</string>
<string name="critical_error">Kritik hata (Lütfen dikkatli olun)</string>
<string name="custom_dns_footer">En az bir DNS sunucusu eklemek için etkinleştirin.</string>
- <string name="custom_dns_hint">IP\'yi girin</string>
<string name="custom_tunnel_host_resolution_error">Özel sunucu ana bilgisayar adı çözülemiyor</string>
<string name="device_inactive_description">Bu cihazı kaldırdın. Tekrar bağlanmak için yeniden giriş yapmanız gerekecek.</string>
<string name="device_inactive_title">Cihaz etkin değil</string>
diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml
index fa1b258fb3..16394cc3fb 100644
--- a/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">在帐户时间即将到期时显示提醒</string>
<string name="account_time_notification_channel_name">帐户时间提醒</string>
<string name="add_a_server">添加服务器</string>
- <string name="add_anyway">仍然添加</string>
<string name="add_time_to_account">在我们的网站上购买额度或兑换优惠券。</string>
<string name="all_applications">所有应用程序</string>
<string name="allow_lan_footer">允许访问同一个网络上的其他设备以进行共享和打印等。</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">正在创建安全连接</string>
<string name="critical_error">严重错误(需要注意)</string>
<string name="custom_dns_footer">启用以添加至少一个 DNS 服务器。</string>
- <string name="custom_dns_hint">输入 IP</string>
<string name="custom_tunnel_host_resolution_error">无法解析自定义服务器的主机名</string>
<string name="device_inactive_description">您已移除此设备。要重新连接,您需要重新登录。</string>
<string name="device_inactive_title">设备处于非活动状态</string>
diff --git a/android/app/src/main/res/values-zh-rTW/strings.xml b/android/app/src/main/res/values-zh-rTW/strings.xml
index 68526a45ac..6e32faf157 100644
--- a/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -8,7 +8,6 @@
<string name="account_time_notification_channel_description">在帳戶時間即將到期時顯示提醒</string>
<string name="account_time_notification_channel_name">帳戶時間提醒</string>
<string name="add_a_server">新增伺服器</string>
- <string name="add_anyway">仍要新增</string>
<string name="add_time_to_account">在我們網站上購買點數或兌換憑證。</string>
<string name="all_applications">所有應用程式</string>
<string name="allow_lan_footer">允許存取同一網路上的其他裝置,以進行共用、列印等。</string>
@@ -43,7 +42,6 @@
<string name="creating_secure_connection">建立安全連線</string>
<string name="critical_error">嚴重錯誤 (需注意)</string>
<string name="custom_dns_footer">啟用以新增至少一個 DNS 伺服器。</string>
- <string name="custom_dns_hint">輸入 IP</string>
<string name="custom_tunnel_host_resolution_error">無法解析自訂伺服器的主機名稱</string>
<string name="device_inactive_description">您已移除此裝置。若要重新連線,您需要重新登入。</string>
<string name="device_inactive_title">裝置處於非活動狀態</string>
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index ddd2fb88e0..bf1608f2ee 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -11,7 +11,6 @@
<color name="white60">#99FFFFFF</color>
<color name="white40">#66FFFFFF</color>
<color name="white20">#33FFFFFF</color>
- <color name="white10">#1AFFFFFF</color>
<color name="green">#44AD4D</color>
<color name="green90">#E644AD4D</color>
<color name="green80">#CC44AD4D</color>
diff --git a/android/app/src/main/res/values/dimensions.xml b/android/app/src/main/res/values/dimensions.xml
index 4f35637a64..76fa24032d 100644
--- a/android/app/src/main/res/values/dimensions.xml
+++ b/android/app/src/main/res/values/dimensions.xml
@@ -23,8 +23,6 @@
<dimen name="cell_right_padding">16dp</dimen>
<dimen name="cell_inner_spacing">8dp</dimen>
<dimen name="cell_label_vertical_padding">14dp</dimen>
- <dimen name="cell_input_width">80dp</dimen>
- <dimen name="cell_input_height">34dp</dimen>
<dimen name="cell_footer_top_padding">6dp</dimen>
<dimen name="cell_footer_horizontal_padding">@dimen/side_margin</dimen>
<dimen name="app_version_warning_icon_size">28dp</dimen>
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 2e4c5f0275..bce6426629 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -147,11 +147,9 @@
<string name="enable">Enable</string>
<string name="enable_custom_dns">Use custom DNS server</string>
<string name="add_a_server">Add a server</string>
- <string name="custom_dns_hint">Enter IP</string>
<string name="custom_dns_footer">Enable to add at least one DNS server.</string>
<string name="confirm_local_dns">The local DNS server will not work unless you enable \"Local
Network Sharing\" under Preferences.</string>
- <string name="add_anyway">Add anyway</string>
<string name="exclude_applications">Excluded applications</string>
<string name="all_applications">All applications</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
@@ -203,4 +201,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>
diff --git a/android/buildSrc/src/main/kotlin/Dependencies.kt b/android/buildSrc/src/main/kotlin/Dependencies.kt
index 4a99bbc0bc..de1be2d9b7 100644
--- a/android/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/android/buildSrc/src/main/kotlin/Dependencies.kt
@@ -43,6 +43,8 @@ object Dependencies {
}
object Compose {
+ const val composeCollapsingToolbar =
+ "me.onebone:toolbar-compose:${Versions.Compose.composeCollapsingToolbar}"
const val constrainLayout =
"androidx.constraintlayout:constraintlayout-compose:${Versions.Compose.constrainLayout}"
const val foundation =
@@ -51,10 +53,12 @@ object Dependencies {
const val viewModelLifecycle =
"androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.Compose.viewModelLifecycle}"
const val material = "androidx.compose.material:material:${Versions.Compose.material}"
+ const val material3 = "androidx.compose.material3:material3:${Versions.Compose.material3}"
const val testManifest = "androidx.compose.ui:ui-test-manifest:${Versions.Compose.base}"
const val uiController =
"com.google.accompanist:accompanist-systemuicontroller:${Versions.Compose.uiController}"
const val ui = "androidx.compose.ui:ui:${Versions.Compose.base}"
+ const val uiUtil = "androidx.compose.ui:ui-util:${Versions.Compose.base}"
const val uiTooling = "androidx.compose.ui:ui-tooling:${Versions.Compose.base}"
const val uiToolingPreview =
"androidx.compose.ui:ui-tooling-preview:${Versions.Compose.base}"
diff --git a/android/buildSrc/src/main/kotlin/Versions.kt b/android/buildSrc/src/main/kotlin/Versions.kt
index 35bc19d1bb..b786f40d3c 100644
--- a/android/buildSrc/src/main/kotlin/Versions.kt
+++ b/android/buildSrc/src/main/kotlin/Versions.kt
@@ -39,9 +39,11 @@ object Versions {
object Compose {
const val base = "1.3.2"
+ const val composeCollapsingToolbar = "2.3.5"
const val constrainLayout = "1.0.1"
const val foundation = "1.3.1"
const val material = "1.3.1"
+ const val material3 = "1.0.1"
const val uiController = "0.28.0"
const val viewModelLifecycle = "2.5.1"
}
diff --git a/android/gradle/verification-metadata.xml b/android/gradle/verification-metadata.xml
index f684f65b10..e0bf7583a8 100644
--- a/android/gradle/verification-metadata.xml
+++ b/android/gradle/verification-metadata.xml
@@ -171,6 +171,14 @@
<sha256 value="078b4dcd5f09689281415d9ea0e09d2775d80f016041dacbcee22d54c43a5fa1" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.animation" name="animation" version="1.3.0">
+ <artifact name="animation-1.3.0.aar">
+ <sha256 value="3b640dc729a7686bedeb33061e38a10294f495bb0040aeaaa970ae1560119f41" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="animation-1.3.0.module">
+ <sha256 value="c4e548544f21d977ef7a80aeb8e388feb628c6bbf9099325559bbd8adb6992d0" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.animation" name="animation" version="1.3.2">
<artifact name="animation-1.3.2.aar">
<sha256 value="7c52b01c26c9ab8946d4cb6bbf819a54ac48038e1a3e741d30a6beac5457547f" origin="Generated by Gradle"/>
@@ -192,6 +200,14 @@
<sha256 value="6834b1b466930369a6cb9f76df6257eff28428e42ef42a26515319638cceee3d" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.animation" name="animation-core" version="1.3.0">
+ <artifact name="animation-core-1.3.0.aar">
+ <sha256 value="bfd9839872589c9d409e4eb943c9055d929c06f0673a3ed186b40645eba8e68f" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="animation-core-1.3.0.module">
+ <sha256 value="56c39013b7c99ccd0339badfef4452972d412dcd9862447ebf0788cde77eb698" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.animation" name="animation-core" version="1.3.2">
<artifact name="animation-core-1.3.2.aar">
<sha256 value="68cd10277608095d2f365b31945b640815f8e98fb53a17b84dc85fe2e52a70cf" origin="Generated by Gradle"/>
@@ -256,6 +272,14 @@
<sha256 value="2d89e99ae979853bd2359a7d5da16405479bce776d176168c2c7e8b431398d80" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.material3" name="material3" version="1.0.1">
+ <artifact name="material3-1.0.1.aar">
+ <sha256 value="7204378ecadec4089da57492fbdb4cb637758e4bc740f26fe6f2db4d8876af05" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="material3-1.0.1.module">
+ <sha256 value="993a826a5cb89f2932d7e0d9dc2dc071c7b6f684420f93b304bd07ddbbfb902b" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.runtime" name="runtime" version="1.1.1">
<artifact name="runtime-1.1.1.module">
<sha256 value="b7e9c3cc6034d099c9160ef49d2dc03eac66d3fe7ea0df7aa0abfa258368de63" origin="Generated by Gradle"/>
@@ -298,6 +322,11 @@
<sha256 value="ad2262144f81040a09bfdec039010dca2cb5026821e4b27403519303b59ae7bf" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.ui" name="ui-graphics" version="1.0.1">
+ <artifact name="ui-graphics-1.0.1.module">
+ <sha256 value="ad9ce40deec721b8988c43ab847d803d00bee88c67cfc838dee565691f35db95" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.ui" name="ui-graphics" version="1.1.1">
<artifact name="ui-graphics-1.1.1.module">
<sha256 value="170b229a8012c6724b4101058dc4c2cfe0f11e78a34aac4a4627cce8888f1a59" origin="Generated by Gradle"/>
@@ -345,6 +374,11 @@
<sha256 value="1b74b6a3275e3bd794a21b6403247455cc3c3978ade66dba214035d9fbabab2d" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.ui" name="ui-text" version="1.3.1">
+ <artifact name="ui-text-1.3.1.module">
+ <sha256 value="7808e6a9bb1ebd8b208b0ab3b206ddc5d4254ace63657d636b463b71983c54d3" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.ui" name="ui-text" version="1.3.2">
<artifact name="ui-text-1.3.2.aar">
<sha256 value="988fe4ea7f042ab83073a65a4448f2a0b8593b95b27d8708c41745af5aa10e00" origin="Generated by Gradle"/>
@@ -353,6 +387,14 @@
<sha256 value="9934a053e86e4847f7bc3c60be46187c9834d7815c27f12a369905170fc08d3e" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.ui" name="ui-tooling" version="1.3.0">
+ <artifact name="ui-tooling-1.3.0.aar">
+ <sha256 value="67d511e5b9c4251cd5d1c7129618738e24765446a367eed18e8d0abba4ef3413" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="ui-tooling-1.3.0.module">
+ <sha256 value="f367f5f746b14e8fb3d7371c5e012a1f922c27de33927a350985043cab3039e9" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.ui" name="ui-tooling" version="1.3.2">
<artifact name="ui-tooling-1.3.2.aar">
<sha256 value="e9fcb88bb28ee67b2ef8d8d995ffd94caafbeb619a6cd09e391857867a17ce5b" origin="Generated by Gradle"/>
@@ -361,6 +403,14 @@
<sha256 value="5922f4576b36c7fa8be7d9d1d229376b20d5ef25a3410185e8562baad43d1788" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="androidx.compose.ui" name="ui-tooling-data" version="1.3.0">
+ <artifact name="ui-tooling-data-1.3.0.aar">
+ <sha256 value="98a9c11622fa3abeaf881cfef2e0f4ab6d2ccdcb7ea9d7d27297b8597184e17f" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="ui-tooling-data-1.3.0.module">
+ <sha256 value="770acffe3600f98be1f18f967842bf86a7e0b20fa431d66d4ad7ba038192bc60" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="androidx.compose.ui" name="ui-tooling-data" version="1.3.2">
<artifact name="ui-tooling-data-1.3.2.aar">
<sha256 value="a60c7c89e0461660b657c4502894266c089c10c017eee4e21078a31e46d9b6d8" origin="Generated by Gradle"/>
@@ -390,11 +440,6 @@
<sha256 value="eecb5446872b5cd3caa1acce0e704780d1a3fa9feb2f06c6728ef9fb231b4cb5" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="androidx.compose.ui" name="ui-util" version="1.0.0">
- <artifact name="ui-util-1.0.0.module">
- <sha256 value="a09871728e5a9d050d2fdcb99a875ef2120dc6deea808f5a6d443dd887e081ca" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="androidx.compose.ui" name="ui-util" version="1.3.2">
<artifact name="ui-util-1.3.2.aar">
<sha256 value="b2f15225c1f59482445b1bc59a6dcb067cee62edaa611140aeaf27587bf41077" origin="Generated by Gradle"/>
@@ -2304,6 +2349,14 @@
<sha256 value="8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3" origin="Generated by Gradle"/>
</artifact>
</component>
+ <component group="me.onebone" name="toolbar-compose" version="2.3.5">
+ <artifact name="toolbar-compose-2.3.5.aar">
+ <sha256 value="5454801b0407039f58406626d387e8abb8cc8f888efe498644d89b8d4a33f1b1" origin="Generated by Gradle"/>
+ </artifact>
+ <artifact name="toolbar-compose-2.3.5.module">
+ <sha256 value="12cfcb37f4d8d5414eb405271681e9805a1085b5ad794356088f369ebb9c9b87" origin="Generated by Gradle"/>
+ </artifact>
+ </component>
<component group="net.bytebuddy" name="byte-buddy" version="1.12.18">
<artifact name="byte-buddy-1.12.18.jar">
<sha256 value="39200c13a72b6a3f4ec43c7b6d2fb78ecbeb25c29e986f4efa572636b39d750e" origin="Generated by Gradle"/>
@@ -2915,11 +2968,6 @@
<sha256 value="2aedcdc6b69b33bdf5cc235bcea88e7cf6601146bb6bcdffdb312bbacd7be261" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.6.21">
- <artifact name="kotlin-stdlib-jdk7-1.6.21.jar">
- <sha256 value="f1b0634dbb94172038463020bb2dd45ca26849f8ce29d625acb0f1569d11dbee" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.7.10">
<artifact name="kotlin-stdlib-jdk7-1.7.10.jar">
<sha256 value="54f61351b1936ad88f4e53059fe781e723eae51d78ed9e7422d8b403574ec682" origin="Generated by Gradle"/>
@@ -2940,11 +2988,6 @@
<sha256 value="1456d82d039ea30d8485b032901f52bbf07e7cdbe8bb1f8708ad32a8574c41ce" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.6.21">
- <artifact name="kotlin-stdlib-jdk8-1.6.21.jar">
- <sha256 value="dab45489b47736d59fce44b80676f1947a9b6bcab10fd60e878a83bd82a6954c" origin="Generated by Gradle"/>
- </artifact>
- </component>
<component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.7.10">
<artifact name="kotlin-stdlib-jdk8-1.7.10.jar">
<sha256 value="8aafdd60c94f454c92e5066d266a5ed53ecc63c78f623b3fd9db56fea4032873" origin="Generated by Gradle"/>
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index c235886bb0..74a69734fd 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -1605,6 +1605,9 @@ msgstr ""
msgid "Account time reminders"
msgstr ""
+msgid "Add DNS server"
+msgstr ""
+
msgid "Advanced"
msgstr ""
@@ -1641,6 +1644,9 @@ msgstr ""
msgid "Enable"
msgstr ""
+msgid "Enter…"
+msgstr ""
+
msgid "Excluded applications"
msgstr ""
@@ -1686,6 +1692,12 @@ msgstr ""
msgid "Privacy policy"
msgstr ""
+msgid "Remove"
+msgstr ""
+
+msgid "Reset to default"
+msgstr ""
+
msgid "Secured"
msgstr ""
@@ -1704,9 +1716,15 @@ msgstr ""
msgid "Split tunneling makes it possible to select which applications should not be routed through the VPN tunnel."
msgstr ""
+msgid "Submit"
+msgstr ""
+
msgid "The local DNS server will not work unless you enable \"Local Network Sharing\" under Preferences."
msgstr ""
+msgid "This address has already been entered."
+msgstr ""
+
msgid "This device is offline, no tunnels can be established"
msgstr ""
@@ -1722,6 +1740,9 @@ msgstr ""
msgid "Unsecured"
msgstr ""
+msgid "Update DNS server"
+msgstr ""
+
msgid "Update available, download to remain safe."
msgstr ""