summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2024-09-18 14:56:30 +0200
committerDavid Göransson <david.goransson@mullvad.net>2024-09-18 14:56:30 +0200
commitb50a8296449a267b4cad1c497070c48545294055 (patch)
tree49eab8b7f75a4727acaf778266f7a16ee18b239d /android/app/src
parent63c1abe65257b3357bbca696ad832968e6ecab44 (diff)
parent4906946ee7fd0bc279a7e023a07804c17c32971c (diff)
downloadmullvadvpn-b50a8296449a267b4cad1c497070c48545294055.tar.xz
mullvadvpn-b50a8296449a267b4cad1c497070c48545294055.zip
Merge branch 'add-feature-indicators-to-the-main-screen-droid-1108'
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt172
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt165
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/MullvadButton.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/SwitchLocationButton.kt155
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/NavigationComposeCell.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Chevron.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt107
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FeatureChip.kt93
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt153
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt198
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionInfoHeader.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt95
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt374
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt9
-rw-r--r--android/app/src/main/res/drawable/icon_reload.xml14
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt16
22 files changed, 903 insertions, 724 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
index c9cd30e439..1ae19c534a 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
@@ -14,10 +14,9 @@ import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.CONNECT_CARD_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION
import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
@@ -59,9 +58,8 @@ class ConnectScreenTest {
setContentWithTheme { ConnectScreen(state = ConnectUiState.INITIAL) }
// Assert
- onNodeWithTag(SCROLLABLE_COLUMN_TEST_TAG).assertExists()
- onNodeWithText("UNSECURED CONNECTION").assertExists()
- onNodeWithText("Secure my connection").assertExists()
+ onNodeWithText("DISCONNECTED").assertExists()
+ onNodeWithText("Connect").assertExists()
}
}
@@ -76,8 +74,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -89,45 +85,7 @@ class ConnectScreenTest {
// Assert
onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists()
- onNodeWithText("CREATING SECURE CONNECTION").assertExists()
- onNodeWithText("Switch location").assertExists()
- onNodeWithText("Cancel").assertExists()
- onNodeWithText("BLOCKING INTERNET").assertExists()
- }
- }
-
- @Test
- fun testConnectingStateQuantumSecured() {
- composeExtension.use {
- // Arrange
- val mockTunnelEndpoint: TunnelEndpoint = mockk(relaxed = true)
- every { mockTunnelEndpoint.quantumResistant } returns true
- setContentWithTheme {
- ConnectScreen(
- state =
- ConnectUiState(
- location = null,
- selectedRelayItemTitle = null,
- tunnelState =
- TunnelState.Connecting(
- endpoint = mockTunnelEndpoint,
- null,
- emptyList(),
- ),
- inAddress = null,
- outAddress = "",
- showLocation = false,
- deviceName = "",
- daysLeftUntilExpiry = null,
- inAppNotification = InAppNotification.TunnelStateBlocked,
- isPlayBuild = false,
- )
- )
- }
-
- // Assert
- onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists()
- onNodeWithText("CREATING QUANTUM SECURE CONNECTION").assertExists()
+ onNodeWithText("CONNECTING...").assertExists()
onNodeWithText("Switch location").assertExists()
onNodeWithText("Cancel").assertExists()
onNodeWithText("BLOCKING INTERNET").assertExists()
@@ -147,40 +105,6 @@ class ConnectScreenTest {
selectedRelayItemTitle = null,
tunnelState =
TunnelState.Connected(mockTunnelEndpoint, null, emptyList()),
- inAddress = null,
- outAddress = "",
- showLocation = false,
- deviceName = "",
- daysLeftUntilExpiry = null,
- inAppNotification = null,
- isPlayBuild = false,
- )
- )
- }
-
- // Assert
- onNodeWithText("SECURE CONNECTION").assertExists()
- onNodeWithText("Switch location").assertExists()
- onNodeWithText("Disconnect").assertExists()
- }
- }
-
- @Test
- fun testConnectedStateQuantumSecured() {
- composeExtension.use {
- // Arrange
- val mockTunnelEndpoint: TunnelEndpoint = mockk(relaxed = true)
- every { mockTunnelEndpoint.quantumResistant } returns true
- setContentWithTheme {
- ConnectScreen(
- state =
- ConnectUiState(
- location = null,
- selectedRelayItemTitle = null,
- tunnelState =
- TunnelState.Connected(mockTunnelEndpoint, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -191,7 +115,7 @@ class ConnectScreenTest {
}
// Assert
- onNodeWithText("QUANTUM SECURE CONNECTION").assertExists()
+ onNodeWithText("CONNECTED").assertExists()
onNodeWithText("Switch location").assertExists()
onNodeWithText("Disconnect").assertExists()
}
@@ -209,8 +133,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = mockLocationName,
tunnelState = TunnelState.Disconnecting(ActionAfterDisconnect.Nothing),
- inAddress = null,
- outAddress = "",
showLocation = true,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -221,7 +143,7 @@ class ConnectScreenTest {
}
// Assert
- onNodeWithText("UNSECURED CONNECTION").assertExists()
+ onNodeWithText("DISCONNECTED").assertExists()
onNodeWithText(mockLocationName).assertExists()
onNodeWithText("Disconnect").assertExists()
}
@@ -239,8 +161,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = mockLocationName,
tunnelState = TunnelState.Disconnected(),
- inAddress = null,
- outAddress = "",
showLocation = true,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -251,9 +171,9 @@ class ConnectScreenTest {
}
// Assert
- onNodeWithText("UNSECURED CONNECTION").assertExists()
+ onNodeWithText("DISCONNECTED").assertExists()
onNodeWithText(mockLocationName).assertExists()
- onNodeWithText("Secure my connection").assertExists()
+ onNodeWithText("Connect").assertExists()
}
}
@@ -272,8 +192,6 @@ class ConnectScreenTest {
TunnelState.Error(
ErrorState(ErrorStateCause.StartTunnelError, true)
),
- inAddress = null,
- outAddress = "",
showLocation = true,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -309,8 +227,6 @@ class ConnectScreenTest {
TunnelState.Error(
ErrorState(ErrorStateCause.StartTunnelError, false)
),
- inAddress = null,
- outAddress = "",
showLocation = true,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -344,8 +260,6 @@ class ConnectScreenTest {
selectedRelayItemTitle = null,
tunnelState =
TunnelState.Disconnecting(ActionAfterDisconnect.Reconnect),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -357,7 +271,7 @@ class ConnectScreenTest {
// Assert
onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR).assertExists()
- onNodeWithText("CREATING SECURE CONNECTION").assertExists()
+ onNodeWithText("CONNECTING...").assertExists()
onNodeWithText("Switch location").assertExists()
onNodeWithText("Disconnect").assertExists()
onNodeWithText("BLOCKING INTERNET").assertExists()
@@ -376,8 +290,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = mockLocationName,
tunnelState = TunnelState.Disconnecting(ActionAfterDisconnect.Block),
- inAddress = null,
- outAddress = "",
showLocation = true,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -388,7 +300,7 @@ class ConnectScreenTest {
}
// Assert
- onNodeWithText("SECURE CONNECTION").assertExists()
+ onNodeWithText("CONNECTED").assertExists()
onNodeWithText(mockLocationName).assertExists()
onNodeWithText("Disconnect").assertExists()
onNodeWithText("BLOCKING INTERNET").assertExists()
@@ -408,8 +320,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = mockLocationName,
tunnelState = TunnelState.Disconnected(),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -442,8 +352,6 @@ class ConnectScreenTest {
selectedRelayItemTitle = null,
tunnelState =
TunnelState.Connected(mockTunnelEndpoint, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -476,8 +384,6 @@ class ConnectScreenTest {
selectedRelayItemTitle = null,
tunnelState =
TunnelState.Connected(mockTunnelEndpoint, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -508,8 +414,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Disconnected(),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -540,8 +444,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -561,18 +463,31 @@ class ConnectScreenTest {
}
@Test
- fun showLocationInfo() {
+ fun showConnectionDetails() {
composeExtension.use {
// Arrange
val mockLocation: GeoIpLocation = mockk(relaxed = true)
val mockTunnelEndpoint: TunnelEndpoint = mockk(relaxed = true)
val mockHostName = "Host-Name"
- val mockPort = 99
- val mockHost = "Host"
- val mockProtocol = TransportProtocol.Udp
- val mockInAddress = Triple(mockHost, mockPort, mockProtocol)
- val mockOutAddress = "HostAddressV4 / HostAddressV4"
+ val inHost = "Host"
+ val inPort = 99
+ val inProtocol = TransportProtocol.Udp
every { mockLocation.hostname } returns mockHostName
+
+ // In
+ every { mockTunnelEndpoint.obfuscation } returns null
+ every { mockTunnelEndpoint.endpoint.address.address.hostAddress } returns inHost
+ every { mockTunnelEndpoint.endpoint.address.port } returns inPort
+ every { mockTunnelEndpoint.endpoint.protocol } returns inProtocol
+
+ // Out Ipv4
+ val outIpv4 = "ipv4address"
+ every { mockLocation.ipv4?.hostAddress } returns outIpv4
+
+ // Out Ipv6
+ val outIpv6 = "ipv6address"
+ every { mockLocation.ipv6?.hostAddress } returns outIpv6
+
setContentWithTheme {
ConnectScreen(
state =
@@ -580,9 +495,11 @@ class ConnectScreenTest {
location = mockLocation,
selectedRelayItemTitle = null,
tunnelState =
- TunnelState.Connected(mockTunnelEndpoint, null, emptyList()),
- inAddress = mockInAddress,
- outAddress = mockOutAddress,
+ TunnelState.Connected(
+ mockTunnelEndpoint,
+ mockLocation,
+ emptyList(),
+ ),
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -593,13 +510,18 @@ class ConnectScreenTest {
}
// Act
- onNodeWithTag(LOCATION_INFO_TEST_TAG).performClick()
+ onNodeWithTag(CONNECT_CARD_HEADER_TEST_TAG).performClick()
// Assert
onNodeWithText(mockHostName).assertExists()
- onNodeWithText("WireGuard").assertExists()
- onNodeWithText("In $mockHost:$mockPort UDP").assertExists()
- onNodeWithText("Out $mockOutAddress").assertExists()
+ onNodeWithText("In").assertExists()
+ onNodeWithText("$inHost:$inPort UDP").assertExists()
+
+ onNodeWithText("Out Ipv4").assertExists()
+ onNodeWithText(outIpv4).assertExists()
+
+ onNodeWithText("Out Ipv6").assertExists()
+ onNodeWithText(outIpv6).assertExists()
}
}
@@ -615,8 +537,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -647,8 +567,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -678,8 +596,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
@@ -711,8 +627,6 @@ class ConnectScreenTest {
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Connecting(null, null, emptyList()),
- inAddress = null,
- outAddress = "",
showLocation = false,
deviceName = "",
daysLeftUntilExpiry = null,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
index 1bb9957114..47690247dc 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/ConnectionButton.kt
@@ -1,43 +1,16 @@
package net.mullvad.mullvadvpn.compose.button
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.CornerSize
-import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.FilledIconButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButtonDefaults
-import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-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 androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import androidx.constraintlayout.compose.ConstraintLayout
-import androidx.constraintlayout.compose.Dimension
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.preview.TunnelStatePreviewParameterProvider
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisconnectButton
@Composable
@Preview
@@ -48,7 +21,6 @@ private fun PreviewConnectionButton(
ConnectionButton(
state = tunnelState,
disconnectClick = {},
- reconnectClick = {},
cancelClick = {},
connectClick = {},
)
@@ -58,18 +30,17 @@ private fun PreviewConnectionButton(
@Composable
fun ConnectionButton(
modifier: Modifier = Modifier,
- reconnectButtonTestTag: String = "",
state: TunnelState,
- disconnectClick: () -> Unit,
- reconnectClick: () -> Unit,
- cancelClick: () -> Unit,
- connectClick: () -> Unit,
+ disconnectClick: () -> Unit = {},
+ cancelClick: () -> Unit = {},
+ connectClick: () -> Unit = {},
) {
+
val containerColor =
if (state is TunnelState.Disconnected) {
MaterialTheme.colorScheme.tertiary
} else {
- MaterialTheme.colorScheme.error.copy(alpha = AlphaDisconnectButton)
+ MaterialTheme.colorScheme.error
}
val contentColor =
@@ -97,7 +68,7 @@ fun ConnectionButton(
}
)
- val onMainClick =
+ val onClick =
when (state) {
is TunnelState.Disconnected -> connectClick
is TunnelState.Connecting -> cancelClick
@@ -108,126 +79,18 @@ fun ConnectionButton(
cancelClick
}
}
+
else -> disconnectClick
}
- ConnectionButton(
+ PrimaryButton(
+ onClick = onClick,
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ ),
modifier = modifier,
text = buttonText,
- containerColor = containerColor,
- contentColor = contentColor,
- mainClick = onMainClick,
- reconnectClick = reconnectClick,
- reconnectButtonTestTag = reconnectButtonTestTag,
- isReconnectButtonEnabled = (state is TunnelState.Disconnected).not(),
)
}
-
-@Composable
-@Suppress("LongMethod")
-private fun ConnectionButton(
- text: String,
- mainClick: () -> Unit,
- reconnectClick: () -> Unit,
- isReconnectButtonEnabled: Boolean,
- containerColor: Color,
- contentColor: Color,
- modifier: Modifier = Modifier,
- reconnectButtonTestTag: String = "",
-) {
- ConstraintLayout(
- modifier = modifier.padding(vertical = Dimens.connectButtonExtraPadding).fillMaxWidth()
- ) {
- // initial height set at 0.dp
- var componentHeight by remember { mutableStateOf(0.dp) }
-
- // get local density from composable
- val density = LocalDensity.current
-
- val (connectionButton, reconnectButton) = createRefs()
- CompositionLocalProvider(
- LocalMinimumInteractiveComponentSize provides
- Dimens.reconnectButtonMinInteractiveComponentSize
- ) {
- val dividerSize = Dimens.listItemDivider
-
- Button(
- onClick = mainClick,
- shape =
- if (isReconnectButtonEnabled) {
- MaterialTheme.shapes.small.copy(
- topEnd = CornerSize(percent = 0),
- bottomEnd = CornerSize(percent = 0),
- )
- } else {
- MaterialTheme.shapes.small
- },
- colors =
- ButtonDefaults.buttonColors(
- containerColor = containerColor,
- contentColor = contentColor,
- ),
- modifier =
- Modifier.constrainAs(connectionButton) {
- start.linkTo(parent.start)
- if (isReconnectButtonEnabled) {
- end.linkTo(reconnectButton.start)
- } else {
- end.linkTo(parent.end)
- }
- width = Dimension.fillToConstraints
- height = Dimension.wrapContent
- }
- .onGloballyPositioned {
- componentHeight = with(density) { it.size.height.toDp() }
- },
- ) {
- // Offset to compensate for the reconnect button.
- Text(
- text = text,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier =
- if (isReconnectButtonEnabled) {
- Modifier.padding(start = componentHeight + Dimens.listItemDivider)
- } else {
- Modifier
- },
- )
- }
-
- if (isReconnectButtonEnabled) {
- FilledIconButton(
- shape =
- MaterialTheme.shapes.small.copy(
- topStart = CornerSize(percent = 0),
- bottomStart = CornerSize(percent = 0),
- ),
- colors =
- IconButtonDefaults.filledIconButtonColors(
- containerColor = containerColor,
- contentColor = contentColor,
- ),
- onClick = reconnectClick,
- modifier =
- Modifier.testTag(reconnectButtonTestTag)
- .constrainAs(reconnectButton) {
- start.linkTo(connectionButton.end, margin = dividerSize)
- top.linkTo(connectionButton.top)
- bottom.linkTo(connectionButton.bottom)
- end.linkTo(parent.end)
- height = Dimension.fillToConstraints
- }
- .aspectRatio(1f, true),
- ) {
- Icon(
- painter = painterResource(id = R.drawable.icon_reload),
- contentDescription = null,
- )
- }
- }
- }
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/MullvadButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/MullvadButton.kt
index 6aa17cefed..af8e1a30d3 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/MullvadButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/MullvadButton.kt
@@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.Alpha20
@@ -157,7 +156,7 @@ private fun BaseButton(
enabled = isEnabled,
contentPadding =
if (hasIcon) {
- PaddingValues(horizontal = 0.dp, vertical = Dimens.buttonVerticalPadding)
+ PaddingValues(vertical = Dimens.buttonVerticalPadding)
} else {
ButtonDefaults.ContentPadding
},
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/SwitchLocationButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/SwitchLocationButton.kt
index 531cf685c4..69bff821c9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/SwitchLocationButton.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/SwitchLocationButton.kt
@@ -1,52 +1,163 @@
package net.mullvad.mullvadvpn.compose.button
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButtonDefaults
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+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.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.component.SpacedColumn
import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.color.Alpha20
+import net.mullvad.mullvadvpn.lib.theme.Dimens
@Preview
@Composable
-private fun PreviewSwitchLocationButton() {
+private fun PreviewConnectionButton() {
AppTheme {
SpacedColumn {
- SwitchLocationButton(onClick = {}, text = "Switch Location", showChevron = false)
- SwitchLocationButton(onClick = {}, text = "Switch Location", showChevron = true)
+ SwitchLocationButton(
+ text = "Switch Location",
+ onSwitchLocation = {},
+ reconnectClick = {},
+ isReconnectButtonEnabled = true,
+ )
+ SwitchLocationButton(
+ text = "Switch Location",
+ onSwitchLocation = {},
+ reconnectClick = {},
+ isReconnectButtonEnabled = false,
+ )
}
}
}
@Composable
+@Suppress("LongMethod")
fun SwitchLocationButton(
- modifier: Modifier = Modifier,
text: String,
- showChevron: Boolean,
- onClick: () -> Unit,
+ onSwitchLocation: () -> Unit,
+ reconnectClick: () -> Unit,
+ isReconnectButtonEnabled: Boolean,
+ modifier: Modifier = Modifier,
+ reconnectButtonTestTag: String = "",
) {
- PrimaryButton(
- onClick = onClick,
- colors =
- ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = Alpha20),
- contentColor = MaterialTheme.colorScheme.onPrimary,
- ),
- modifier = modifier,
- text = text,
- trailingIcon =
- if (showChevron) {
- {
+ ConstraintLayout(
+ modifier = modifier.padding(vertical = Dimens.connectButtonExtraPadding).fillMaxWidth()
+ ) {
+ // initial height set at 0.dp
+ var componentHeight by remember { mutableStateOf(0.dp) }
+
+ // get local density from composable
+ val density = LocalDensity.current
+
+ val (connectionButton, reconnectButton) = createRefs()
+ CompositionLocalProvider(
+ LocalMinimumInteractiveComponentSize provides
+ Dimens.reconnectButtonMinInteractiveComponentSize
+ ) {
+ val dividerSize = Dimens.listItemDivider
+
+ Button(
+ onClick = onSwitchLocation,
+ shape =
+ if (isReconnectButtonEnabled) {
+ MaterialTheme.shapes.small.copy(
+ topEnd = CornerSize(percent = 0),
+ bottomEnd = CornerSize(percent = 0),
+ )
+ } else {
+ MaterialTheme.shapes.small
+ },
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ modifier =
+ Modifier.constrainAs(connectionButton) {
+ start.linkTo(parent.start)
+ if (isReconnectButtonEnabled) {
+ end.linkTo(reconnectButton.start)
+ } else {
+ end.linkTo(parent.end)
+ }
+ width = Dimension.fillToConstraints
+ height = Dimension.wrapContent
+ }
+ .onGloballyPositioned {
+ componentHeight = with(density) { it.size.height.toDp() }
+ },
+ ) {
+ // Offset to compensate for the reconnect button.
+ Text(
+ text = text,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier =
+ if (isReconnectButtonEnabled) {
+ Modifier.padding(start = componentHeight + Dimens.listItemDivider)
+ } else {
+ Modifier
+ },
+ )
+ }
+
+ if (isReconnectButtonEnabled) {
+ FilledIconButton(
+ shape =
+ MaterialTheme.shapes.small.copy(
+ topStart = CornerSize(percent = 0),
+ bottomStart = CornerSize(percent = 0),
+ ),
+ colors =
+ IconButtonDefaults.filledIconButtonColors(
+ containerColor = MaterialTheme.colorScheme.primary,
+ contentColor = MaterialTheme.colorScheme.onPrimary,
+ ),
+ onClick = reconnectClick,
+ modifier =
+ Modifier.testTag(reconnectButtonTestTag)
+ .constrainAs(reconnectButton) {
+ start.linkTo(connectionButton.end, margin = dividerSize)
+ top.linkTo(connectionButton.top)
+ bottom.linkTo(connectionButton.bottom)
+ end.linkTo(parent.end)
+ height = Dimension.fillToConstraints
+ }
+ .defaultMinSize(minWidth = Dimens.switchLocationRetryMinWidth),
+ ) {
Icon(
- painter = painterResource(id = R.drawable.icon_chevron),
+ painter = painterResource(id = R.drawable.icon_reload),
contentDescription = null,
)
}
- } else null,
- )
+ }
+ }
+ }
}
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
index a4233cc3b1..a5d2ee37f9 100644
--- 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
@@ -12,6 +12,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
@@ -114,6 +115,7 @@ internal fun NavigationTitleView(
@Composable
internal fun DefaultNavigationView(chevronContentDescription: String, tint: Color) {
Icon(
+ modifier = Modifier.rotate(-90f),
painter = painterResource(id = R.drawable.icon_chevron),
contentDescription = chevronContentDescription,
tint = tint,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Chevron.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Chevron.kt
index 15758be10e..ff20dd0a67 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Chevron.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Chevron.kt
@@ -3,20 +3,32 @@ package net.mullvad.mullvadvpn.compose.component
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
@Composable
+@Preview
+private fun PreviewChevron() {
+ Column {
+ Chevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = false)
+ Chevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = true)
+ }
+}
+
+@Composable
fun Chevron(modifier: Modifier = Modifier, color: Color, isExpanded: Boolean) {
- val degree = remember(isExpanded) { if (isExpanded) 270f else 90f }
+ val degree = remember(isExpanded) { if (isExpanded) 180f else 0f }
val animatedRotation =
animateFloatAsState(
targetValue = degree,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
index c736b6709e..535dcf2bb8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ConnectionStatusText.kt
@@ -29,89 +29,48 @@ private fun PreviewConnectionStatusText(
}
@Composable
-fun ConnectionStatusText(state: TunnelState, modifier: Modifier = Modifier) {
- when (state) {
- is TunnelState.Disconnecting -> {
- when (state.actionAfterDisconnect) {
- ActionAfterDisconnect.Nothing -> DisconnectedText(modifier = modifier)
- ActionAfterDisconnect.Block ->
- ConnectedText(isQuantumResistant = false, modifier = modifier)
- ActionAfterDisconnect.Reconnect ->
- ConnectingText(isQuantumResistant = false, modifier = modifier)
- }
- }
- is TunnelState.Disconnected -> DisconnectedText(modifier = modifier)
- is TunnelState.Connecting ->
- ConnectingText(
- isQuantumResistant = state.endpoint?.quantumResistant == true,
- modifier = modifier,
- )
- is TunnelState.Connected ->
- ConnectedText(isQuantumResistant = state.endpoint.quantumResistant, modifier = modifier)
- is TunnelState.Error ->
- ErrorText(isBlocking = state.errorState.isBlocking, modifier = modifier)
- }
-}
-
-@Composable
-private fun DisconnectedText(modifier: Modifier) {
+fun ConnectionStatusText(state: TunnelState) {
Text(
- text = textResource(id = R.string.unsecured_connection),
- color = MaterialTheme.colorScheme.error,
+ text = state.text(),
+ color = state.textColor(),
style = MaterialTheme.typography.connectionStatus,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- modifier = modifier,
)
}
@Composable
-private fun ConnectingText(isQuantumResistant: Boolean, modifier: Modifier) {
- Text(
- text =
- textResource(
- id =
- if (isQuantumResistant) R.string.quantum_creating_secure_connection
- else R.string.creating_secure_connection
- ),
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.connectionStatus,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = modifier,
- )
-}
-
-@Composable
-private fun ConnectedText(isQuantumResistant: Boolean, modifier: Modifier) {
- Text(
- text =
+private fun TunnelState.text() =
+ when (this) {
+ is TunnelState.Connected -> textResource(id = R.string.connected)
+ is TunnelState.Connecting -> textResource(id = R.string.connecting)
+ is TunnelState.Disconnected -> textResource(id = R.string.disconnected)
+ is TunnelState.Disconnecting ->
+ when (actionAfterDisconnect) {
+ ActionAfterDisconnect.Nothing -> textResource(id = R.string.disconnected)
+ ActionAfterDisconnect.Block -> textResource(id = R.string.connected)
+ ActionAfterDisconnect.Reconnect -> textResource(id = R.string.connecting)
+ }
+ is TunnelState.Error ->
textResource(
id =
- if (isQuantumResistant) R.string.quantum_secure_connection
- else R.string.secure_connection
- ),
- color = MaterialTheme.colorScheme.tertiary,
- style = MaterialTheme.typography.connectionStatus,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = modifier,
- )
-}
+ if (errorState.isBlocking) R.string.blocked_connection else R.string.error_state
+ )
+ }.uppercase()
@Composable
-private fun ErrorText(isBlocking: Boolean, modifier: Modifier) {
- Text(
- text =
- textResource(
- id = if (isBlocking) R.string.blocked_connection else R.string.error_state
- ),
- color =
- if (isBlocking) MaterialTheme.colorScheme.onSurface
- else MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.connectionStatus,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = modifier,
- )
-}
+private fun TunnelState.textColor() =
+ when (this) {
+ is TunnelState.Connected -> MaterialTheme.colorScheme.tertiary
+ is TunnelState.Connecting -> MaterialTheme.colorScheme.onSurface
+ is TunnelState.Disconnected -> MaterialTheme.colorScheme.error
+ is TunnelState.Disconnecting ->
+ when (actionAfterDisconnect) {
+ ActionAfterDisconnect.Nothing -> MaterialTheme.colorScheme.error
+ ActionAfterDisconnect.Block -> MaterialTheme.colorScheme.tertiary
+ ActionAfterDisconnect.Reconnect -> MaterialTheme.colorScheme.onSurface
+ }
+ is TunnelState.Error ->
+ if (errorState.isBlocking) MaterialTheme.colorScheme.onSurface
+ else MaterialTheme.colorScheme.error
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FeatureChip.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FeatureChip.kt
new file mode 100644
index 0000000000..65a63dd089
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/FeatureChip.kt
@@ -0,0 +1,93 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import net.mullvad.mullvadvpn.lib.theme.AppTheme
+import net.mullvad.mullvadvpn.lib.theme.shape.chipShape
+
+@Preview
+@Composable
+private fun PreviewMullvadFeatureChip() {
+ AppTheme { Row { MullvadFeatureChip(text = "DAITA") } }
+}
+
+@Composable
+fun MullvadFeatureChip(
+ containerColor: Color = MaterialTheme.colorScheme.surfaceContainerLowest,
+ borderColor: Color = MaterialTheme.colorScheme.primary,
+ labelColor: Color = MaterialTheme.colorScheme.onPrimary,
+ iconColor: Color = MaterialTheme.colorScheme.onPrimary,
+ text: String,
+) {
+ FilterChip(
+ shape = MaterialTheme.shapes.chipShape,
+ colors =
+ FilterChipDefaults.filterChipColors(
+ disabledContainerColor = containerColor,
+ disabledLabelColor = labelColor,
+ labelColor = labelColor,
+ iconColor = iconColor,
+ ),
+ border =
+ FilterChipDefaults.filterChipBorder(
+ borderColor = borderColor,
+ enabled = true,
+ selected = false,
+ ),
+ selected = false,
+ onClick = {},
+ enabled = false,
+ label = {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ )
+}
+
+@Composable
+fun MullvadMoreChip(
+ onClick: () -> Unit,
+ containerColor: Color = MaterialTheme.colorScheme.background,
+ borderColor: Color = Color.Transparent,
+ labelColor: Color = MaterialTheme.colorScheme.onPrimary,
+ iconColor: Color = MaterialTheme.colorScheme.onPrimary,
+ text: String,
+) {
+ FilterChip(
+ onClick = onClick,
+ shape = MaterialTheme.shapes.chipShape,
+ colors =
+ FilterChipDefaults.filterChipColors(
+ containerColor = containerColor,
+ labelColor = labelColor,
+ iconColor = iconColor,
+ ),
+ border =
+ FilterChipDefaults.filterChipBorder(
+ borderColor = borderColor,
+ enabled = true,
+ selected = false,
+ ),
+ selected = false,
+ label = {
+ Text(
+ text = text,
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt
deleted file mode 100644
index 3fbd40c537..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-package net.mullvad.mullvadvpn.compose.component
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.tooling.preview.Preview
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_CONNECTION_OUT_TEST_TAG
-import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
-import net.mullvad.mullvadvpn.lib.model.TransportProtocol
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-
-@Preview
-@Composable
-private fun PreviewLocationInfo() {
- AppTheme {
- LocationInfo(
- onToggleTunnelInfo = {},
- isVisible = true,
- isExpanded = true,
- location = null,
- isUsingDaita = false,
- inAddress = null,
- outAddress = "",
- )
- }
-}
-
-@Composable
-fun LocationInfo(
- modifier: Modifier = Modifier,
- colorExpanded: Color = MaterialTheme.colorScheme.onSurface,
- colorCollapsed: Color = MaterialTheme.colorScheme.onSurfaceVariant,
- onToggleTunnelInfo: () -> Unit,
- isVisible: Boolean,
- isExpanded: Boolean,
- location: GeoIpLocation?,
- isUsingDaita: Boolean,
- inAddress: Triple<String, Int, TransportProtocol>?,
- outAddress: String,
-) {
- Column(
- modifier =
- if (isVisible) {
- Modifier.clickable { onToggleTunnelInfo() }.alpha(AlphaVisible)
- } else {
- Modifier.alpha(AlphaInvisible)
- }
- .then(modifier)
- ) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- RelayHostname(
- hostname = location?.hostname,
- isUsingDaita = isUsingDaita,
- isExpanded = isExpanded,
- colorExpanded = colorExpanded,
- colorCollapsed = colorCollapsed,
- )
- Chevron(
- isExpanded = isExpanded,
- color =
- if (isExpanded) {
- colorExpanded
- } else {
- colorCollapsed
- },
- modifier = Modifier.padding(horizontal = Dimens.chevronMargin),
- )
- }
- Text(
- text =
- if (isExpanded) {
- stringResource(id = R.string.wireguard)
- } else {
- ""
- },
- color = colorExpanded,
- style = MaterialTheme.typography.labelMedium,
- )
- val textInAddress =
- inAddress?.let {
- val protocol =
- when (inAddress.third) {
- TransportProtocol.Tcp -> stringResource(id = R.string.tcp)
- TransportProtocol.Udp -> stringResource(id = R.string.udp)
- }
- "${inAddress.first}:${inAddress.second} $protocol"
- } ?: ""
- Text(
- text = "${stringResource(id = R.string.in_address)} $textInAddress",
- color = colorExpanded,
- style = MaterialTheme.typography.labelMedium,
- modifier = Modifier.alpha(if (isExpanded) AlphaVisible else AlphaInvisible),
- )
- Text(
- text = "${stringResource(id = R.string.out_address)} $outAddress",
- color = colorExpanded,
- style = MaterialTheme.typography.labelMedium,
- modifier =
- Modifier.testTag(LOCATION_INFO_CONNECTION_OUT_TEST_TAG)
- .alpha(
- if (isExpanded && outAddress.isNotEmpty()) AlphaVisible else AlphaInvisible
- ),
- )
- }
-}
-
-@Composable
-private fun RelayHostname(
- hostname: String?,
- isUsingDaita: Boolean,
- isExpanded: Boolean,
- colorExpanded: Color,
- colorCollapsed: Color,
-) {
- val hostnameTitle =
- when {
- hostname != null && isUsingDaita -> {
- stringResource(
- id = R.string.connected_using_daita,
- hostname,
- stringResource(id = R.string.daita),
- )
- }
- hostname != null -> hostname
- else -> ""
- }
-
- Text(
- text = hostnameTitle,
- color =
- if (isExpanded) {
- colorExpanded
- } else {
- colorCollapsed
- },
- style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt
new file mode 100644
index 0000000000..b6ab3f2166
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt
@@ -0,0 +1,198 @@
+package net.mullvad.mullvadvpn.compose.component.connectioninfo
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.Dimension
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_CONNECTION_OUT_TEST_TAG
+import net.mullvad.mullvadvpn.lib.model.TransportProtocol
+import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
+import net.mullvad.mullvadvpn.lib.model.TunnelState
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+@Composable
+fun ConnectionDetailPanel(tunnelState: TunnelState.Connected) {
+
+ ConnectionInfoHeader(
+ stringResource(R.string.connect_panel_connection_details),
+ Modifier.fillMaxWidth().padding(bottom = Dimens.smallPadding),
+ )
+
+ ConnectionDetails(
+ tunnelState.endpoint.toInAddress(),
+ tunnelState.location()?.ipv4?.hostAddress,
+ tunnelState.location()?.ipv6?.hostAddress,
+ modifier = Modifier.padding(bottom = Dimens.smallPadding),
+ )
+}
+
+@Suppress("LongMethod")
+@Composable
+fun ConnectionDetails(
+ inIPV4: String,
+ outIPV4: String?,
+ outIPV6: String?,
+ modifier: Modifier = Modifier,
+) {
+ ConstraintLayout(modifier = modifier.fillMaxWidth()) {
+ val (inAddrHeader, inAddr, outAddrV4Header, outAddrV4, outAddrV6Header, outAddrV6) =
+ createRefs()
+ val headerBarrier = createEndBarrier(inAddrHeader, outAddrV4Header, outAddrV6Header)
+
+ val inAddrBarrier = createBottomBarrier(inAddrHeader, inAddr)
+ val outAddrV4Barrier = createBottomBarrier(inAddrHeader, inAddr, outAddrV4Header, outAddrV4)
+
+ val outAddrV6Barrier =
+ createBottomBarrier(
+ inAddrHeader,
+ inAddr,
+ outAddrV4Header,
+ outAddrV4,
+ outAddrV6Header,
+ outAddrV6,
+ )
+
+ Text(
+ text = stringResource(R.string.connection_details_in),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier =
+ Modifier.padding(end = Dimens.smallPadding).constrainAs(inAddrHeader) {
+ start.linkTo(parent.start)
+ top.linkTo(parent.top)
+ bottom.linkTo(inAddrBarrier)
+ height = Dimension.wrapContent
+ width = Dimension.wrapContent
+ },
+ )
+ Text(
+ text = inIPV4,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier =
+ Modifier.constrainAs(inAddr) {
+ start.linkTo(headerBarrier)
+ end.linkTo(parent.end)
+ top.linkTo(parent.top)
+ bottom.linkTo(inAddrBarrier)
+ height = Dimension.wrapContent
+ width = Dimension.fillToConstraints
+ },
+ )
+
+ if (outIPV4 != null) {
+ Text(
+ text = stringResource(R.string.connection_details_out_ipv4),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier =
+ Modifier.padding(end = Dimens.smallPadding).constrainAs(outAddrV4Header) {
+ start.linkTo(parent.start)
+ top.linkTo(inAddrBarrier)
+ bottom.linkTo(outAddrV4Barrier)
+ height = Dimension.wrapContent
+ width = Dimension.wrapContent
+ },
+ )
+ Box(
+ modifier =
+ Modifier.constrainAs(outAddrV4) {
+ start.linkTo(headerBarrier)
+ end.linkTo(parent.end)
+ top.linkTo(inAddrBarrier)
+ bottom.linkTo(outAddrV4Barrier)
+ height = Dimension.wrapContent
+ width = Dimension.fillToConstraints
+ }
+ ) {
+ SelectionContainer {
+ Text(
+ modifier = Modifier.testTag(LOCATION_INFO_CONNECTION_OUT_TEST_TAG),
+ text = outIPV4,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+
+ if (outIPV6 != null) {
+ Text(
+ text = stringResource(R.string.connection_details_out_ipv6),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ modifier =
+ Modifier.padding(end = Dimens.smallPadding).constrainAs(outAddrV6Header) {
+ start.linkTo(parent.start)
+ top.linkTo(outAddrV4Barrier)
+ bottom.linkTo(outAddrV6Barrier)
+ height = Dimension.wrapContent
+ width = Dimension.wrapContent
+ },
+ )
+ Box(
+ modifier =
+ Modifier.constrainAs(outAddrV6) {
+ start.linkTo(headerBarrier)
+ end.linkTo(parent.end)
+ top.linkTo(outAddrV4Barrier)
+ bottom.linkTo(outAddrV6Barrier)
+ height = Dimension.wrapContent
+ width = Dimension.fillToConstraints
+ }
+ ) {
+ SelectionContainer {
+ Text(
+ text = outIPV6,
+ color = MaterialTheme.colorScheme.onPrimary,
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TunnelEndpoint.toInAddress(): String {
+ val relayEndpoint = this.obfuscation?.endpoint ?: this.endpoint
+
+ val host = relayEndpoint.address.address.hostAddress ?: ""
+ val port = relayEndpoint.address.port
+ val protocol = relayEndpoint.protocol
+
+ return buildString {
+ append(host)
+ append(":")
+ append(port)
+ append(" ")
+ append(
+ when (protocol) {
+ TransportProtocol.Tcp -> stringResource(id = R.string.tcp)
+ TransportProtocol.Udp -> stringResource(id = R.string.udp)
+ }
+ )
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionInfoHeader.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionInfoHeader.kt
new file mode 100644
index 0000000000..fe6e5de012
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionInfoHeader.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.compose.component.connectioninfo
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextOverflow
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+@Composable
+fun ConnectionInfoHeader(text: String, modifier: Modifier = Modifier) {
+ Text(
+ modifier = modifier.padding(top = Dimens.smallPadding),
+ text = text,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ overflow = TextOverflow.Ellipsis,
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt
new file mode 100644
index 0000000000..8815149f27
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/FeatureIndicatorsPanel.kt
@@ -0,0 +1,95 @@
+package net.mullvad.mullvadvpn.compose.component.connectioninfo
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.ContextualFlowRow
+import androidx.compose.foundation.layout.ContextualFlowRowOverflow
+import androidx.compose.foundation.layout.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.component.MullvadFeatureChip
+import net.mullvad.mullvadvpn.compose.component.MullvadMoreChip
+import net.mullvad.mullvadvpn.compose.component.textResource
+import net.mullvad.mullvadvpn.lib.model.FeatureIndicator
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+@Composable
+fun FeatureIndicatorsPanel(
+ featureIndicators: List<FeatureIndicator>,
+ expanded: Boolean,
+ onToggleExpand: () -> Unit,
+) {
+ if (featureIndicators.isNotEmpty()) {
+ if (expanded) {
+ ConnectionInfoHeader(
+ stringResource(R.string.connect_panel_active_features),
+ Modifier.fillMaxWidth(),
+ )
+ }
+ FeatureIndicators(featureIndicators, expanded, onToggleExpand)
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Composable
+fun FeatureIndicators(
+ features: List<FeatureIndicator>,
+ expanded: Boolean,
+ onToggleExpand: () -> Unit,
+) {
+ ContextualFlowRow(
+ modifier = Modifier.fillMaxWidth(),
+ itemCount = features.size,
+ // FlowRow may crash if maxLines is set to 1
+ // https://issuetracker.google.com/issues/367440149 &
+ // https://issuetracker.google.com/issues/355003185
+ maxLines = if (expanded) Int.MAX_VALUE else 2,
+ horizontalArrangement = Arrangement.spacedBy(Dimens.smallPadding),
+ overflow =
+ ContextualFlowRowOverflow.expandOrCollapseIndicator(
+ expandIndicator = {
+ val hiddenFeatureCount = totalItemCount - shownItemCount
+ MullvadMoreChip(
+ onClick = onToggleExpand,
+ text =
+ stringResource(
+ R.string.feature_indicators_show_more,
+ hiddenFeatureCount,
+ ),
+ containerColor = Color.Transparent,
+ )
+ },
+ collapseIndicator = {},
+ ),
+ ) { index ->
+ MullvadFeatureChip(text = features[index].text())
+ }
+
+ // Spacing are added to compensate for when the
+ if (features.isEmpty() && !expanded) {
+ Spacer(Modifier.height(Dimens.smallSpacer))
+ }
+}
+
+@Composable
+private fun FeatureIndicator.text(): String {
+ val resource =
+ when (this) {
+ FeatureIndicator.QUANTUM_RESISTANCE -> R.string.feature_quantum_resistant
+ FeatureIndicator.SPLIT_TUNNELING -> R.string.feature_split_tunneling
+ FeatureIndicator.SHADOWSOCKS,
+ FeatureIndicator.UDP_2_TCP -> R.string.feature_udp_2_tcp
+ FeatureIndicator.LAN_SHARING -> R.string.feature_lan_sharing
+ FeatureIndicator.DNS_CONTENT_BLOCKERS -> R.string.feature_dns_content_blockers
+ FeatureIndicator.CUSTOM_DNS -> R.string.feature_custom_dns
+ FeatureIndicator.SERVER_IP_OVERRIDE -> R.string.feature_server_ip_override
+ FeatureIndicator.CUSTOM_MTU -> R.string.feature_custom_mtu
+ FeatureIndicator.DAITA -> R.string.feature_daita
+ }
+ return textResource(resource)
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
index bdce4abef6..ac325c8f9c 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
@@ -74,7 +74,7 @@ private fun PreviewNotificationBanner() {
@Composable
fun NotificationBanner(
- modifier: Modifier,
+ modifier: Modifier = Modifier,
notification: InAppNotification?,
isPlayBuild: Boolean,
onClickUpdateVersion: () -> Unit,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt
index b6c98b5ed8..f95c4681f9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AutoConnectAndLockdownModeScreen.kt
@@ -115,7 +115,7 @@ fun AutoConnectAndLockdownModeScreen(onBackClick: () -> Unit = {}) {
}
},
isEnabled = { pagerState.currentPage != 0 },
- rotation = 180f,
+ rotation = 90f,
)
// Go to next page
@@ -132,7 +132,7 @@ fun AutoConnectAndLockdownModeScreen(onBackClick: () -> Unit = {}) {
}
},
isEnabled = { pagerState.currentPage != pagerState.pageCount - 1 },
- rotation = 0f,
+ rotation = -90f,
)
PageIndicator(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index c9ee962eb1..b4811f95f0 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -4,28 +4,31 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
-import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -35,14 +38,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInParent
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.dropUnlessResumed
@@ -60,19 +64,20 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ConnectionButton
import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton
+import net.mullvad.mullvadvpn.compose.component.Chevron
import net.mullvad.mullvadvpn.compose.component.ConnectionStatusText
-import net.mullvad.mullvadvpn.compose.component.LocationInfo
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName
+import net.mullvad.mullvadvpn.compose.component.connectioninfo.ConnectionDetailPanel
+import net.mullvad.mullvadvpn.compose.component.connectioninfo.FeatureIndicatorsPanel
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.component.notificationbanner.NotificationBanner
import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.LOCATION_INFO_TEST_TAG
+import net.mullvad.mullvadvpn.compose.test.CONNECT_CARD_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
@@ -94,15 +99,23 @@ import net.mullvad.mullvadvpn.lib.model.Longitude
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.Shapes
+import net.mullvad.mullvadvpn.lib.theme.color.Alpha20
+import net.mullvad.mullvadvpn.lib.theme.color.Alpha80
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
+import net.mullvad.mullvadvpn.lib.theme.typeface.connectionStatus
+import net.mullvad.mullvadvpn.lib.theme.typeface.hostname
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
import net.mullvad.mullvadvpn.util.removeHtmlTags
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
import org.koin.androidx.compose.koinViewModel
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
+private val SCREEN_HEIGHT_THRESHOLD = 700.dp
+private const val SHORT_SCREEN_INDICATOR_BIAS = 0.2f
+private const val TALL_SCREEN_INDICATOR_BIAS = 0.3f
@Preview
@Composable
@@ -139,16 +152,19 @@ fun Connect(
is ConnectViewModel.UiSideEffect.OpenAccountManagementPageInBrowser -> {
openAccountPage(sideEffect.token)
}
+
is ConnectViewModel.UiSideEffect.OutOfTime ->
navigator.navigate(OutOfTimeDestination) {
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+
ConnectViewModel.UiSideEffect.RevokedDevice ->
navigator.navigate(DeviceRevokedDestination) {
launchSingleTop = true
popUpTo(NavGraphs.root) { inclusive = true }
}
+
is ConnectViewModel.UiSideEffect.NoVpnPermission -> launchVpnPermission.launch(Unit)
is ConnectViewModel.UiSideEffect.ConnectError ->
launch {
@@ -209,8 +225,6 @@ fun ConnectScreen(
onDismissNewDeviceClick: () -> Unit = {},
) {
- val scrollState = rememberScrollState()
-
ScaffoldWithTopBarAndDeviceName(
topBarColor = state.tunnelState.topBarColor(),
iconTintColor = state.tunnelState.iconTintColor(),
@@ -220,41 +234,44 @@ fun ConnectScreen(
timeLeft = state.daysLeftUntilExpiry,
snackbarHostState = snackbarHostState,
) {
- var progressIndicatorBias by remember { mutableFloatStateOf(0f) }
+ val configuration = LocalConfiguration.current
+ val screenHeight = configuration.screenHeightDp.dp
+ val indicatorPercentOffset =
+ if (screenHeight < SCREEN_HEIGHT_THRESHOLD) SHORT_SCREEN_INDICATOR_BIAS
+ else TALL_SCREEN_INDICATOR_BIAS
+
+ Box(Modifier.padding(it).fillMaxSize()) {
+ MullvadMap(state, indicatorPercentOffset)
- MapColumn(state, it, progressIndicatorBias, scrollState) {
- Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
MullvadCircularProgressIndicatorLarge(
color = MaterialTheme.colorScheme.onSurface,
modifier =
- Modifier.animateContentSize()
- .padding(
- start = Dimens.sideMargin,
- end = Dimens.sideMargin,
- top = Dimens.mediumPadding,
- )
- .alpha(if (state.showLoading) AlphaVisible else AlphaInvisible)
- .align(Alignment.CenterHorizontally)
- .testTag(CIRCULAR_PROGRESS_INDICATOR)
- .onGloballyPositioned {
- val offsetY = it.positionInParent().y + it.size.height / 2
- it.parentLayoutCoordinates?.let {
- val parentHeight = it.size.height
- val verticalBias = offsetY / parentHeight
- if (verticalBias.isFinite()) {
- progressIndicatorBias = verticalBias
- }
+ Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.placeRelative(
+ x = (constraints.maxWidth * 0.5f - placeable.width / 2).toInt(),
+ y =
+ (constraints.maxHeight * indicatorPercentOffset -
+ placeable.height / 2)
+ .toInt(),
+ )
}
- },
+ }
+ .alpha(if (state.showLoading) AlphaVisible else AlphaInvisible)
+ .testTag(CIRCULAR_PROGRESS_INDICATOR),
)
- Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
-
- ConnectionInfo(state = state)
- Spacer(modifier = Modifier.height(Dimens.buttonSpacing))
-
- ButtonPanel(
- state,
+ NotificationBanner(
+ notification = state.inAppNotification,
+ isPlayBuild = state.isPlayBuild,
+ onClickUpdateVersion = onUpdateVersionClick,
+ onClickShowAccount = onManageAccountClick,
+ onClickDismissNewDevice = onDismissNewDeviceClick,
+ )
+ ConnectionCard(
+ state = state,
+ modifier = Modifier.align(Alignment.BottomCenter),
onSwitchLocationClick,
onDisconnectClick,
onReconnectClick,
@@ -262,26 +279,11 @@ fun ConnectScreen(
onConnectClick,
)
}
-
- NotificationBanner(
- modifier = Modifier.padding(top = it.calculateTopPadding()),
- notification = state.inAppNotification,
- isPlayBuild = state.isPlayBuild,
- onClickUpdateVersion = onUpdateVersionClick,
- onClickShowAccount = onManageAccountClick,
- onClickDismissNewDevice = onDismissNewDeviceClick,
- )
}
}
@Composable
-private fun MapColumn(
- state: ConnectUiState,
- it: PaddingValues,
- progressIndicatorBias: Float,
- scrollState: ScrollState,
- content: @Composable ColumnScope.() -> Unit,
-) {
+private fun MullvadMap(state: ConnectUiState, progressIndicatorBias: Float) {
// Distance to marker when secure/unsecure
val baseZoom =
@@ -295,7 +297,7 @@ private fun MapColumn(
val markers = state.tunnelState.toMarker(state.location)?.let { listOf(it) } ?: emptyList()
AnimatedMap(
- modifier = Modifier.padding(top = it.calculateTopPadding()),
+ modifier = Modifier,
cameraLocation = state.location?.toLatLong() ?: fallbackLatLong,
cameraBaseZoom = baseZoom.value,
cameraVerticalBias = progressIndicatorBias,
@@ -306,64 +308,164 @@ private fun MapColumn(
oceanColor = MaterialTheme.colorScheme.surface,
),
)
+}
- Column(
- verticalArrangement = Arrangement.Bottom,
- horizontalAlignment = Alignment.Start,
+@Composable
+private fun ConnectionCard(
+ state: ConnectUiState,
+ modifier: Modifier = Modifier,
+ onSwitchLocationClick: () -> Unit,
+ onDisconnectClick: () -> Unit,
+ onReconnectClick: () -> Unit,
+ onCancelClick: () -> Unit,
+ onConnectClick: () -> Unit,
+) {
+ var expanded by rememberSaveable(state.tunnelState::class) { mutableStateOf(false) }
+ val containerColor =
+ animateColorAsState(
+ if (expanded) MaterialTheme.colorScheme.surfaceContainer
+ else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = Alpha80),
+ label = "connection_card_color",
+ )
+
+ Card(
modifier =
- Modifier.animateContentSize()
- .padding(top = it.calculateTopPadding())
- .fillMaxHeight()
- .drawVerticalScrollbar(
- scrollState,
- color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar),
- )
- .verticalScroll(scrollState)
- .testTag(SCROLLABLE_COLUMN_TEST_TAG),
+ modifier.widthIn(max = Dimens.connectionCardMaxWidth).padding(Dimens.mediumPadding),
+ Shapes.large,
+ colors = CardDefaults.cardColors(containerColor = containerColor.value),
) {
- content()
- // We need to manually add this padding so we align size with the map
- // component and marker with the progress indicator.
- Spacer(modifier = Modifier.height(it.calculateBottomPadding()))
+ Column(
+ modifier =
+ Modifier.padding(
+ top = Dimens.mediumPadding,
+ start = Dimens.mediumPadding,
+ end = Dimens.mediumPadding,
+ bottom = Dimens.smallPadding,
+ )
+ ) {
+ ConnectionCardHeader(state, state.location, expanded) { expanded = !expanded }
+
+ AnimatedContent(
+ state.tunnelState as? TunnelState.Connected to expanded,
+ modifier = Modifier.weight(1f, fill = false),
+ label = "connection_card_connection_details",
+ ) { (connectedState, isExpanded) ->
+ if (connectedState != null) {
+ ConnectionInfo(
+ connectedState,
+ isExpanded,
+ onToggleExpand = { expanded = !expanded },
+ )
+ } else {
+ Spacer(Modifier.height(Dimens.smallSpacer))
+ }
+ }
+
+ Spacer(Modifier.height(Dimens.mediumPadding))
+
+ ButtonPanel(
+ state,
+ onSwitchLocationClick,
+ onDisconnectClick,
+ onReconnectClick,
+ onCancelClick,
+ onConnectClick,
+ )
+ }
}
}
@Composable
-private fun ConnectionInfo(state: ConnectUiState) {
- ConnectionStatusText(
- state = state.tunnelState,
- modifier = Modifier.padding(horizontal = Dimens.sideMargin),
- )
- Text(
- text = state.location?.country ?: "",
- style = MaterialTheme.typography.headlineLarge,
- color = MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(horizontal = Dimens.sideMargin),
- )
- Text(
- text = state.location?.city ?: "",
- style = MaterialTheme.typography.headlineLarge,
- color = MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(horizontal = Dimens.sideMargin),
- )
- var expanded by rememberSaveable { mutableStateOf(false) }
- LocationInfo(
- onToggleTunnelInfo = { expanded = !expanded },
- isVisible = state.showLocationInfo,
- isExpanded = expanded,
- location = state.location,
- isUsingDaita = state.tunnelState.isUsingDaita(),
- inAddress = state.inAddress,
- outAddress = state.outAddress,
+private fun ConnectionCardHeader(
+ state: ConnectUiState,
+ location: GeoIpLocation?,
+ expanded: Boolean,
+ onToggleExpand: () -> Unit,
+) {
+ Column(
modifier =
Modifier.fillMaxWidth()
- .padding(horizontal = Dimens.sideMargin)
- .testTag(LOCATION_INFO_TEST_TAG),
- )
+ .clickable(
+ enabled = state.tunnelState is TunnelState.Connected,
+ onClick = onToggleExpand,
+ )
+ .testTag(CONNECT_CARD_HEADER_TEST_TAG)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ ConnectionStatusText(state = state.tunnelState)
+ if (state.tunnelState is TunnelState.Connected) {
+ Chevron(isExpanded = !expanded, color = MaterialTheme.colorScheme.onSurface)
+ }
+ }
+
+ Text(
+ modifier = Modifier.fillMaxWidth().padding(top = Dimens.tinyPadding),
+ text = location.asString(),
+ style = MaterialTheme.typography.connectionStatus,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+
+ val hostname = location?.hostname
+ AnimatedContent(hostname, label = "hostname") {
+ if (it != null) {
+ Text(
+ modifier = Modifier.fillMaxWidth(),
+ text = it,
+ style = MaterialTheme.typography.hostname,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+ }
+}
+
+private fun GeoIpLocation?.asString(): String {
+ return if (this == null) ""
+ else {
+ buildString {
+ append(country)
+ city?.let {
+ append(", ")
+ append(it)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ConnectionInfo(
+ tunnelState: TunnelState.Connected,
+ expanded: Boolean,
+ onToggleExpand: () -> Unit,
+) {
+ val scrollState = rememberScrollState()
+ Column {
+ if (expanded) {
+ HorizontalDivider(
+ Modifier.padding(vertical = Dimens.smallPadding),
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(Alpha20),
+ )
+ }
+ Column(
+ modifier =
+ Modifier.fillMaxWidth()
+ .drawVerticalScrollbar(
+ scrollState,
+ color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar),
+ )
+ .verticalScroll(scrollState)
+ ) {
+ FeatureIndicatorsPanel(tunnelState.featureIndicators, expanded, onToggleExpand)
+
+ if (expanded) {
+ ConnectionDetailPanel(tunnelState)
+ }
+ }
+ }
}
@Composable
@@ -384,34 +486,32 @@ private fun ButtonPanel(
action.invoke()
}
}
+ Column(modifier = Modifier.padding(vertical = Dimens.tinyPadding)) {
+ SwitchLocationButton(
+ text =
+ if (state.showLocation && state.selectedRelayItemTitle != null) {
+ state.selectedRelayItemTitle
+ } else {
+ stringResource(id = R.string.switch_location)
+ },
+ onSwitchLocation = onSwitchLocationClick,
+ reconnectClick = { handleThrottledAction(onReconnectClick) },
+ isReconnectButtonEnabled =
+ state.tunnelState is TunnelState.Connected ||
+ state.tunnelState is TunnelState.Connecting,
+ modifier = Modifier.testTag(SELECT_LOCATION_BUTTON_TEST_TAG),
+ reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG,
+ )
+ Spacer(Modifier.height(Dimens.buttonVerticalPadding))
- SwitchLocationButton(
- modifier =
- Modifier.fillMaxWidth()
- .padding(horizontal = Dimens.sideMargin)
- .testTag(SELECT_LOCATION_BUTTON_TEST_TAG),
- onClick = onSwitchLocationClick,
- showChevron = state.showLocation,
- text =
- if (state.showLocation && state.selectedRelayItemTitle != null) {
- state.selectedRelayItemTitle
- } else {
- stringResource(id = R.string.switch_location)
- },
- )
- Spacer(modifier = Modifier.height(Dimens.buttonSpacing))
- ConnectionButton(
- state = state.tunnelState,
- modifier =
- Modifier.padding(horizontal = Dimens.sideMargin)
- .padding(bottom = Dimens.screenVerticalMargin)
- .testTag(CONNECT_BUTTON_TEST_TAG),
- disconnectClick = onDisconnectClick,
- reconnectClick = { handleThrottledAction(onReconnectClick) },
- cancelClick = onCancelClick,
- connectClick = { handleThrottledAction(onConnectClick) },
- reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG,
- )
+ ConnectionButton(
+ modifier = Modifier.fillMaxWidth().testTag(CONNECT_BUTTON_TEST_TAG),
+ state = state.tunnelState,
+ disconnectClick = onDisconnectClick,
+ cancelClick = onCancelClick,
+ connectClick = { handleThrottledAction(onConnectClick) },
+ )
+ }
}
@Composable
@@ -423,12 +523,14 @@ fun TunnelState.toMarker(location: GeoIpLocation?): Marker? {
location.toLatLong(),
colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.tertiary),
)
+
is TunnelState.Connecting -> null
is TunnelState.Disconnected ->
Marker(
location.toLatLong(),
colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.error),
)
+
is TunnelState.Disconnecting -> null
is TunnelState.Error -> null
}
@@ -453,11 +555,13 @@ private fun ConnectViewModel.UiSideEffect.ConnectError.toMessage(context: Contex
when (this) {
ConnectViewModel.UiSideEffect.ConnectError.NoVpnPermission ->
context.getString(R.string.vpn_permission_denied_error)
+
is ConnectViewModel.UiSideEffect.ConnectError.AlwaysOnVpn ->
// Snackbar currently do not support annotated string
context
.getString(R.string.always_on_vpn_error_notification_content, appName)
.removeHtmlTags()
+
ConnectViewModel.UiSideEffect.ConnectError.Generic ->
context.getString(R.string.error_occurred)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
index 910bdaa17f..98b5219785 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt
@@ -1,7 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
-import net.mullvad.mullvadvpn.lib.model.TransportProtocol
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.repository.InAppNotification
@@ -9,8 +8,6 @@ data class ConnectUiState(
val location: GeoIpLocation?,
val selectedRelayItemTitle: String?,
val tunnelState: TunnelState,
- val inAddress: Triple<String, Int, TransportProtocol>?,
- val outAddress: String,
val showLocation: Boolean,
val inAppNotification: InAppNotification?,
val deviceName: String?,
@@ -18,8 +15,6 @@ data class ConnectUiState(
val isPlayBuild: Boolean,
) {
- val showLocationInfo: Boolean =
- tunnelState !is TunnelState.Disconnected && location?.hostname != null
val showLoading =
tunnelState is TunnelState.Connecting || tunnelState is TunnelState.Disconnecting
@@ -29,8 +24,6 @@ data class ConnectUiState(
location = null,
selectedRelayItemTitle = null,
tunnelState = TunnelState.Disconnected(),
- inAddress = null,
- outAddress = "",
showLocation = false,
inAppNotification = null,
deviceName = null,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
index 299c99190d..4163010c1d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt
@@ -26,7 +26,7 @@ const val SCROLLABLE_COLUMN_TEST_TAG = "scrollable_column_test_tag"
const val SELECT_LOCATION_BUTTON_TEST_TAG = "select_location_button_test_tag"
const val CONNECT_BUTTON_TEST_TAG = "connect_button_test_tag"
const val RECONNECT_BUTTON_TEST_TAG = "reconnect_button_test_tag"
-const val LOCATION_INFO_TEST_TAG = "location_info_test_tag"
+const val CONNECT_CARD_HEADER_TEST_TAG = "connect_card_header_test_tag"
const val LOCATION_INFO_CONNECTION_OUT_TEST_TAG = "location_info_connection_out_test_tag"
// ConnectScreen - Notification banner
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt
deleted file mode 100644
index d908f44158..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/GeoIpLocationExtensions.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package net.mullvad.mullvadvpn.util
-
-import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
-
-fun GeoIpLocation.toOutAddress(): String =
- when {
- ipv6 != null && ipv4 != null -> "${ipv4!!.hostAddress} / ${ipv6!!.hostAddress}"
- ipv6 != null -> ipv6!!.hostAddress ?: ""
- ipv4 != null -> ipv4!!.hostAddress ?: ""
- else -> ""
- }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
index d8c310b029..20d8f4fc8e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/TunnelEndpointExtensions.kt
@@ -1,12 +1 @@
package net.mullvad.mullvadvpn.util
-
-import net.mullvad.mullvadvpn.lib.model.TransportProtocol
-import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
-
-fun TunnelEndpoint.toInAddress(): Triple<String, Int, TransportProtocol> {
- val relayEndpoint = this.obfuscation?.endpoint ?: this.endpoint
- val host = relayEndpoint.address.address.hostAddress ?: ""
- val port = relayEndpoint.address.port
- val protocol = relayEndpoint.protocol
- return Triple(host, port, protocol)
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
index 7da838e58f..33d81f3ba1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt
@@ -33,8 +33,6 @@ import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
import net.mullvad.mullvadvpn.util.combine
import net.mullvad.mullvadvpn.util.daysFromNow
import net.mullvad.mullvadvpn.util.isSuccess
-import net.mullvad.mullvadvpn.util.toInAddress
-import net.mullvad.mullvadvpn.util.toOutAddress
@Suppress("LongParameterList")
class ConnectViewModel(
@@ -83,13 +81,6 @@ class ConnectViewModel(
},
selectedRelayItemTitle = selectedRelayItemTitle,
tunnelState = tunnelState,
- inAddress =
- when (tunnelState) {
- is TunnelState.Connected -> tunnelState.endpoint.toInAddress()
- is TunnelState.Connecting -> tunnelState.endpoint?.toInAddress()
- else -> null
- },
- outAddress = tunnelState.location()?.toOutAddress() ?: "",
showLocation =
when (tunnelState) {
is TunnelState.Disconnected -> true
diff --git a/android/app/src/main/res/drawable/icon_reload.xml b/android/app/src/main/res/drawable/icon_reload.xml
new file mode 100644
index 0000000000..1ab457eff5
--- /dev/null
+++ b/android/app/src/main/res/drawable/icon_reload.xml
@@ -0,0 +1,14 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <clip-path
+ android:pathData="M0,0h24v24h-24z"/>
+ <path
+ android:pathData="M6,12C6,8.686 8.686,6 12,6C13.776,6 15.373,6.771 16.472,8H15C14.448,8 14,8.448 14,9C14,9.552 14.448,10 15,10H19C19.552,10 20,9.552 20,9V5C20,4.448 19.552,4 19,4C18.448,4 18,4.448 18,5V6.709C16.535,5.049 14.39,4 12,4C7.582,4 4,7.582 4,12C4,16.418 7.582,20 12,20C14.13,20 16.067,19.166 17.5,17.809C17.901,17.43 17.918,16.797 17.539,16.396C17.159,15.995 16.526,15.977 16.125,16.357C15.049,17.376 13.598,18 12,18C8.686,18 6,15.314 6,12Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+ </group>
+</vector>
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
index 79f97ff466..267813d62a 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt
@@ -9,7 +9,6 @@ import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlin.test.assertEquals
import kotlin.test.assertIs
@@ -41,8 +40,6 @@ import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
-import net.mullvad.mullvadvpn.util.toInAddress
-import net.mullvad.mullvadvpn.util.toOutAddress
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -95,9 +92,6 @@ class ConnectViewModelTest {
@BeforeEach
fun setup() {
- mockkStatic(TUNNEL_ENDPOINT_EXTENSIONS)
- mockkStatic(GEO_IP_LOCATIONS_EXTENSIONS)
-
every { mockServiceConnectionManager.connectionState } returns serviceConnectionState
every { mockAccountRepository.accountData } returns accountExpiryState
@@ -164,8 +158,7 @@ class ConnectViewModelTest {
val tunnelEndpoint: TunnelEndpoint = mockk()
val location: GeoIpLocation = mockk()
val tunnelStateTestItem = TunnelState.Connected(tunnelEndpoint, location, emptyList())
- every { tunnelEndpoint.toInAddress() } returns mockk(relaxed = true)
- every { location.toOutAddress() } returns "1.1.1.1"
+ every { location.ipv4?.hostAddress } returns "1.1.1.1"
every { location.hostname } returns "hostname"
// Act, Assert
@@ -330,11 +323,4 @@ class ConnectViewModelTest {
// Assert
assertIs<ConnectViewModel.UiSideEffect.OutOfTime>(deferred.await())
}
-
- companion object {
- private const val TUNNEL_ENDPOINT_EXTENSIONS =
- "net.mullvad.mullvadvpn.util.TunnelEndpointExtensionsKt"
- private const val GEO_IP_LOCATIONS_EXTENSIONS =
- "net.mullvad.mullvadvpn.util.GeoIpLocationExtensionsKt"
- }
}