Skip to content

Commit

Permalink
Merge branch 'android-gl-maps'
Browse files Browse the repository at this point in the history
  • Loading branch information
albin-mullvad committed Feb 15, 2024
2 parents d0b4981 + db41792 commit d42287a
Show file tree
Hide file tree
Showing 36 changed files with 1,641 additions and 60 deletions.
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ dependencies {
implementation(project(Dependencies.Mullvad.talpidLib))
implementation(project(Dependencies.Mullvad.themeLib))
implementation(project(Dependencies.Mullvad.paymentLib))
implementation(project(Dependencies.Mullvad.mapLib))

// Play implementation
playImplementation(project(Dependencies.Mullvad.billingLib))
Expand Down
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
android:required="false" />
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:glEsVersion="0x00020000"
android:required="false" />
<application android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private fun PreviewNotificationBanner() {

@Composable
fun NotificationBanner(
modifier: Modifier,
notification: InAppNotification?,
isPlayBuild: Boolean,
onClickUpdateVersion: () -> Unit,
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,13 +21,18 @@ 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
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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()))
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -32,7 +38,7 @@ data class ConnectUiState(
inAppNotification = null,
deviceName = null,
daysLeftUntilExpiry = null,
isPlayBuild = false
isPlayBuild = false,
)
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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))
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class ConnectViewModel(
inAppNotification = notifications.firstOrNull(),
deviceName = deviceName,
daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow(),
isPlayBuild = isPlayBuild
isPlayBuild = isPlayBuild,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,9 @@ class ConnectViewModelTest {
ipv6 = mockk(relaxed = true),
country = "Sweden",
city = "Gothenburg",
hostname = "Host"
hostname = "Host",
latitude = 57.7065,
longitude = 11.967
)

// Act, Assert
Expand Down
1 change: 1 addition & 0 deletions android/buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ object Dependencies {
const val commonTestLib = ":lib:common-test"
const val billingLib = ":lib:billing"
const val paymentLib = ":lib:payment"
const val mapLib = ":lib:map"
}

object Plugin {
Expand Down
Loading

0 comments on commit d42287a

Please sign in to comment.