Skip to content

Commit d42287a

Browse files
committed
Merge branch 'android-gl-maps'
2 parents d0b4981 + db41792 commit d42287a

File tree

36 files changed

+1641
-60
lines changed

36 files changed

+1641
-60
lines changed

android/app/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ dependencies {
320320
implementation(project(Dependencies.Mullvad.talpidLib))
321321
implementation(project(Dependencies.Mullvad.themeLib))
322322
implementation(project(Dependencies.Mullvad.paymentLib))
323+
implementation(project(Dependencies.Mullvad.mapLib))
323324

324325
// Play implementation
325326
playImplementation(project(Dependencies.Mullvad.billingLib))

android/app/src/main/AndroidManifest.xml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
android:required="false" />
1818
<uses-feature android:name="android.software.leanback"
1919
android:required="false" />
20+
<uses-feature android:glEsVersion="0x00020000"
21+
android:required="false" />
2022
<application android:label="@string/app_name"
2123
android:icon="@mipmap/ic_launcher"
2224
android:roundIcon="@mipmap/ic_launcher"

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

+129-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,28 @@ 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
77+
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
6178
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
6279
import net.mullvad.mullvadvpn.lib.theme.color.AlphaTopBar
80+
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
81+
import net.mullvad.mullvadvpn.model.GeoIpLocation
82+
import net.mullvad.mullvadvpn.model.LatLong
83+
import net.mullvad.mullvadvpn.model.Latitude
84+
import net.mullvad.mullvadvpn.model.Longitude
6385
import net.mullvad.mullvadvpn.model.TunnelState
6486
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
6587
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
66-
import net.mullvad.talpid.tunnel.ActionAfterDisconnect
6788
import org.koin.androidx.compose.koinViewModel
6889

6990
private const val CONNECT_BUTTON_THROTTLE_MILLIS = 1000
@@ -164,66 +185,80 @@ fun ConnectScreen(
164185
}
165186

166187
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),
188+
topBarColor = uiState.tunnelUiState.topBarColor(),
189+
iconTintColor = uiState.tunnelUiState.iconTintColor(),
180190
onSettingsClicked = onSettingsClick,
181191
onAccountClicked = onAccountClick,
182192
deviceName = uiState.deviceName,
183193
timeLeft = uiState.daysLeftUntilExpiry
184194
) {
195+
var progressIndicatorBias by remember { mutableFloatStateOf(0f) }
196+
197+
// Distance to marker when secure/unsecure
198+
val baseZoom =
199+
animateFloatAsState(
200+
targetValue =
201+
if (uiState.tunnelRealState is TunnelState.Connected) SECURE_ZOOM
202+
else UNSECURE_ZOOM,
203+
animationSpec = tween(SECURE_ZOOM_ANIMATION_MILLIS),
204+
label = "baseZoom"
205+
)
206+
207+
val markers =
208+
uiState.tunnelRealState.toMarker(uiState.location)?.let { listOf(it) } ?: emptyList()
209+
210+
AnimatedMap(
211+
modifier = Modifier.padding(top = it.calculateTopPadding()),
212+
cameraLocation = uiState.location?.toLatLong() ?: fallbackLatLong,
213+
cameraBaseZoom = baseZoom.value,
214+
cameraVerticalBias = progressIndicatorBias,
215+
markers = markers,
216+
globeColors =
217+
GlobeColors(
218+
landColor = MaterialTheme.colorScheme.primary,
219+
oceanColor = MaterialTheme.colorScheme.secondary,
220+
)
221+
)
222+
185223
Column(
186224
verticalArrangement = Arrangement.Bottom,
187225
horizontalAlignment = Alignment.Start,
188226
modifier =
189-
Modifier.background(color = MaterialTheme.colorScheme.primary)
190-
.padding(it)
227+
Modifier.animateContentSize()
228+
.padding(top = it.calculateTopPadding())
191229
.fillMaxHeight()
192230
.drawVerticalScrollbar(
193231
scrollState,
194232
color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaScrollbar)
195233
)
196234
.verticalScroll(scrollState)
197-
.padding(bottom = Dimens.screenVerticalMargin)
198235
.testTag(SCROLLABLE_COLUMN_TEST_TAG)
199236
) {
200-
NotificationBanner(
201-
notification = uiState.inAppNotification,
202-
isPlayBuild = uiState.isPlayBuild,
203-
onClickUpdateVersion = onUpdateVersionClick,
204-
onClickShowAccount = onManageAccountClick,
205-
onClickDismissNewDevice = onDismissNewDeviceClick,
237+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
238+
MullvadCircularProgressIndicatorLarge(
239+
color = MaterialTheme.colorScheme.onPrimary,
240+
modifier =
241+
Modifier.animateContentSize()
242+
.padding(
243+
start = Dimens.sideMargin,
244+
end = Dimens.sideMargin,
245+
top = Dimens.mediumPadding
246+
)
247+
.alpha(if (uiState.showLoading) AlphaVisible else AlphaInvisible)
248+
.align(Alignment.CenterHorizontally)
249+
.testTag(CIRCULAR_PROGRESS_INDICATOR)
250+
.onGloballyPositioned {
251+
val offsetY = it.positionInParent().y + it.size.height / 2
252+
it.parentLayoutCoordinates?.let {
253+
val parentHeight = it.size.height
254+
val verticalBias = offsetY / parentHeight
255+
if (verticalBias.isFinite()) {
256+
progressIndicatorBias = verticalBias
257+
}
258+
}
259+
}
206260
)
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))
261+
Spacer(modifier = Modifier.defaultMinSize(minHeight = Dimens.mediumPadding).weight(1f))
227262
ConnectionStatusText(
228263
state = uiState.tunnelRealState,
229264
modifier = Modifier.padding(horizontal = Dimens.sideMargin)
@@ -243,9 +278,7 @@ fun ConnectScreen(
243278
var expanded by rememberSaveable { mutableStateOf(false) }
244279
LocationInfo(
245280
onToggleTunnelInfo = { expanded = !expanded },
246-
isVisible =
247-
uiState.tunnelRealState !is TunnelState.Disconnected &&
248-
uiState.location?.hostname != null,
281+
isVisible = uiState.showLocationInfo,
249282
isExpanded = expanded,
250283
location = uiState.location,
251284
inAddress = uiState.inAddress,
@@ -282,6 +315,55 @@ fun ConnectScreen(
282315
connectClick = { handleThrottledAction(onConnectClick) },
283316
reconnectButtonTestTag = RECONNECT_BUTTON_TEST_TAG
284317
)
318+
// We need to manually add this padding so we align size with the map
319+
// component and marker with the progress indicator.
320+
Spacer(modifier = Modifier.height(it.calculateBottomPadding()))
285321
}
322+
323+
NotificationBanner(
324+
modifier = Modifier.padding(top = it.calculateTopPadding()),
325+
notification = uiState.inAppNotification,
326+
isPlayBuild = uiState.isPlayBuild,
327+
onClickUpdateVersion = onUpdateVersionClick,
328+
onClickShowAccount = onManageAccountClick,
329+
onClickDismissNewDevice = onDismissNewDeviceClick,
330+
)
331+
}
332+
}
333+
334+
@Composable
335+
fun TunnelState.toMarker(location: GeoIpLocation?): Marker? {
336+
if (location == null) return null
337+
return when (this) {
338+
is TunnelState.Connected ->
339+
Marker(
340+
location.toLatLong(),
341+
colors =
342+
LocationMarkerColors(centerColor = MaterialTheme.colorScheme.inversePrimary),
343+
)
344+
is TunnelState.Connecting -> null
345+
is TunnelState.Disconnected ->
346+
Marker(
347+
location.toLatLong(),
348+
colors = LocationMarkerColors(centerColor = MaterialTheme.colorScheme.error)
349+
)
350+
is TunnelState.Disconnecting -> null
351+
is TunnelState.Error -> null
286352
}
287353
}
354+
355+
@Composable
356+
fun TunnelState.topBarColor(): Color =
357+
if (isSecured()) MaterialTheme.colorScheme.inversePrimary else MaterialTheme.colorScheme.error
358+
359+
@Composable
360+
fun TunnelState.iconTintColor(): Color =
361+
if (isSecured()) {
362+
MaterialTheme.colorScheme.onPrimary
363+
} else {
364+
MaterialTheme.colorScheme.onError
365+
}
366+
.copy(alpha = AlphaTopBar)
367+
368+
fun GeoIpLocation.toLatLong() =
369+
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
}

android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,9 @@ class ConnectViewModelTest {
209209
ipv6 = mockk(relaxed = true),
210210
country = "Sweden",
211211
city = "Gothenburg",
212-
hostname = "Host"
212+
hostname = "Host",
213+
latitude = 57.7065,
214+
longitude = 11.967
213215
)
214216

215217
// Act, Assert

android/buildSrc/src/main/kotlin/Dependencies.kt

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ object Dependencies {
9999
const val commonTestLib = ":lib:common-test"
100100
const val billingLib = ":lib:billing"
101101
const val paymentLib = ":lib:payment"
102+
const val mapLib = ":lib:map"
102103
}
103104

104105
object Plugin {

0 commit comments

Comments
 (0)