Skip to content

Commit 175e3f3

Browse files
committed
Integrate map into ConnectScreen
1 parent 4d327de commit 175e3f3

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

+123-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,85 @@ 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+
221+
NotificationBanner(
222+
modifier = Modifier.padding(top = it.calculateTopPadding()),
223+
notification = uiState.inAppNotification,
224+
isPlayBuild = uiState.isPlayBuild,
225+
onClickUpdateVersion = onUpdateVersionClick,
226+
onClickShowAccount = onManageAccountClick,
227+
onClickDismissNewDevice = onDismissNewDeviceClick,
228+
)
229+
185230
Column(
186231
verticalArrangement = Arrangement.Bottom,
187232
horizontalAlignment = Alignment.Start,
188233
modifier =
189-
Modifier.background(color = MaterialTheme.colorScheme.primary)
190-
.padding(it)
234+
Modifier.animateContentSize()
235+
.padding(top = it.calculateTopPadding())
191236
.fillMaxHeight()
192237
.drawVerticalScrollbar(
193238
scrollState,
194239
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar)
195240
)
196241
.verticalScroll(scrollState)
197-
.padding(bottom = Dimens.screenVerticalMargin)
198242
.testTag(SCROLLABLE_COLUMN_TEST_TAG)
199243
) {
200-
NotificationBanner(
201-
notification = uiState.inAppNotification,
202-
isPlayBuild = uiState.isPlayBuild,
203-
onClickUpdateVersion = onUpdateVersionClick,
204-
onClickShowAccount = onManageAccountClick,
205-
onClickDismissNewDevice = onDismissNewDeviceClick,
244+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
245+
MullvadCircularProgressIndicatorLarge(
246+
color = MaterialTheme.colorScheme.onPrimary,
247+
modifier =
248+
Modifier.animateContentSize()
249+
.padding(
250+
start = Dimens.sideMargin,
251+
end = Dimens.sideMargin,
252+
top = Dimens.mediumPadding
253+
)
254+
.alpha(if (uiState.showLoading) 1f else 0f)
255+
.align(Alignment.CenterHorizontally)
256+
.testTag(CIRCULAR_PROGRESS_INDICATOR)
257+
.onGloballyPositioned {
258+
val offsetY = it.positionInParent().y + it.size.height / 2
259+
it.parentLayoutCoordinates?.let {
260+
progressIndicatorBias = offsetY / it.size.height.toFloat()
261+
}
262+
}
206263
)
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))
264+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
227265
ConnectionStatusText(
228266
state = uiState.tunnelRealState,
229267
modifier = Modifier.padding(horizontal = Dimens.sideMargin)
@@ -243,9 +281,7 @@ fun ConnectScreen(
243281
var expanded by rememberSaveable { mutableStateOf(false) }
244282
LocationInfo(
245283
onToggleTunnelInfo = { expanded = !expanded },
246-
isVisible =
247-
uiState.tunnelRealState !is TunnelState.Disconnected &&
248-
uiState.location?.hostname != null,
284+
isVisible = uiState.showLocationInfo,
249285
isExpanded = expanded,
250286
location = uiState.location,
251287
inAddress = uiState.inAddress,
@@ -282,6 +318,46 @@ fun ConnectScreen(
282318
connectClick = { handleThrottledAction(onConnectClick) },
283319
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
284320
)
321+
// We need to manually add this padding so we align size with the map
322+
// component and marker with the progress indicator.
323+
Spacer(modifier = Modifier.height(it.calculateBottomPadding()))
285324
}
286325
}
287326
}
327+
328+
@Composable
329+
fun TunnelState.toMarker(location: GeoIpLocation?): Marker? {
330+
if (location == null) return null
331+
return when (this) {
332+
is TunnelState.Connected ->
333+
Marker(
334+
location.toLatLong(),
335+
colors =
336+
LocationMarkerColors(centerColor = MaterialTheme.colorScheme.inversePrimary),
337+
)
338+
is TunnelState.Connecting -> null
339+
is TunnelState.Disconnected ->
340+
Marker(
341+
location.toLatLong(),
342+
colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.error)
343+
)
344+
is TunnelState.Disconnecting -> null
345+
is TunnelState.Error -> null
346+
}
347+
}
348+
349+
@Composable
350+
fun TunnelState.topBarColor(): Color =
351+
if (isSecured()) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.error
352+
353+
@Composable
354+
fun TunnelState.iconTintColor(): Color =
355+
if (isSecured()) {
356+
MaterialTheme.colorScheme.onPrimary
357+
} else {
358+
MaterialTheme.colorScheme.onError
359+
}
360+
.copy(alpha = AlphaTopBar)
361+
362+
fun GeoIpLocation.toLatLong() =
363+
LatLong(Latitude(latitude.toFloat()), Longitude(longitude.toFloat()))

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

+10-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ data class ConnectUiState(
1717
val inAppNotification: InAppNotification?,
1818
val deviceName: String?,
1919
val daysLeftUntilExpiry: Int?,
20-
val isPlayBuild: Boolean
20+
val isPlayBuild: Boolean,
21+
val animateMap: Boolean
2122
) {
23+
24+
val showLocationInfo: Boolean =
25+
tunnelRealState !is TunnelState.Disconnected && location?.hostname != null
26+
val showLoading =
27+
tunnelRealState is TunnelState.Connecting || tunnelRealState is TunnelState.Disconnecting
28+
2229
companion object {
2330
val INITIAL =
2431
ConnectUiState(
@@ -32,7 +39,8 @@ data class ConnectUiState(
3239
inAppNotification = null,
3340
deviceName = null,
3441
daysLeftUntilExpiry = null,
35-
isPlayBuild = false
42+
isPlayBuild = false,
43+
animateMap = true
3644
)
3745
}
3846
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ class ConnectViewModel(
129129
inAppNotification = notifications.firstOrNull(),
130130
deviceName = deviceName,
131131
daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow(),
132-
isPlayBuild = isPlayBuild
132+
isPlayBuild = isPlayBuild,
133+
animateMap = true
133134
)
134135
}
135136
}

0 commit comments

Comments
 (0)