summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson90@gmail.com>2024-02-13 16:28:56 +0100
committerDavid Göransson <david.goransson90@gmail.com>2024-02-15 13:59:43 +0100
commitdb4179285e29c0c94b9195f7579ef17daaa8b719 (patch)
treec13dbdc286dfc9740a7d19b8c2c03e578817cd46 /android
parentec0662046cea00a0597ae7fe468500a451beafb3 (diff)
downloadmullvadvpn-db4179285e29c0c94b9195f7579ef17daaa8b719.tar.xz
mullvadvpn-db4179285e29c0c94b9195f7579ef17daaa8b719.zip
Integrate map into ConnectScreen
Diffstat (limited to 'android')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt176
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt2
5 files changed, 150 insertions, 51 deletions
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 3d4a71f1af..94dc40a175 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
@@ -89,6 +89,7 @@ private fun PreviewNotificationBanner() {
@Composable
fun NotificationBanner(
+ modifier: Modifier,
notification: InAppNotification?,
isPlayBuild: Boolean,
onClickUpdateVersion: () -> Unit,
@@ -101,7 +102,7 @@ fun NotificationBanner(
visible = notification != null,
enter = slideInVertically(initialOffsetY = { -it }),
exit = slideOutVertically(targetOffsetY = { -it }),
- modifier = Modifier.animateContentSize()
+ modifier = modifier
) {
val visibleNotification = notification ?: previous
if (visibleNotification != null)
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 84d2a0418e..a3ebaabf99 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
@@ -2,10 +2,13 @@ package net.mullvad.mullvadvpn.compose.screen
import android.content.Intent
import android.net.Uri
-import androidx.compose.foundation.background
+import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -18,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
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
@@ -25,6 +29,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
@@ -55,15 +63,28 @@ 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.constant.SECURE_ZOOM
+import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
+import net.mullvad.mullvadvpn.constant.UNSECURE_ZOOM
+import net.mullvad.mullvadvpn.constant.fallbackLatLong
import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.lib.map.AnimatedMap
+import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
+import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
+import net.mullvad.mullvadvpn.lib.map.data.Marker
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.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
+import net.mullvad.mullvadvpn.model.GeoIpLocation
+import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.model.Latitude
+import net.mullvad.mullvadvpn.model.Longitude
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
import org.koin.androidx.compose.koinViewModel
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
@@ -164,66 +185,80 @@ fun ConnectScreen(
}
ScaffoldWithTopBarAndDeviceName(
- topBarColor =
- if (uiState.tunnelUiState.isSecured()) {
- MaterialTheme.colorScheme.inversePrimary
- } else {
- MaterialTheme.colorScheme.error
- },
- iconTintColor =
- if (uiState.tunnelUiState.isSecured()) {
- MaterialTheme.colorScheme.onPrimary
- } else {
- MaterialTheme.colorScheme.onError
- }
- .copy(alpha = AlphaTopBar),
+ topBarColor = uiState.tunnelUiState.topBarColor(),
+ iconTintColor = uiState.tunnelUiState.iconTintColor(),
onSettingsClicked = onSettingsClick,
onAccountClicked = onAccountClick,
deviceName = uiState.deviceName,
timeLeft = uiState.daysLeftUntilExpiry
) {
+ var progressIndicatorBias by remember { mutableFloatStateOf(0f) }
+
+ // Distance to marker when secure/unsecure
+ val baseZoom =
+ animateFloatAsState(
+ targetValue =
+ if (uiState.tunnelRealState is TunnelState.Connected) SECURE_ZOOM
+ else UNSECURE_ZOOM,
+ animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS),
+ label = "baseZoom"
+ )
+
+ val markers =
+ uiState.tunnelRealState.toMarker(uiState.location)?.let { listOf(it) } ?: emptyList()
+
+ AnimatedMap(
+ modifier = Modifier.padding(top = it.calculateTopPadding()),
+ cameraLocation = uiState.location?.toLatLong() ?: fallbackLatLong,
+ cameraBaseZoom = baseZoom.value,
+ cameraVerticalBias = progressIndicatorBias,
+ markers = markers,
+ globeColors =
+ GlobeColors(
+ landColor = MaterialTheme.colorScheme.primary,
+ oceanColor = MaterialTheme.colorScheme.secondary,
+ )
+ )
+
Column(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.Start,
modifier =
- Modifier.background(color = MaterialTheme.colorScheme.primary)
- .padding(it)
+ Modifier.animateContentSize()
+ .padding(top = it.calculateTopPadding())
.fillMaxHeight()
.drawVerticalScrollbar(
scrollState,
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar)
)
.verticalScroll(scrollState)
- .padding(bottom = Dimens.screenVerticalMargin)
.testTag(SCROLLABLE_COLUMN_TEST_TAG)
) {
- NotificationBanner(
- notification = uiState.inAppNotification,
- isPlayBuild = uiState.isPlayBuild,
- onClickUpdateVersion = onUpdateVersionClick,
- onClickShowAccount = onManageAccountClick,
- onClickDismissNewDevice = onDismissNewDeviceClick,
+ Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
+ MullvadCircularProgressIndicatorLarge(
+ color = MaterialTheme.colorScheme.onPrimary,
+ modifier =
+ Modifier.animateContentSize()
+ .padding(
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin,
+ top = Dimens.mediumPadding
+ )
+ .alpha(if (uiState.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
+ }
+ }
+ }
)
- Spacer(modifier = Modifier.weight(1f))
- if (
- uiState.tunnelRealState is TunnelState.Connecting ||
- (uiState.tunnelRealState is TunnelState.Disconnecting &&
- uiState.tunnelRealState.actionAfterDisconnect ==
- ActionAfterDisconnect.Reconnect)
- ) {
- MullvadCircularProgressIndicatorLarge(
- color = MaterialTheme.colorScheme.onPrimary,
- modifier =
- Modifier.padding(
- start = Dimens.sideMargin,
- end = Dimens.sideMargin,
- top = Dimens.mediumPadding
- )
- .align(Alignment.CenterHorizontally)
- .testTag(CIRCULAR_PROGRESS_INDICATOR)
- )
- }
- Spacer(modifier = Modifier.height(Dimens.mediumPadding))
+ Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
ConnectionStatusText(
state = uiState.tunnelRealState,
modifier = Modifier.padding(horizontal = Dimens.sideMargin)
@@ -243,9 +278,7 @@ fun ConnectScreen(
var expanded by rememberSaveable { mutableStateOf(false) }
LocationInfo(
onToggleTunnelInfo = { expanded = !expanded },
- isVisible =
- uiState.tunnelRealState !is TunnelState.Disconnected &&
- uiState.location?.hostname != null,
+ isVisible = uiState.showLocationInfo,
isExpanded = expanded,
location = uiState.location,
inAddress = uiState.inAddress,
@@ -282,6 +315,55 @@ fun ConnectScreen(
connectClick = { handleThrottledAction(onConnectClick) },
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
)
+ // 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()))
}
+
+ NotificationBanner(
+ modifier = Modifier.padding(top = it.calculateTopPadding()),
+ notification = uiState.inAppNotification,
+ isPlayBuild = uiState.isPlayBuild,
+ onClickUpdateVersion = onUpdateVersionClick,
+ onClickShowAccount = onManageAccountClick,
+ onClickDismissNewDevice = onDismissNewDeviceClick,
+ )
+ }
+}
+
+@Composable
+fun TunnelState.toMarker(location: GeoIpLocation?): Marker? {
+ if (location == null) return null
+ return when (this) {
+ is TunnelState.Connected ->
+ Marker(
+ location.toLatLong(),
+ colors =
+ LocationMarkerColors(centerColor = MaterialTheme.colorScheme.inversePrimary),
+ )
+ is TunnelState.Connecting -> null
+ is TunnelState.Disconnected ->
+ Marker(
+ location.toLatLong(),
+ colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.error)
+ )
+ is TunnelState.Disconnecting -> null
+ is TunnelState.Error -> null
}
}
+
+@Composable
+fun TunnelState.topBarColor(): Color =
+ if (isSecured()) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.error
+
+@Composable
+fun TunnelState.iconTintColor(): Color =
+ if (isSecured()) {
+ MaterialTheme.colorScheme.onPrimary
+ } else {
+ MaterialTheme.colorScheme.onError
+ }
+ .copy(alpha = AlphaTopBar)
+
+fun GeoIpLocation.toLatLong() =
+ LatLong(Latitude(latitude.toFloat()), Longitude(longitude.toFloat()))
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 988ae914e9..4a1c41e562 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
@@ -17,8 +17,14 @@ data class ConnectUiState(
val inAppNotification: InAppNotification?,
val deviceName: String?,
val daysLeftUntilExpiry: Int?,
- val isPlayBuild: Boolean
+ val isPlayBuild: Boolean,
) {
+
+ val showLocationInfo: Boolean =
+ tunnelRealState !is TunnelState.Disconnected && location?.hostname != null
+ val showLoading =
+ tunnelRealState is TunnelState.Connecting || tunnelRealState is TunnelState.Disconnecting
+
companion object {
val INITIAL =
ConnectUiState(
@@ -32,7 +38,7 @@ data class ConnectUiState(
inAppNotification = null,
deviceName = null,
daysLeftUntilExpiry = null,
- isPlayBuild = false
+ isPlayBuild = false,
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
index 4ccf15bb63..d58107c713 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt
@@ -1,6 +1,9 @@
package net.mullvad.mullvadvpn.constant
import androidx.compose.animation.core.Spring
+import net.mullvad.mullvadvpn.model.LatLong
+import net.mullvad.mullvadvpn.model.Latitude
+import net.mullvad.mullvadvpn.model.Longitude
const val MINIMUM_LOADING_TIME_MILLIS = 500L
@@ -9,3 +12,10 @@ const val SCREEN_ANIMATION_TIME_MILLIS = Spring.StiffnessMediumLow.toInt()
const val HORIZONTAL_SLIDE_FACTOR = 1 / 3f
fun Int.withHorizontalScalingFactor(): Int = (this * HORIZONTAL_SLIDE_FACTOR).toInt()
+
+const val SECURE_ZOOM = 1.15f
+const val UNSECURE_ZOOM = 1.20f
+const val SECURE_ZOOM_ANIMATION_MILLIS = 2000
+
+// Location of Gothenburg, Sweden
+val fallbackLatLong = LatLong(Latitude(57.7065f), Longitude(11.967f))
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 bee5b1ad00..12427e2285 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
@@ -129,7 +129,7 @@ class ConnectViewModel(
inAppNotification = notifications.firstOrNull(),
deviceName = deviceName,
daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow(),
- isPlayBuild = isPlayBuild
+ isPlayBuild = isPlayBuild,
)
}
}