Skip to content

Commit 87ed9e3

Browse files
committed
Integrate map into ConnectScreen
1 parent 51c9a16 commit 87ed9e3

File tree

5 files changed

+147
-51
lines changed

5 files changed

+147
-51
lines changed

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ private fun PreviewNotificationBanner() {
8989

9090
@Composable
9191
fun NotificationBanner(
92+
modifier: Modifier,
9293
notification: InAppNotification?,
9394
isPlayBuild: Boolean,
9495
onClickUpdateVersion: () -> Unit,
@@ -101,7 +102,7 @@ fun NotificationBanner(
101102
visible = notification != null,
102103
enter = slideInVertically(initialOffsetY = { -it }),
103104
exit = slideOutVertically(targetOffsetY = { -it }),
104-
modifier = Modifier.animateContentSize()
105+
modifier = modifier
105106
) {
106107
val visibleNotification = notification ?: previous
107108
if (visibleNotification != null)

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt

+126-47
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package net.mullvad.mullvadvpn.compose.screen
22

33
import android.content.Intent
44
import android.net.Uri
5-
import androidx.compose.foundation.background
5+
import androidx.compose.animation.animateContentSize
6+
import androidx.compose.animation.core.animateFloatAsState
7+
import androidx.compose.animation.core.tween
68
import androidx.compose.foundation.layout.Arrangement
79
import androidx.compose.foundation.layout.Column
810
import androidx.compose.foundation.layout.Spacer
11+
import androidx.compose.foundation.layout.defaultMinSize
912
import androidx.compose.foundation.layout.fillMaxHeight
1013
import androidx.compose.foundation.layout.fillMaxWidth
1114
import androidx.compose.foundation.layout.height
@@ -18,13 +21,18 @@ import androidx.compose.runtime.Composable
1821
import androidx.compose.runtime.LaunchedEffect
1922
import androidx.compose.runtime.collectAsState
2023
import androidx.compose.runtime.getValue
24+
import androidx.compose.runtime.mutableFloatStateOf
2125
import androidx.compose.runtime.mutableLongStateOf
2226
import androidx.compose.runtime.mutableStateOf
2327
import androidx.compose.runtime.remember
2428
import androidx.compose.runtime.saveable.rememberSaveable
2529
import androidx.compose.runtime.setValue
2630
import androidx.compose.ui.Alignment
2731
import androidx.compose.ui.Modifier
32+
import androidx.compose.ui.draw.alpha
33+
import androidx.compose.ui.graphics.Color
34+
import androidx.compose.ui.layout.onGloballyPositioned
35+
import androidx.compose.ui.layout.positionInParent
2836
import androidx.compose.ui.platform.LocalContext
2937
import androidx.compose.ui.platform.testTag
3038
import androidx.compose.ui.res.stringResource
@@ -55,15 +63,26 @@ import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
5563
import net.mullvad.mullvadvpn.compose.test.SCROLLABLE_COLUMN_TEST_TAG
5664
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
5765
import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
66+
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM
67+
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
68+
import net.mullvad.mullvadvpn.constant.UNSECURE_ZOOM
69+
import net.mullvad.mullvadvpn.constant.fallbackLatLong
5870
import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
71+
import net.mullvad.mullvadvpn.lib.map.AnimatedMap
72+
import net.mullvad.mullvadvpn.lib.map.data.GlobeColors
73+
import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors
74+
import net.mullvad.mullvadvpn.lib.map.data.Marker
5975
import net.mullvad.mullvadvpn.lib.theme.AppTheme
6076
import net.mullvad.mullvadvpn.lib.theme.Dimens
6177
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
6278
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
79+
import net.mullvad.mullvadvpn.model.GeoIpLocation
80+
import net.mullvad.mullvadvpn.model.LatLong
81+
import net.mullvad.mullvadvpn.model.Latitude
82+
import net.mullvad.mullvadvpn.model.Longitude
6383
import net.mullvad.mullvadvpn.model.TunnelState
6484
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
6585
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
66-
import net.mullvad.talpid.tunnel.ActionAfterDisconnect
6786
import org.koin.androidx.compose.koinViewModel
6887

6988
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
@@ -164,66 +183,79 @@ fun ConnectScreen(
164183
}
165184

166185
ScaffoldWithTopBarAndDeviceName(
167-
topBarColor =
168-
if (uiState.tunnelUiState.isSecured()) {
169-
MaterialTheme.colorScheme.inversePrimary
170-
} else {
171-
MaterialTheme.colorScheme.error
172-
},
173-
iconTintColor =
174-
if (uiState.tunnelUiState.isSecured()) {
175-
MaterialTheme.colorScheme.onPrimary
176-
} else {
177-
MaterialTheme.colorScheme.onError
178-
}
179-
.copy(alpha = AlphaTopBar),
186+
topBarColor = uiState.tunnelUiState.topBarColor(),
187+
iconTintColor = uiState.tunnelUiState.iconTintColor(),
180188
onSettingsClicked = onSettingsClick,
181189
onAccountClicked = onAccountClick,
182190
deviceName = uiState.deviceName,
183191
timeLeft = uiState.daysLeftUntilExpiry
184192
) {
193+
var progressIndicatorBias by remember { mutableFloatStateOf(0f) }
194+
195+
// Distance to marker when secure/unsecure
196+
val baseZoom =
197+
animateFloatAsState(
198+
targetValue =
199+
if (uiState.tunnelRealState is TunnelState.Connected) SECURE_ZOOM
200+
else UNSECURE_ZOOM,
201+
animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS),
202+
label = "baseZoom"
203+
)
204+
205+
val markers =
206+
uiState.tunnelRealState.toMarker(uiState.location)?.let { listOf(it) } ?: emptyList()
207+
208+
AnimatedMap(
209+
modifier = Modifier.padding(top = it.calculateTopPadding()),
210+
cameraLocation = uiState.location?.toLatLong() ?: fallbackLatLong,
211+
cameraBaseZoom = baseZoom.value,
212+
cameraVerticalBias = progressIndicatorBias,
213+
markers = markers,
214+
globeColors =
215+
GlobeColors(
216+
landColor = MaterialTheme.colorScheme.primary,
217+
oceanColor = MaterialTheme.colorScheme.secondary,
218+
)
219+
)
220+
185221
Column(
186222
verticalArrangement = Arrangement.Bottom,
187223
horizontalAlignment = Alignment.Start,
188224
modifier =
189-
Modifier.background(color = MaterialTheme.colorScheme.primary)
190-
.padding(it)
225+
Modifier.animateContentSize()
226+
.padding(top = it.calculateTopPadding())
191227
.fillMaxHeight()
192228
.drawVerticalScrollbar(
193229
scrollState,
194230
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar)
195231
)
196232
.verticalScroll(scrollState)
197-
.padding(bottom = Dimens.screenVerticalMargin)
198233
.testTag(SCROLLABLE_COLUMN_TEST_TAG)
199234
) {
200-
NotificationBanner(
201-
notification = uiState.inAppNotification,
202-
isPlayBuild = uiState.isPlayBuild,
203-
onClickUpdateVersion = onUpdateVersionClick,
204-
onClickShowAccount = onManageAccountClick,
205-
onClickDismissNewDevice = onDismissNewDeviceClick,
235+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
236+
MullvadCircularProgressIndicatorLarge(
237+
color = MaterialTheme.colorScheme.onPrimary,
238+
modifier =
239+
Modifier.animateContentSize()
240+
.padding(
241+
start = Dimens.sideMargin,
242+
end = Dimens.sideMargin,
243+
top = Dimens.mediumPadding
244+
)
245+
.alpha(if (uiState.showLoading) 1f else 0f)
246+
.align(Alignment.CenterHorizontally)
247+
.testTag(CIRCULAR_PROGRESS_INDICATOR)
248+
.onGloballyPositioned {
249+
val offsetY = it.positionInParent().y + it.size.height / 2
250+
it.parentLayoutCoordinates?.let {
251+
val parentHeight = it.size.height
252+
if (parentHeight != 0) {
253+
progressIndicatorBias = offsetY / parentHeight
254+
}
255+
}
256+
}
206257
)
207-
Spacer(modifier = Modifier.weight(1f))
208-
if (
209-
uiState.tunnelRealState is TunnelState.Connecting ||
210-
(uiState.tunnelRealState is TunnelState.Disconnecting &&
211-
uiState.tunnelRealState.actionAfterDisconnect ==
212-
ActionAfterDisconnect.Reconnect)
213-
) {
214-
MullvadCircularProgressIndicatorLarge(
215-
color = MaterialTheme.colorScheme.onPrimary,
216-
modifier =
217-
Modifier.padding(
218-
start = Dimens.sideMargin,
219-
end = Dimens.sideMargin,
220-
top = Dimens.mediumPadding
221-
)
222-
.align(Alignment.CenterHorizontally)
223-
.testTag(CIRCULAR_PROGRESS_INDICATOR)
224-
)
225-
}
226-
Spacer(modifier = Modifier.height(Dimens.mediumPadding))
258+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
227259
ConnectionStatusText(
228260
state = uiState.tunnelRealState,
229261
modifier = Modifier.padding(horizontal = Dimens.sideMargin)
@@ -243,9 +275,7 @@ fun ConnectScreen(
243275
var expanded by rememberSaveable { mutableStateOf(false) }
244276
LocationInfo(
245277
onToggleTunnelInfo = { expanded = !expanded },
246-
isVisible =
247-
uiState.tunnelRealState !is TunnelState.Disconnected &&
248-
uiState.location?.hostname != null,
278+
isVisible = uiState.showLocationInfo,
249279
isExpanded = expanded,
250280
location = uiState.location,
251281
inAddress = uiState.inAddress,
@@ -282,6 +312,55 @@ fun ConnectScreen(
282312
connectClick = { handleThrottledAction(onConnectClick) },
283313
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
284314
)
315+
// We need to manually add this padding so we align size with the map
316+
// component and marker with the progress indicator.
317+
Spacer(modifier = Modifier.height(it.calculateBottomPadding()))
285318
}
319+
320+
NotificationBanner(
321+
modifier = Modifier.padding(top = it.calculateTopPadding()),
322+
notification = uiState.inAppNotification,
323+
isPlayBuild = uiState.isPlayBuild,
324+
onClickUpdateVersion = onUpdateVersionClick,
325+
onClickShowAccount = onManageAccountClick,
326+
onClickDismissNewDevice = onDismissNewDeviceClick,
327+
)
328+
}
329+
}
330+
331+
@Composable
332+
fun TunnelState.toMarker(location: GeoIpLocation?): Marker? {
333+
if (location == null) return null
334+
return when (this) {
335+
is TunnelState.Connected ->
336+
Marker(
337+
location.toLatLong(),
338+
colors =
339+
LocationMarkerColors(centerColor = MaterialTheme.colorScheme.inversePrimary),
340+
)
341+
is TunnelState.Connecting -> null
342+
is TunnelState.Disconnected ->
343+
Marker(
344+
location.toLatLong(),
345+
colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.error)
346+
)
347+
is TunnelState.Disconnecting -> null
348+
is TunnelState.Error -> null
286349
}
287350
}
351+
352+
@Composable
353+
fun TunnelState.topBarColor(): Color =
354+
if (isSecured()) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.error
355+
356+
@Composable
357+
fun TunnelState.iconTintColor(): Color =
358+
if (isSecured()) {
359+
MaterialTheme.colorScheme.onPrimary
360+
} else {
361+
MaterialTheme.colorScheme.onError
362+
}
363+
.copy(alpha = AlphaTopBar)
364+
365+
fun GeoIpLocation.toLatLong() =
366+
LatLong(Latitude(latitude.toFloat()), Longitude(longitude.toFloat()))

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt

+8-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@ data class ConnectUiState(
1717
val inAppNotification: InAppNotification?,
1818
val deviceName: String?,
1919
val daysLeftUntilExpiry: Int?,
20-
val isPlayBuild: Boolean
20+
val isPlayBuild: Boolean,
2121
) {
22+
23+
val showLocationInfo: Boolean =
24+
tunnelRealState !is TunnelState.Disconnected && location?.hostname != null
25+
val showLoading =
26+
tunnelRealState is TunnelState.Connecting || tunnelRealState is TunnelState.Disconnecting
27+
2228
companion object {
2329
val INITIAL =
2430
ConnectUiState(
@@ -32,7 +38,7 @@ data class ConnectUiState(
3238
inAppNotification = null,
3339
deviceName = null,
3440
daysLeftUntilExpiry = null,
35-
isPlayBuild = false
41+
isPlayBuild = false,
3642
)
3743
}
3844
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/AnimationConstant.kt

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package net.mullvad.mullvadvpn.constant
22

33
import androidx.compose.animation.core.Spring
4+
import net.mullvad.mullvadvpn.model.LatLong
5+
import net.mullvad.mullvadvpn.model.Latitude
6+
import net.mullvad.mullvadvpn.model.Longitude
47

58
const val MINIMUM_LOADING_TIME_MILLIS = 500L
69

@@ -9,3 +12,10 @@ const val SCREEN_ANIMATION_TIME_MILLIS = Spring.StiffnessMediumLow.toInt()
912
const val HORIZONTAL_SLIDE_FACTOR = 1 / 3f
1013

1114
fun Int.withHorizontalScalingFactor(): Int = (this * HORIZONTAL_SLIDE_FACTOR).toInt()
15+
16+
const val SECURE_ZOOM = 1.15f
17+
const val UNSECURE_ZOOM = 1.20f
18+
const val SECURE_ZOOM_ANIMATION_MILLIS = 2000
19+
20+
// Location of Gothenburg, Sweden
21+
val fallbackLatLong = LatLong(Latitude(57.7065f), Longitude(11.967f))

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class ConnectViewModel(
129129
inAppNotification = notifications.firstOrNull(),
130130
deviceName = deviceName,
131131
daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow(),
132-
isPlayBuild = isPlayBuild
132+
isPlayBuild = isPlayBuild,
133133
)
134134
}
135135
}

0 commit comments

Comments
 (0)