Merge branch 'improve-tv-ui-experience'
This commit is contained in:
commit
22fc75dc52
@ -29,6 +29,8 @@ Line wrap the file at 100 chars. Th
|
||||
### Changed
|
||||
- Disable Wireguard port setting when a obfuscation is selected since it is not used when an
|
||||
obfuscation is applied.
|
||||
- Adapt UI on Connect Screen for Android TV, including a navigation rail and redesigned in-app
|
||||
notification bar.
|
||||
|
||||
### Removed
|
||||
- Remove Google's resolvers from encrypted DNS proxy.
|
||||
|
@ -372,6 +372,8 @@ dependencies {
|
||||
implementation(projects.lib.resource)
|
||||
implementation(projects.lib.shared)
|
||||
implementation(projects.lib.talpid)
|
||||
implementation(projects.lib.tv)
|
||||
implementation(projects.lib.ui.component)
|
||||
implementation(projects.tile)
|
||||
implementation(projects.lib.theme)
|
||||
implementation(projects.service)
|
||||
@ -388,6 +390,7 @@ dependencies {
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.tv)
|
||||
implementation(libs.arrow)
|
||||
implementation(libs.arrow.optics)
|
||||
implementation(libs.arrow.resilience)
|
||||
|
@ -19,8 +19,6 @@ import net.mullvad.mullvadvpn.compose.state.ConnectUiState
|
||||
import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR
|
||||
import net.mullvad.mullvadvpn.compose.test.CONNECT_BUTTON_TEST_TAG
|
||||
import net.mullvad.mullvadvpn.compose.test.CONNECT_CARD_HEADER_TEST_TAG
|
||||
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION
|
||||
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_TEXT_ACTION
|
||||
import net.mullvad.mullvadvpn.compose.test.RECONNECT_BUTTON_TEST_TAG
|
||||
import net.mullvad.mullvadvpn.compose.test.SELECT_LOCATION_BUTTON_TEST_TAG
|
||||
import net.mullvad.mullvadvpn.compose.test.TOP_BAR_ACCOUNT_BUTTON
|
||||
@ -28,11 +26,13 @@ import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
|
||||
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.TransportProtocol
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelState
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.test.NOTIFICATION_BANNER_ACTION
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.test.NOTIFICATION_BANNER_TEXT_ACTION
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -1,49 +1,25 @@
|
||||
package net.mullvad.mullvadvpn.compose.component.notificationbanner
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import java.time.Duration
|
||||
import net.mullvad.mullvadvpn.compose.component.MullvadTopBar
|
||||
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER
|
||||
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_ACTION
|
||||
import net.mullvad.mullvadvpn.compose.test.NOTIFICATION_BANNER_TEXT_ACTION
|
||||
import net.mullvad.mullvadvpn.compose.util.rememberPrevious
|
||||
import net.mullvad.mullvadvpn.compose.util.isTv
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.theme.AppTheme
|
||||
import net.mullvad.mullvadvpn.lib.theme.Dimens
|
||||
import net.mullvad.mullvadvpn.lib.theme.color.warning
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.ui.notification.StatusLevel
|
||||
import net.mullvad.mullvadvpn.lib.tv.NotificationBannerTv
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.AnimatedNotificationBanner
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@ -52,18 +28,17 @@ private fun PreviewNotificationBanner() {
|
||||
Column(Modifier.background(color = MaterialTheme.colorScheme.surface)) {
|
||||
val bannerDataList =
|
||||
listOf(
|
||||
InAppNotification.UnsupportedVersion(
|
||||
versionInfo = VersionInfo(currentVersion = "1.0", isSupported = false)
|
||||
),
|
||||
InAppNotification.AccountExpiry(expiry = Duration.ZERO),
|
||||
InAppNotification.TunnelStateBlocked,
|
||||
InAppNotification.NewDevice("Courageous Turtle"),
|
||||
InAppNotification.TunnelStateError(
|
||||
error = ErrorState(ErrorStateCause.FirewallPolicyError.Generic, true)
|
||||
),
|
||||
InAppNotification.NewVersionChangelog,
|
||||
)
|
||||
.map { it.toNotificationData(false, {}, {}, {}, {}, {}) }
|
||||
InAppNotification.UnsupportedVersion(
|
||||
versionInfo = VersionInfo(currentVersion = "1.0", isSupported = false)
|
||||
),
|
||||
InAppNotification.AccountExpiry(expiry = Duration.ZERO),
|
||||
InAppNotification.TunnelStateBlocked,
|
||||
InAppNotification.NewDevice("Courageous Turtle"),
|
||||
InAppNotification.TunnelStateError(
|
||||
error = ErrorState(ErrorStateCause.FirewallPolicyError.Generic, true)
|
||||
),
|
||||
InAppNotification.NewVersionChangelog,
|
||||
)
|
||||
|
||||
bannerDataList.forEach {
|
||||
MullvadTopBar(
|
||||
@ -72,7 +47,15 @@ private fun PreviewNotificationBanner() {
|
||||
onAccountClicked = {},
|
||||
iconTintColor = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Notification(it)
|
||||
NotificationBanner(
|
||||
notification = it,
|
||||
isPlayBuild = false,
|
||||
openAppListing = {},
|
||||
onClickShowAccount = {},
|
||||
onClickShowChangelog = {},
|
||||
onClickDismissChangelog = {},
|
||||
onClickDismissNewDevice = {},
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
@ -90,163 +73,28 @@ fun NotificationBanner(
|
||||
onClickDismissChangelog: () -> Unit,
|
||||
onClickDismissNewDevice: () -> Unit,
|
||||
) {
|
||||
// Fix for animating to invisible state
|
||||
val previous = rememberPrevious(current = notification, shouldUpdate = { _, _ -> true })
|
||||
AnimatedVisibility(
|
||||
visible = notification != null,
|
||||
enter = slideInVertically(initialOffsetY = { -it }),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }),
|
||||
modifier = modifier,
|
||||
) {
|
||||
val visibleNotification = notification ?: previous
|
||||
if (visibleNotification != null)
|
||||
Notification(
|
||||
visibleNotification.toNotificationData(
|
||||
isPlayBuild = isPlayBuild,
|
||||
openAppListing,
|
||||
onClickShowAccount,
|
||||
onClickShowChangelog,
|
||||
onClickDismissChangelog,
|
||||
onClickDismissNewDevice,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun Notification(notificationBannerData: NotificationData) {
|
||||
val (title, message, statusLevel, action) = notificationBannerData
|
||||
ConstraintLayout(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(
|
||||
start = Dimens.notificationBannerStartPadding,
|
||||
end = Dimens.notificationBannerEndPadding,
|
||||
top = Dimens.smallPadding,
|
||||
bottom = Dimens.smallPadding,
|
||||
)
|
||||
.animateContentSize()
|
||||
.testTag(NOTIFICATION_BANNER)
|
||||
) {
|
||||
val (status, textTitle, textMessage, actionIcon) = createRefs()
|
||||
NotificationDot(
|
||||
statusLevel,
|
||||
Modifier.constrainAs(status) {
|
||||
top.linkTo(textTitle.top)
|
||||
start.linkTo(parent.start)
|
||||
bottom.linkTo(textTitle.bottom)
|
||||
},
|
||||
if (isTv()) {
|
||||
NotificationBannerTv(
|
||||
modifier = modifier,
|
||||
notification = notification,
|
||||
isPlayBuild = isPlayBuild,
|
||||
openAppListing = openAppListing,
|
||||
onClickShowAccount = onClickShowAccount,
|
||||
onClickShowChangelog = onClickShowChangelog,
|
||||
onClickDismissChangelog = onClickDismissChangelog,
|
||||
onClickDismissNewDevice = onClickDismissNewDevice,
|
||||
)
|
||||
Text(
|
||||
text = title.toUpperCase(),
|
||||
modifier =
|
||||
Modifier.constrainAs(textTitle) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(status.end)
|
||||
if (message != null) {
|
||||
bottom.linkTo(textMessage.top)
|
||||
} else {
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
if (action != null) {
|
||||
end.linkTo(actionIcon.start)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.padding(start = Dimens.smallPadding),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
message?.let { message ->
|
||||
Text(
|
||||
text = message.text,
|
||||
modifier =
|
||||
Modifier.constrainAs(textMessage) {
|
||||
top.linkTo(textTitle.bottom)
|
||||
start.linkTo(textTitle.start)
|
||||
if (action != null) {
|
||||
end.linkTo(actionIcon.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
width = Dimension.fillToConstraints
|
||||
height = Dimension.wrapContent
|
||||
}
|
||||
.padding(start = Dimens.smallPadding, top = Dimens.tinyPadding)
|
||||
.wrapContentWidth(Alignment.Start)
|
||||
.let {
|
||||
if (message is NotificationMessage.ClickableText) {
|
||||
it.clickable(
|
||||
onClickLabel = message.contentDescription,
|
||||
role = Role.Button,
|
||||
) {
|
||||
message.onClick()
|
||||
}
|
||||
.testTag(NOTIFICATION_BANNER_TEXT_ACTION)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
action?.let {
|
||||
NotificationAction(
|
||||
it.icon,
|
||||
onClick = it.onClick,
|
||||
contentDescription = it.contentDescription,
|
||||
modifier =
|
||||
Modifier.constrainAs(actionIcon) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationDot(statusLevel: StatusLevel, modifier: Modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.background(
|
||||
color =
|
||||
when (statusLevel) {
|
||||
StatusLevel.Error -> MaterialTheme.colorScheme.error
|
||||
StatusLevel.Warning -> MaterialTheme.colorScheme.warning
|
||||
StatusLevel.Info -> MaterialTheme.colorScheme.tertiary
|
||||
},
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(Dimens.notificationStatusIconSize)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationAction(
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
IconButton(modifier = modifier.testTag(NOTIFICATION_BANNER_ACTION), onClick = onClick) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(Dimens.notificationIconPadding),
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
} else {
|
||||
AnimatedNotificationBanner(
|
||||
modifier = modifier,
|
||||
notificationModifier = Modifier.fillMaxWidth(),
|
||||
notification = notification,
|
||||
isPlayBuild = isPlayBuild,
|
||||
openAppListing = openAppListing,
|
||||
onClickShowAccount = onClickShowAccount,
|
||||
onClickShowChangelog = onClickShowChangelog,
|
||||
onClickDismissChangelog = onClickDismissChangelog,
|
||||
onClickDismissNewDevice = onClickDismissNewDevice,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,11 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||
import com.ramcosta.composedestinations.spec.DestinationStyle
|
||||
import net.mullvad.mullvadvpn.R
|
||||
import net.mullvad.mullvadvpn.compose.component.textResource
|
||||
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
|
||||
import net.mullvad.mullvadvpn.compose.preview.DevicePreviewParameterProvider
|
||||
import net.mullvad.mullvadvpn.lib.model.Device
|
||||
import net.mullvad.mullvadvpn.lib.model.DeviceId
|
||||
import net.mullvad.mullvadvpn.lib.theme.AppTheme
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.toAnnotatedString
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -24,10 +24,10 @@ import androidx.core.text.HtmlCompat
|
||||
import net.mullvad.mullvadvpn.R
|
||||
import net.mullvad.mullvadvpn.compose.button.PrimaryButton
|
||||
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
|
||||
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
|
||||
import net.mullvad.mullvadvpn.lib.theme.AppTheme
|
||||
import net.mullvad.mullvadvpn.lib.theme.Dimens
|
||||
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.toAnnotatedString
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.mullvad.mullvadvpn.compose.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.viewmodel.AppInfoUiState
|
||||
|
||||
class AppInfoUiStatePreviewParameterProvider : PreviewParameterProvider<AppInfoUiState> {
|
||||
|
@ -5,6 +5,7 @@ import java.net.InetAddress
|
||||
import net.mullvad.mullvadvpn.compose.state.ConnectUiState
|
||||
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
|
||||
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
|
||||
class ConnectUiStatePreviewParameterProvider : PreviewParameterProvider<ConnectUiState> {
|
||||
override val values = sequenceOf(ConnectUiState.INITIAL) + generateOtherStates()
|
||||
@ -29,7 +30,7 @@ private fun generateOtherStates(): Sequence<ConnectUiState> =
|
||||
),
|
||||
TunnelStatePreviewData.generateErrorState(isBlocking = true),
|
||||
)
|
||||
.map { state ->
|
||||
.mapIndexed { index, state ->
|
||||
ConnectUiState(
|
||||
location =
|
||||
GeoIpLocation(
|
||||
@ -45,7 +46,8 @@ private fun generateOtherStates(): Sequence<ConnectUiState> =
|
||||
selectedRelayItemTitle = "Relay Title",
|
||||
tunnelState = state,
|
||||
showLocation = true,
|
||||
inAppNotification = null,
|
||||
inAppNotification =
|
||||
if (index == 0) InAppNotification.NewDevice("Test Device") else null,
|
||||
deviceName = "Cool Beans",
|
||||
daysLeftUntilExpiry = 42,
|
||||
isPlayBuild = true,
|
||||
|
@ -54,13 +54,13 @@ import kotlinx.coroutines.launch
|
||||
import net.mullvad.mullvadvpn.R
|
||||
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
|
||||
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndButton
|
||||
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
|
||||
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
|
||||
import net.mullvad.mullvadvpn.lib.common.util.openVpnSettings
|
||||
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.AlphaVisible
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.toAnnotatedString
|
||||
import net.mullvad.mullvadvpn.service.constant.IS_PLAY_BUILD
|
||||
import net.mullvad.mullvadvpn.util.appendHideNavOnPlayBuild
|
||||
|
||||
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
@ -25,6 +26,8 @@ import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -71,6 +74,7 @@ import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton
|
||||
import net.mullvad.mullvadvpn.compose.component.ConnectionStatusText
|
||||
import net.mullvad.mullvadvpn.compose.component.ExpandChevron
|
||||
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
|
||||
import net.mullvad.mullvadvpn.compose.component.MullvadSnackbar
|
||||
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName
|
||||
import net.mullvad.mullvadvpn.compose.component.connectioninfo.ConnectionDetailPanel
|
||||
import net.mullvad.mullvadvpn.compose.component.connectioninfo.FeatureIndicatorsPanel
|
||||
@ -89,6 +93,7 @@ import net.mullvad.mullvadvpn.compose.transitions.HomeTransition
|
||||
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
|
||||
import net.mullvad.mullvadvpn.compose.util.CreateVpnProfile
|
||||
import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
|
||||
import net.mullvad.mullvadvpn.compose.util.isTv
|
||||
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
|
||||
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM
|
||||
import net.mullvad.mullvadvpn.constant.SECURE_ZOOM_ANIMATION_MILLIS
|
||||
@ -115,6 +120,7 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
|
||||
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
|
||||
import net.mullvad.mullvadvpn.lib.theme.typeface.connectionStatus
|
||||
import net.mullvad.mullvadvpn.lib.theme.typeface.hostname
|
||||
import net.mullvad.mullvadvpn.lib.tv.NavigationDrawerTv
|
||||
import net.mullvad.mullvadvpn.util.removeHtmlTags
|
||||
import net.mullvad.mullvadvpn.viewmodel.ConnectViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
@ -267,71 +273,130 @@ fun ConnectScreen(
|
||||
onAccountClick: () -> Unit,
|
||||
onDismissNewDeviceClick: () -> Unit,
|
||||
) {
|
||||
|
||||
ScaffoldWithTopBarAndDeviceName(
|
||||
topBarColor = state.tunnelState.topBarColor(),
|
||||
iconTintColor = state.tunnelState.iconTintColor(),
|
||||
onSettingsClicked = onSettingsClick,
|
||||
onAccountClicked = onAccountClick,
|
||||
deviceName = state.deviceName,
|
||||
timeLeft = state.daysLeftUntilExpiry,
|
||||
snackbarHostState = snackbarHostState,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val indicatorPercentOffset =
|
||||
if (screenHeight < SCREEN_HEIGHT_THRESHOLD) SHORT_SCREEN_INDICATOR_BIAS
|
||||
else TALL_SCREEN_INDICATOR_BIAS
|
||||
|
||||
Box(
|
||||
Modifier.padding(
|
||||
top = it.calculateTopPadding(),
|
||||
start = it.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = it.calculateEndPadding(LocalLayoutDirection.current),
|
||||
)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
MullvadMap(state, indicatorPercentOffset)
|
||||
|
||||
MullvadCircularProgressIndicatorLarge(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier =
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.placeRelative(
|
||||
x = (constraints.maxWidth * 0.5f - placeable.width / 2).toInt(),
|
||||
y =
|
||||
(constraints.maxHeight * indicatorPercentOffset -
|
||||
placeable.height / 2)
|
||||
.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.alpha(if (state.showLoading) AlphaVisible else AlphaInvisible)
|
||||
.testTag(CIRCULAR_PROGRESS_INDICATOR),
|
||||
val content =
|
||||
@Composable { padding: PaddingValues ->
|
||||
Content(
|
||||
padding,
|
||||
state,
|
||||
onDisconnectClick,
|
||||
onReconnectClick,
|
||||
onConnectClick,
|
||||
onCancelClick,
|
||||
onSwitchLocationClick,
|
||||
onOpenAppListing,
|
||||
onManageAccountClick,
|
||||
onChangelogClick,
|
||||
onDismissChangelogClick,
|
||||
onDismissNewDeviceClick,
|
||||
)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().padding(bottom = it.calculateBottomPadding())) {
|
||||
NotificationBanner(
|
||||
notification = state.inAppNotification,
|
||||
isPlayBuild = state.isPlayBuild,
|
||||
openAppListing = onOpenAppListing,
|
||||
onClickShowAccount = onManageAccountClick,
|
||||
onClickShowChangelog = onChangelogClick,
|
||||
onClickDismissChangelog = onDismissChangelogClick,
|
||||
onClickDismissNewDevice = onDismissNewDeviceClick,
|
||||
)
|
||||
ConnectionCard(
|
||||
state = state,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
onSwitchLocationClick = onSwitchLocationClick,
|
||||
onDisconnectClick = onDisconnectClick,
|
||||
onReconnectClick = onReconnectClick,
|
||||
onCancelClick = onCancelClick,
|
||||
onConnectClick = onConnectClick,
|
||||
if (isTv()) {
|
||||
Scaffold(
|
||||
snackbarHost = {
|
||||
SnackbarHost(
|
||||
snackbarHostState,
|
||||
snackbar = { snackbarData -> MullvadSnackbar(snackbarData = snackbarData) },
|
||||
)
|
||||
}
|
||||
) {
|
||||
NavigationDrawerTv(
|
||||
daysLeftUntilExpiry = state.daysLeftUntilExpiry,
|
||||
deviceName = state.deviceName,
|
||||
onSettingsClick = onSettingsClick,
|
||||
onAccountClick = onAccountClick,
|
||||
) {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScaffoldWithTopBarAndDeviceName(
|
||||
topBarColor = state.tunnelState.topBarColor(),
|
||||
iconTintColor = state.tunnelState.iconTintColor(),
|
||||
onSettingsClicked = onSettingsClick,
|
||||
onAccountClicked = onAccountClick,
|
||||
deviceName = state.deviceName,
|
||||
timeLeft = state.daysLeftUntilExpiry,
|
||||
snackbarHostState = snackbarHostState,
|
||||
) {
|
||||
content(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
paddingValues: PaddingValues,
|
||||
state: ConnectUiState,
|
||||
onDisconnectClick: () -> Unit,
|
||||
onReconnectClick: () -> Unit,
|
||||
onConnectClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onSwitchLocationClick: () -> Unit,
|
||||
onOpenAppListing: () -> Unit,
|
||||
onManageAccountClick: () -> Unit,
|
||||
onChangelogClick: () -> Unit,
|
||||
onDismissChangelogClick: () -> Unit,
|
||||
onDismissNewDeviceClick: () -> Unit,
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeight = configuration.screenHeightDp.dp
|
||||
val indicatorPercentOffset =
|
||||
if (screenHeight < SCREEN_HEIGHT_THRESHOLD) SHORT_SCREEN_INDICATOR_BIAS
|
||||
else TALL_SCREEN_INDICATOR_BIAS
|
||||
|
||||
Box(
|
||||
Modifier.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
start = paddingValues.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = paddingValues.calculateEndPadding(LocalLayoutDirection.current),
|
||||
)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
MullvadMap(state, indicatorPercentOffset)
|
||||
|
||||
MullvadCircularProgressIndicatorLarge(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier =
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.placeRelative(
|
||||
x = (constraints.maxWidth * 0.5f - placeable.width / 2).toInt(),
|
||||
y =
|
||||
(constraints.maxHeight * indicatorPercentOffset -
|
||||
placeable.height / 2)
|
||||
.toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.alpha(if (state.showLoading) AlphaVisible else AlphaInvisible)
|
||||
.testTag(CIRCULAR_PROGRESS_INDICATOR),
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.fillMaxSize().padding(bottom = paddingValues.calculateBottomPadding())
|
||||
) {
|
||||
NotificationBanner(
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
notification = state.inAppNotification,
|
||||
isPlayBuild = state.isPlayBuild,
|
||||
openAppListing = onOpenAppListing,
|
||||
onClickShowAccount = onManageAccountClick,
|
||||
onClickShowChangelog = onChangelogClick,
|
||||
onClickDismissChangelog = onDismissChangelogClick,
|
||||
onClickDismissNewDevice = onDismissNewDeviceClick,
|
||||
)
|
||||
ConnectionCard(
|
||||
state = state,
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
onSwitchLocationClick = onSwitchLocationClick,
|
||||
onDisconnectClick = onDisconnectClick,
|
||||
onReconnectClick = onReconnectClick,
|
||||
onCancelClick = onCancelClick,
|
||||
onConnectClick = onConnectClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.mullvad.mullvadvpn.compose.state
|
||||
|
||||
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelState
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
|
||||
data class ConnectUiState(
|
||||
val location: GeoIpLocation?,
|
||||
|
@ -49,11 +49,6 @@ const val LOCATION_INFO_TEST_TAG = "location_info_test_tag"
|
||||
const val LOCATION_INFO_CONNECTION_IN_TEST_TAG = "location_info_connection_in_test_tag"
|
||||
const val LOCATION_INFO_CONNECTION_OUT_TEST_TAG = "location_info_connection_out_test_tag"
|
||||
|
||||
// ConnectScreen - Notification banner
|
||||
const val NOTIFICATION_BANNER = "notification_banner"
|
||||
const val NOTIFICATION_BANNER_ACTION = "notification_banner_action"
|
||||
const val NOTIFICATION_BANNER_TEXT_ACTION = "notification_banner_text_action"
|
||||
|
||||
// PlayPayment
|
||||
const val PLAY_PAYMENT_INFO_ICON_TEST_TAG = "play_payment_info_icon_test_tag"
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
package net.mullvad.mullvadvpn.compose.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.booleanResource
|
||||
import net.mullvad.mullvadvpn.R
|
||||
|
||||
@Composable
|
||||
fun isTv(): Boolean {
|
||||
return booleanResource(R.bool.isTv) ||
|
||||
LocalContext.current.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
|
||||
}
|
@ -1,60 +1,16 @@
|
||||
package net.mullvad.mullvadvpn.repository
|
||||
|
||||
import java.time.Duration
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.usecase.AccountExpiryInAppNotificationUseCase
|
||||
import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase
|
||||
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
|
||||
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
|
||||
import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase
|
||||
|
||||
enum class StatusLevel {
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
sealed class InAppNotification {
|
||||
abstract val statusLevel: StatusLevel
|
||||
abstract val priority: Long
|
||||
|
||||
data class TunnelStateError(val error: ErrorState) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data object TunnelStateBlocked : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 1000
|
||||
}
|
||||
|
||||
data class UnsupportedVersion(val versionInfo: VersionInfo) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 999
|
||||
}
|
||||
|
||||
data class AccountExpiry(val expiry: Duration) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Warning
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data class NewDevice(val deviceName: String) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Info
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data object NewVersionChangelog : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Info
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
}
|
||||
|
||||
class InAppNotificationController(
|
||||
accountExpiryInAppNotificationUseCase: AccountExpiryInAppNotificationUseCase,
|
||||
newDeviceNotificationUseCase: NewDeviceNotificationUseCase,
|
||||
|
@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
|
||||
import net.mullvad.mullvadvpn.lib.model.BuildVersion
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
|
||||
class AppVersionInfoRepository(
|
||||
private val buildVersion: BuildVersion,
|
||||
|
@ -5,8 +5,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD
|
||||
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL
|
||||
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.AccountExpiryTicker
|
||||
|
@ -2,8 +2,8 @@ package net.mullvad.mullvadvpn.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.repository.ChangelogRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
|
||||
class NewChangelogNotificationUseCase(private val changelogRepository: ChangelogRepository) {
|
||||
operator fun invoke() =
|
||||
|
@ -3,8 +3,8 @@ package net.mullvad.mullvadvpn.usecase
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.repository.NewDeviceRepository
|
||||
|
||||
class NewDeviceNotificationUseCase(
|
||||
|
@ -4,9 +4,9 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelState
|
||||
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
|
||||
class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProxy) {
|
||||
operator fun invoke(): Flow<List<InAppNotification>> =
|
||||
|
@ -2,8 +2,8 @@ package net.mullvad.mullvadvpn.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
|
||||
|
||||
class VersionNotificationUseCase(
|
||||
|
@ -14,8 +14,8 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mullvad.mullvadvpn.R
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.repository.ChangelogRepository
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
|
||||
|
||||
class AppInfoViewModel(
|
||||
|
@ -15,7 +15,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotificationController
|
||||
import net.mullvad.mullvadvpn.usecase.AccountExpiryInAppNotificationUseCase
|
||||
import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase
|
||||
|
@ -17,8 +17,8 @@ import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
|
||||
import net.mullvad.mullvadvpn.lib.model.AccountData
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_CLOSE_TO_EXPIRY_THRESHOLD
|
||||
import net.mullvad.mullvadvpn.service.notifications.accountexpiry.ACCOUNT_EXPIRY_IN_APP_NOTIFICATION_UPDATE_INTERVAL
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
|
@ -16,8 +16,8 @@ import net.mullvad.mullvadvpn.lib.model.AccountNumber
|
||||
import net.mullvad.mullvadvpn.lib.model.Device
|
||||
import net.mullvad.mullvadvpn.lib.model.DeviceId
|
||||
import net.mullvad.mullvadvpn.lib.model.DeviceState
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.repository.NewDeviceRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
@ -12,9 +12,9 @@ import kotlinx.coroutines.test.runTest
|
||||
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
|
||||
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelState
|
||||
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -10,8 +10,8 @@ import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.model.AccountData
|
||||
import net.mullvad.mullvadvpn.lib.model.DeviceState
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint
|
||||
import net.mullvad.mullvadvpn.lib.model.TunnelState
|
||||
import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken
|
||||
@ -31,7 +32,6 @@ import net.mullvad.mullvadvpn.lib.shared.AccountRepository
|
||||
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
|
||||
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
|
||||
import net.mullvad.mullvadvpn.repository.ChangelogRepository
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotificationController
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
|
||||
|
@ -13,11 +13,11 @@ import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
|
||||
import net.mullvad.mullvadvpn.lib.model.Constraint
|
||||
import net.mullvad.mullvadvpn.lib.model.DeviceState
|
||||
import net.mullvad.mullvadvpn.lib.model.Settings
|
||||
import net.mullvad.mullvadvpn.lib.model.VersionInfo
|
||||
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
|
||||
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
|
||||
import net.mullvad.mullvadvpn.repository.SettingsRepository
|
||||
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
|
||||
import net.mullvad.mullvadvpn.ui.VersionInfo
|
||||
import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
@ -22,6 +22,7 @@ androidx-testmonitor = "1.7.2"
|
||||
androidx-testorchestrator = "1.5.1"
|
||||
androidx-testrunner = "1.6.2"
|
||||
androidx-uiautomator = "2.4.0-alpha01"
|
||||
androidx-tv = "1.0.0"
|
||||
|
||||
# Arrow
|
||||
arrow = "2.0.1"
|
||||
@ -31,6 +32,7 @@ compose = "1.7.8"
|
||||
compose-destinations = "2.1.0"
|
||||
compose-constraintlayout = "1.1.1"
|
||||
compose-material3 = "1.3.1"
|
||||
compose-material-tv = "1.1.0-alpha01"
|
||||
|
||||
# Update suppression for 'InvalidPackage' in config/lint.xml
|
||||
grpc = "1.71.0"
|
||||
@ -99,6 +101,7 @@ androidx-test-runner = { module = "androidx.test:runner", version.ref = "android
|
||||
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test" }
|
||||
androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", version.ref = "androidx-uiautomator" }
|
||||
androidx-test-orchestrator = { module = "androidx.test:orchestrator", version.ref = "androidx-testorchestrator" }
|
||||
androidx-tv = { module = "androidx.tv:tv-material", version.ref = "androidx-tv" }
|
||||
|
||||
# Arrow
|
||||
arrow = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
|
||||
|
@ -200,6 +200,7 @@
|
||||
<trusting group="androidx.lifecycle"/>
|
||||
<trusting group="androidx.navigation"/>
|
||||
<trusting group="androidx.profileinstaller" name="profileinstaller"/>
|
||||
<trusting group="androidx.tv" name="tv-material"/>
|
||||
<trusting group="^androidx[.]compose($|([.].*))" regex="true"/>
|
||||
<trusting group="^androidx[.]test($|([.].*))" regex="true"/>
|
||||
<trusting group="^com[.]android($|([.].*))" regex="true"/>
|
||||
@ -654,6 +655,11 @@
|
||||
<sha256 value="e98defdf92ca1fcbeaf16e78a60c18052b01340da3849b93e25e944f06a4e527" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation" version="1.6.8">
|
||||
<artifact name="animation-1.6.8.module">
|
||||
<sha256 value="31e6783f9a1de6e021942c5be1f1d777e330bfe017f5429032a24f4c3a940726" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation" version="1.7.0">
|
||||
<artifact name="animation-1.7.0.module">
|
||||
<sha256 value="de5277c940808643cc4928d33ffd3a20c0d1da49c1f985bf57f4edbe9f0ed625" origin="Generated by Gradle"/>
|
||||
@ -678,6 +684,11 @@
|
||||
<sha256 value="a2773807796a9ea149e4d567d3e4746bc3b2514147a180b00be2ca307f732c4d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-android" version="1.6.8">
|
||||
<artifact name="animation-android-1.6.8.module">
|
||||
<sha256 value="4f2718c77ef295fbbba8a92b0deaf6d97fc21d0bcd1753d9cba3c20b25c4a076" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-android" version="1.7.2">
|
||||
<artifact name="animation-android-1.7.2.module">
|
||||
<sha256 value="7f4773c5800c09adb41f37a266effaac1618737eeadae80310a771a37cd8b547" origin="Generated by Gradle"/>
|
||||
@ -706,6 +717,11 @@
|
||||
<sha256 value="61bfa244fb1979bcca6dcb80a5c040115aecbad84d7d2e29cf237b483cab2248" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core" version="1.6.8">
|
||||
<artifact name="animation-core-1.6.8.module">
|
||||
<sha256 value="73e54651dbbfec4641840b3f3b7ad477833132cd336de2a22cc93ab233d3bd5c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core" version="1.7.0">
|
||||
<artifact name="animation-core-1.7.0.module">
|
||||
<sha256 value="f47f2eeea6677efe6e54516ece48c9835810dc4a8581fe530761463a602cd239" origin="Generated by Gradle"/>
|
||||
@ -735,6 +751,11 @@
|
||||
<sha256 value="c571f43b5780fb80f42e85644e0ba5696af088d7228f3251c3e1d64c3bd64376" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core-android" version="1.6.8">
|
||||
<artifact name="animation-core-android-1.6.8.module">
|
||||
<sha256 value="2814fcf1645cb1d5782b216236b99a4e2dde5bdcbb8e815f4514c044c28b2bef" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-core-android" version="1.7.8">
|
||||
<artifact name="animation-core-android-1.7.8.module">
|
||||
<sha256 value="3ce792d09bcf03a1cc1d55f1424822f0961ec9bb019450d2f036c7af1205a10c" origin="Generated by Gradle"/>
|
||||
@ -756,6 +777,11 @@
|
||||
<sha256 value="7d45e05892d2b9a14341e56e40c090bf9970128ab25c0d2140ba51a0389ce639" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-desktop" version="1.6.8">
|
||||
<artifact name="animation-desktop-1.6.8.module">
|
||||
<sha256 value="1ab7498b6321262d6c42611a8d9034d950bc11ede7eac09b7e2fc6bf97dbf8c2" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.animation" name="animation-jvmstubs" version="1.7.8">
|
||||
<artifact name="animation-jvmstubs-1.7.8.jar">
|
||||
<sha256 value="085168cfa19c0bf96ce0e1ef5bd2586997555d706ae2572d522aef141cb4d335" origin="Generated by Gradle"/>
|
||||
@ -764,6 +790,11 @@
|
||||
<sha256 value="9a3c2e37dd9715444b0c55358dbe1bdca4d042e6d5071e512a029d3c65bda225" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation" version="1.6.8">
|
||||
<artifact name="foundation-1.6.8.module">
|
||||
<sha256 value="045615477691111fefca60926d3657707d4af3dc5d0221a9cfbbe9cf92399699" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation" version="1.7.0">
|
||||
<artifact name="foundation-1.7.0.module">
|
||||
<sha256 value="8462ad30b9671fb6afd6c757c9b0830f66bb13ff0ca4d28e554e7b151a1c7291" origin="Generated by Gradle"/>
|
||||
@ -780,6 +811,11 @@
|
||||
<sha256 value="40c973f6464c280219e3e96443c5c6d536e84a077a8d493d55a545b3869744bd" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-android" version="1.6.8">
|
||||
<artifact name="foundation-android-1.6.8.module">
|
||||
<sha256 value="b92502e46bc91f147a8569758db9430bba34e6af477068c3cfea1207f872ab27" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-android" version="1.7.8">
|
||||
<artifact name="foundation-android-1.7.8.module">
|
||||
<sha256 value="dc733df156257961210fca6e715d1b5406afd2ed92989e44ed44366fc395bb1e" origin="Generated by Gradle"/>
|
||||
@ -796,6 +832,11 @@
|
||||
<sha256 value="a693be7c9683ab7a2932de9624047f92457a7543546c8f624f1e6528df892505" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout" version="1.6.8">
|
||||
<artifact name="foundation-layout-1.6.8.module">
|
||||
<sha256 value="7563a54ddec275c1428c3aed8ff4627a5e2ab405051e81b126e3c1c99502aa11" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout" version="1.7.0">
|
||||
<artifact name="foundation-layout-1.7.0.module">
|
||||
<sha256 value="7846029e463db74321494031fbfd0d1cd95d269c8bf586213bca191052e63876" origin="Generated by Gradle"/>
|
||||
@ -820,6 +861,11 @@
|
||||
<sha256 value="4094e5a3626380b22b684086e548be51afe18e7979f46d261d4f1e0b6898fb39" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout-android" version="1.6.8">
|
||||
<artifact name="foundation-layout-android-1.6.8.module">
|
||||
<sha256 value="f7769c1f05d0361bc0258291baea6865bba438bebd090ce21ff5ec1771b58c71" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout-android" version="1.7.0">
|
||||
<artifact name="foundation-layout-android-1.7.0.module">
|
||||
<sha256 value="4f6877bd205bd23a54f6f8209b07333fb878fcbd5187ba9a9881b298cd12b8a8" origin="Generated by Gradle"/>
|
||||
@ -838,6 +884,11 @@
|
||||
<sha256 value="a83233e768e30aab870e6667277ec91dd40adb5663d32f36dfa5dbad367db561" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout-desktop" version="1.6.8">
|
||||
<artifact name="foundation-layout-desktop-1.6.8.module">
|
||||
<sha256 value="47a7bcfbd1473204da930c9a3611e967ef1953cbfdae64e785863012573e7487" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.foundation" name="foundation-layout-jvmstubs" version="1.7.8">
|
||||
<artifact name="foundation-layout-jvmstubs-1.7.8.jar">
|
||||
<sha256 value="c03f5dca426beedbdfdf53abcf90ca5445dc6eb354688db763078c47f74bb5e3" origin="Generated by Gradle"/>
|
||||
@ -859,6 +910,11 @@
|
||||
<sha256 value="bf08f6b522f64eef4467d56e566951039eae55b54c37023712a81d4dfb214bdd" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material" version="1.6.8">
|
||||
<artifact name="material-1.6.8.module">
|
||||
<sha256 value="85760ca4ac3b28d2e869669f042ac41976da2ad16ea17e2f280c2ae1b6f7eebd" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-android" version="1.6.0">
|
||||
<artifact name="material-android-1.6.0.module">
|
||||
<sha256 value="923f5d7b44d423605193b651023f1b4fd7e1d8cf763bd64942b80122ff7e2885" origin="Generated by Gradle"/>
|
||||
@ -883,6 +939,14 @@
|
||||
<sha256 value="951f2a3a6c0913819dfaae7c69cb8cdf977f7c79bd53fef03e4faf459ee30a0f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core" version="1.6.8">
|
||||
<artifact name="material-icons-core-1.6.8.module">
|
||||
<sha256 value="ea1acaa1dfd488d6cab2d9d645010892e784b10b592adbe8290c5a8aaaf1944e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="material-icons-core-metadata-1.6.8.jar">
|
||||
<sha256 value="951f2a3a6c0913819dfaae7c69cb8cdf977f7c79bd53fef03e4faf459ee30a0f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core" version="1.7.8">
|
||||
<artifact name="material-icons-core-1.7.8.module">
|
||||
<sha256 value="f9d63655bac19ff7f27abf68a9c0f38f5e42c85e365655b990e6e1a317f92e2f" origin="Generated by Gradle"/>
|
||||
@ -899,6 +963,11 @@
|
||||
<sha256 value="0400755a3aa7270893445a18cd845e35064c9de02c1c41cf2083ad4724bcac6f" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core-android" version="1.6.8">
|
||||
<artifact name="material-icons-core-android-1.6.8.module">
|
||||
<sha256 value="8e0540fb09dd3a483168488cceec4806e4cc0a7946354a64c72c61e63fd415ff" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core-android" version="1.7.8">
|
||||
<artifact name="material-icons-core-android-1.7.8.module">
|
||||
<sha256 value="99a1ca83e54261a65eb96d44ea02fae43588be45ade5e97963d73e8489ea4a54" origin="Generated by Gradle"/>
|
||||
@ -907,6 +976,11 @@
|
||||
<sha256 value="332c06b25e662cc417fb087e76b8faa5cb249f4992ffa3360084a3d4ab882284" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core-desktop" version="1.6.8">
|
||||
<artifact name="material-icons-core-desktop-1.6.8.module">
|
||||
<sha256 value="898008d26735f253b40fef3fc1b66d34d6d593706e679b4f21d0ce6e1ad1c75a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.material" name="material-icons-core-desktop" version="1.7.8">
|
||||
<artifact name="material-icons-core-desktop-1.7.8.jar">
|
||||
<sha256 value="b5729220e242132b22b0c0317a304ff167a05cc685c3e9e6483d5dfca3495f56" origin="Generated by Gradle"/>
|
||||
@ -995,6 +1069,11 @@
|
||||
<sha256 value="89a673451d542de4819c0da6c0c680f5cc15d93e234eb67a3feed3a1a774d348" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.runtime" name="runtime" version="1.6.8">
|
||||
<artifact name="runtime-1.6.8.module">
|
||||
<sha256 value="15d27ca9a22e02345d2193c1d1ab509f77c714c6b3533df1a5a2c268e667b097" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.runtime" name="runtime" version="1.7.0">
|
||||
<artifact name="runtime-1.7.0.module">
|
||||
<sha256 value="7b9351b0ff6df9276d02d40f12765ae55bf5c6dfb8ff8df4c77dfca138fb9fc1" origin="Generated by Gradle"/>
|
||||
@ -1118,6 +1197,11 @@
|
||||
<sha256 value="56366721d8a87924773839c2725e72ef252da662b306b9ed662afd64b6508e6e" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-graphics" version="1.6.8">
|
||||
<artifact name="ui-graphics-1.6.8.module">
|
||||
<sha256 value="eb4d03821a8d2c7919743a8b80ba9f96186c6022bd7ecec02940bc1555abee20" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-graphics" version="1.7.8">
|
||||
<artifact name="ui-graphics-1.7.8.module">
|
||||
<sha256 value="809d8da1b129997269435728c351a7939260d9acd28e2ccb9f7400eec2ec2d56" origin="Generated by Gradle"/>
|
||||
@ -1126,6 +1210,11 @@
|
||||
<sha256 value="5ba9ece097ba31fea936095e1c34b1f517645a1af55e4a641e707c583e61d94b" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-graphics-android" version="1.6.8">
|
||||
<artifact name="ui-graphics-android-1.6.8.module">
|
||||
<sha256 value="39bece706d28b44b3fb2a3ae5b1508f3247ba35d5d405d96033378c98e615965" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-graphics-android" version="1.7.8">
|
||||
<artifact name="ui-graphics-android-1.7.8.module">
|
||||
<sha256 value="fafcba9f63c7bbd89304e7dae09327b2f2ae9ae1a3676b0fe60469403b973dcd" origin="Generated by Gradle"/>
|
||||
@ -1195,6 +1284,11 @@
|
||||
<sha256 value="c830a2fdf5cf174da80bbcaf977b69a4f55dd935dce78bab676e191780aa449a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-text" version="1.6.8">
|
||||
<artifact name="ui-text-1.6.8.module">
|
||||
<sha256 value="0fae7dc013e91f4792b34b13b36416684cd1750cbb360e498d09e79bd7ce4af7" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-text" version="1.7.8">
|
||||
<artifact name="ui-text-1.7.8.module">
|
||||
<sha256 value="1ec485dbc361a0b132a0c01b441d42932ee207542ca4f8d82fb3e2a1560e3143" origin="Generated by Gradle"/>
|
||||
@ -1203,6 +1297,11 @@
|
||||
<sha256 value="5bd379bebabeb47ea92b3777bb1ceb31dc93c294b46f264dcf8e4c010f4699fc" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-text-android" version="1.6.8">
|
||||
<artifact name="ui-text-android-1.6.8.module">
|
||||
<sha256 value="31c5457679534b6bdeaffa0071266614f6fb9af0b7119928427eb2fb00ff6748" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-text-android" version="1.7.8">
|
||||
<artifact name="ui-text-android-1.7.8.module">
|
||||
<sha256 value="1fa981ca4d73faea7079a722564237f05465ebb257e81509895aebf207392331" origin="Generated by Gradle"/>
|
||||
@ -1325,6 +1424,11 @@
|
||||
<sha256 value="85b335a307cb203c0ee477bfeada5023aadc282bd8ab9d114d69d61937490e17" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-util" version="1.6.8">
|
||||
<artifact name="ui-util-1.6.8.module">
|
||||
<sha256 value="c91b6fe99cd05baae7c6858dc0d52dd643c9ce3edf15308b51b6c6e6bb22873c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-util" version="1.7.8">
|
||||
<artifact name="ui-util-1.7.8.module">
|
||||
<sha256 value="c6489709dad543bdfee77f42784a7b26284ce913e2e8cf33554ad2ed79c7dea7" origin="Generated by Gradle"/>
|
||||
@ -1333,6 +1437,11 @@
|
||||
<sha256 value="25afa139ccbda2c33c6d9e7be3579e2ca9295f986e3b4f5b96297d6ce0fba86a" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-util-android" version="1.6.8">
|
||||
<artifact name="ui-util-android-1.6.8.module">
|
||||
<sha256 value="c6fccb2b21c7187a0905c5b667939680c8996b7cbe0c33a8c8a477a2f6b5e53c" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.compose.ui" name="ui-util-android" version="1.7.8">
|
||||
<artifact name="ui-util-android-1.7.8.module">
|
||||
<sha256 value="a18c49781d7324410966e1faa76dd165a874474a13f8bb0c6f2214b50bbb4d6c" origin="Generated by Gradle"/>
|
||||
@ -2437,6 +2546,14 @@
|
||||
<sha256 value="d0d8d486b6bd33206dbf3f1a6d167e9b43c268ea63c3321c886b1543ad05ece3" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.tv" name="tv-material" version="1.0.0">
|
||||
<artifact name="tv-material-1.0.0.aar">
|
||||
<sha256 value="a66890ad58ffc31036d8a9ea99fa3ab478c5daa7189b296c6b92e05e8c7db604" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
<artifact name="tv-material-1.0.0.module">
|
||||
<sha256 value="fa6689598785362efcb66a328907ecead533c7fd79f7d52c649aefa52841400d" origin="Generated by Gradle"/>
|
||||
</artifact>
|
||||
</component>
|
||||
<component group="androidx.vectordrawable" name="vectordrawable" version="1.1.0">
|
||||
<artifact name="vectordrawable-1.1.0.aar">
|
||||
<sha256 value="46fd633ac01b49b7fcabc263bf098c5a8b9e9a69774d234edcca04fb02df8e26" origin="Generated by Gradle"/>
|
||||
|
@ -0,0 +1,44 @@
|
||||
package net.mullvad.mullvadvpn.lib.model
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
enum class StatusLevel {
|
||||
Error,
|
||||
Warning,
|
||||
Info,
|
||||
}
|
||||
|
||||
sealed class InAppNotification {
|
||||
abstract val statusLevel: StatusLevel
|
||||
abstract val priority: Long
|
||||
|
||||
data class TunnelStateError(val error: ErrorState) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data object TunnelStateBlocked : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 1000
|
||||
}
|
||||
|
||||
data class UnsupportedVersion(val versionInfo: VersionInfo) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Error
|
||||
override val priority: Long = 999
|
||||
}
|
||||
|
||||
data class AccountExpiry(val expiry: Duration) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Warning
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data class NewDevice(val deviceName: String) : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Info
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
|
||||
data object NewVersionChangelog : InAppNotification() {
|
||||
override val statusLevel = StatusLevel.Info
|
||||
override val priority: Long = 1001
|
||||
}
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
package net.mullvad.mullvadvpn.ui
|
||||
package net.mullvad.mullvadvpn.lib.model
|
||||
|
||||
data class VersionInfo(val currentVersion: String, val isSupported: Boolean)
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="isTv">true</bool>
|
||||
</resources>
|
4
android/lib/resource/src/main/res/values/booleans.xml
Normal file
4
android/lib/resource/src/main/res/values/booleans.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="isTv">false</bool>
|
||||
</resources>
|
@ -40,9 +40,12 @@ data class Dimensions(
|
||||
val largePadding: Dp = 32.dp,
|
||||
val listIconSize: Dp = 24.dp,
|
||||
val listItemDivider: Dp = 1.dp,
|
||||
val mediumIconSize: Dp = 32.dp,
|
||||
val mediumPadding: Dp = 16.dp,
|
||||
val mediumSpacer: Dp = 16.dp,
|
||||
val miniPadding: Dp = 4.dp,
|
||||
val mullvadLogoTextStartPadding: Dp = 6.dp,
|
||||
val mullvadLogoTextHeight: Dp = 13.dp,
|
||||
val notificationBannerEndPadding: Dp = 8.dp,
|
||||
val notificationBannerStartPadding: Dp = 16.dp,
|
||||
val notificationEndIconPadding: Dp = 4.dp,
|
||||
@ -75,6 +78,9 @@ data class Dimensions(
|
||||
val tinyPadding: Dp = 4.dp,
|
||||
val titleIconSize: Dp = 48.dp,
|
||||
val topPadding: Dp = 20.dp,
|
||||
val tvDrawerHorizontalPadding: Dp = 12.dp,
|
||||
val tvDrawerHeaderStartPadding: Dp = 12.dp,
|
||||
val tvDrawerHeaderWithFocusStartPadding: Dp = 16.dp,
|
||||
val verticalDividerPadding: Dp = 12.dp,
|
||||
val verticalSpace: Dp = 20.dp,
|
||||
val verticalSpacer: Dp = 1.dp,
|
||||
|
48
android/lib/tv/build.gradle.kts
Normal file
48
android/lib/tv/build.gradle.kts
Normal file
@ -0,0 +1,48 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.mullvad.mullvadvpn.lib.tv"
|
||||
compileSdk = Versions.compileSdkVersion
|
||||
buildToolsVersion = Versions.buildToolsVersion
|
||||
|
||||
defaultConfig { minSdk = Versions.minSdkVersion }
|
||||
|
||||
buildFeatures { compose = true }
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = Versions.jvmTarget
|
||||
allWarningsAsErrors = true
|
||||
}
|
||||
|
||||
lint {
|
||||
lintConfig = file("${rootProject.projectDir}/config/lint.xml")
|
||||
abortOnError = true
|
||||
warningsAsErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.androidx.tv)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(projects.lib.model)
|
||||
implementation(projects.lib.resource)
|
||||
implementation(projects.lib.shared)
|
||||
implementation(projects.lib.theme)
|
||||
implementation(projects.lib.ui.component)
|
||||
|
||||
// UI tooling
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
}
|
1
android/lib/tv/src/main/AndroidManifest.xml
Normal file
1
android/lib/tv/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest />
|
@ -0,0 +1,251 @@
|
||||
package net.mullvad.mullvadvpn.lib.tv
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.Default
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.DrawerValue
|
||||
import androidx.tv.material3.ModalNavigationDrawer
|
||||
import androidx.tv.material3.NavigationDrawerItem
|
||||
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||
import androidx.tv.material3.NavigationDrawerScope
|
||||
import androidx.tv.material3.rememberDrawerState
|
||||
import net.mullvad.mullvadvpn.lib.theme.AppTheme
|
||||
import net.mullvad.mullvadvpn.lib.theme.Dimens
|
||||
|
||||
private class DrawerValueProvider : PreviewParameterProvider<DrawerValue> {
|
||||
override val values: Sequence<DrawerValue>
|
||||
get() = sequenceOf(DrawerValue.Closed, DrawerValue.Open)
|
||||
}
|
||||
|
||||
@Preview("Closed|Open")
|
||||
@Composable
|
||||
fun PreviewNavigationDrawerTvClosed(
|
||||
@PreviewParameter(DrawerValueProvider::class) drawerValue: DrawerValue
|
||||
) {
|
||||
AppTheme {
|
||||
NavigationDrawerTv(
|
||||
daysLeftUntilExpiry = 30,
|
||||
deviceName = "Cool Cat",
|
||||
initialDrawerValue = drawerValue,
|
||||
onSettingsClick = {},
|
||||
onAccountClick = {},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun NavigationDrawerTv(
|
||||
daysLeftUntilExpiry: Long?,
|
||||
deviceName: String?,
|
||||
initialDrawerValue: DrawerValue = DrawerValue.Closed,
|
||||
onSettingsClick: (() -> Unit),
|
||||
onAccountClick: (() -> Unit),
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val drawerState = rememberDrawerState(initialDrawerValue)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val brush = remember { Brush.horizontalGradient(listOf(Color.Black, Color.Transparent)) }
|
||||
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
if (drawerState.currentValue == DrawerValue.Open) {
|
||||
BackHandler(
|
||||
onBack = {
|
||||
drawerState.setValue(DrawerValue.Closed)
|
||||
focusManager.moveFocus(FocusDirection.Right)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
modifier =
|
||||
Modifier.focusRequester(focusRequester).focusProperties {
|
||||
enter = { if (focusRequester.restoreFocusedChild()) Cancel else Default }
|
||||
},
|
||||
drawerState = drawerState,
|
||||
scrimBrush = brush,
|
||||
drawerContent = {
|
||||
Box(
|
||||
Modifier.fillMaxHeight()
|
||||
.background(brush)
|
||||
.padding(
|
||||
top = Dimens.screenVerticalMargin,
|
||||
bottom = Dimens.screenVerticalMargin,
|
||||
start = Dimens.tvDrawerHorizontalPadding,
|
||||
end = Dimens.tvDrawerHorizontalPadding,
|
||||
)
|
||||
.selectableGroup()
|
||||
) {
|
||||
val animatedPadding =
|
||||
animateDpAsState(
|
||||
if (hasFocus) Dimens.tvDrawerHeaderWithFocusStartPadding
|
||||
else Dimens.tvDrawerHeaderStartPadding
|
||||
)
|
||||
|
||||
NavigationDrawerTvHeader(
|
||||
modifier =
|
||||
Modifier.align(Alignment.TopStart).padding(start = animatedPadding.value),
|
||||
isExpanded = hasFocus,
|
||||
daysLeftUntilExpiry = daysLeftUntilExpiry,
|
||||
deviceName = deviceName,
|
||||
)
|
||||
DrawerItemTv(
|
||||
modifier =
|
||||
Modifier.align(Alignment.CenterStart).onFocusChanged {
|
||||
focusRequester.saveFocusedChild()
|
||||
},
|
||||
icon = Icons.Default.AccountCircle,
|
||||
text = stringResource(R.string.settings_account),
|
||||
onClick = onAccountClick,
|
||||
)
|
||||
DrawerItemTv(
|
||||
modifier =
|
||||
Modifier.align(Alignment.BottomStart).onFocusChanged {
|
||||
focusRequester.saveFocusedChild()
|
||||
},
|
||||
icon = Icons.Default.Settings,
|
||||
text = stringResource(R.string.settings),
|
||||
onClick = onSettingsClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationDrawerScope.DrawerItemTv(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
NavigationDrawerItem(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
selected = false,
|
||||
leadingContent = {
|
||||
Icon(
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
text = text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NavigationDrawerTvHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
isExpanded: Boolean,
|
||||
daysLeftUntilExpiry: Long?,
|
||||
deviceName: String?,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
modifier.width(
|
||||
if (isExpanded) NavigationDrawerItemDefaults.ExpandedDrawerItemWidth
|
||||
else NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Dimens.mullvadLogoTextStartPadding),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(Dimens.mediumIconSize),
|
||||
painter = painterResource(id = R.drawable.logo_icon),
|
||||
contentDescription = null, // No meaningful user info or action.
|
||||
tint = Color.Unspecified, // Logo should not be tinted
|
||||
)
|
||||
if (isExpanded) {
|
||||
Icon(
|
||||
modifier = Modifier.height(Dimens.mullvadLogoTextHeight),
|
||||
painter = painterResource(id = R.drawable.logo_text),
|
||||
contentDescription = null, // No meaningful user info or action.
|
||||
tint = Color.Unspecified, // Logo should not be tinted
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
if (isExpanded) {
|
||||
Text(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.top_bar_device_name, deviceName ?: ""),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text =
|
||||
stringResource(
|
||||
id = R.string.top_bar_time_left,
|
||||
pluralStringResource(
|
||||
id = R.plurals.days,
|
||||
daysLeftUntilExpiry?.toInt() ?: 0,
|
||||
daysLeftUntilExpiry ?: 0,
|
||||
),
|
||||
),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Clip,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package net.mullvad.mullvadvpn.lib.tv
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.theme.AppTheme
|
||||
import net.mullvad.mullvadvpn.lib.theme.Dimens
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.AnimatedNotificationBanner
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewNotificationBannerTv() {
|
||||
AppTheme {
|
||||
NotificationBannerTv(
|
||||
notification = InAppNotification.NewDevice("Sad Panda"),
|
||||
isPlayBuild = true,
|
||||
openAppListing = {},
|
||||
onClickShowAccount = {},
|
||||
onClickShowChangelog = {},
|
||||
onClickDismissChangelog = {},
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NotificationBannerTv(
|
||||
modifier: Modifier = Modifier,
|
||||
notification: InAppNotification?,
|
||||
isPlayBuild: Boolean,
|
||||
openAppListing: () -> Unit,
|
||||
onClickShowAccount: () -> Unit,
|
||||
onClickShowChangelog: () -> Unit,
|
||||
onClickDismissChangelog: () -> Unit,
|
||||
onClickDismissNewDevice: () -> Unit,
|
||||
) {
|
||||
AnimatedNotificationBanner(
|
||||
modifier = modifier,
|
||||
notificationModifier =
|
||||
Modifier.width(Dimens.connectionCardMaxWidth)
|
||||
.padding(start = Dimens.mediumPadding, end = Dimens.mediumPadding)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
bottomEnd = Dimens.mediumPadding,
|
||||
bottomStart = Dimens.mediumPadding,
|
||||
topStart = 0.dp,
|
||||
topEnd = 0.dp,
|
||||
)
|
||||
),
|
||||
notification = notification,
|
||||
isPlayBuild = isPlayBuild,
|
||||
openAppListing = openAppListing,
|
||||
onClickShowAccount = onClickShowAccount,
|
||||
onClickShowChangelog = onClickShowChangelog,
|
||||
onClickDismissChangelog = onClickDismissChangelog,
|
||||
onClickDismissNewDevice = onClickDismissNewDevice,
|
||||
)
|
||||
}
|
44
android/lib/ui/component/build.gradle.kts
Normal file
44
android/lib/ui/component/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
alias(libs.plugins.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "net.mullvad.mullvadvpn.lib.ui.component"
|
||||
compileSdk = Versions.compileSdkVersion
|
||||
buildToolsVersion = Versions.buildToolsVersion
|
||||
|
||||
defaultConfig { minSdk = Versions.minSdkVersion }
|
||||
|
||||
buildFeatures { compose = true }
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = Versions.jvmTarget
|
||||
allWarningsAsErrors = true
|
||||
}
|
||||
|
||||
lint {
|
||||
lintConfig = file("${rootProject.projectDir}/config/lint.xml")
|
||||
abortOnError = true
|
||||
warningsAsErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.constrainlayout)
|
||||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.compose.icons.extended)
|
||||
implementation(libs.androidx.ktx)
|
||||
implementation(projects.lib.resource)
|
||||
implementation(projects.lib.shared)
|
||||
implementation(projects.lib.theme)
|
||||
implementation(projects.lib.model)
|
||||
}
|
1
android/lib/ui/component/src/main/AndroidManifest.xml
Normal file
1
android/lib/ui/component/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest />
|
@ -0,0 +1,208 @@
|
||||
package net.mullvad.mullvadvpn.lib.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.StatusLevel
|
||||
import net.mullvad.mullvadvpn.lib.theme.Dimens
|
||||
import net.mullvad.mullvadvpn.lib.theme.color.warning
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.test.NOTIFICATION_BANNER
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.test.NOTIFICATION_BANNER_ACTION
|
||||
import net.mullvad.mullvadvpn.lib.ui.component.test.NOTIFICATION_BANNER_TEXT_ACTION
|
||||
|
||||
@Composable
|
||||
fun AnimatedNotificationBanner(
|
||||
modifier: Modifier = Modifier,
|
||||
notificationModifier: Modifier = Modifier,
|
||||
notification: InAppNotification?,
|
||||
isPlayBuild: Boolean,
|
||||
openAppListing: () -> Unit,
|
||||
onClickShowAccount: () -> Unit,
|
||||
onClickShowChangelog: () -> Unit,
|
||||
onClickDismissChangelog: () -> Unit,
|
||||
onClickDismissNewDevice: () -> Unit,
|
||||
) {
|
||||
// Fix for animating to invisible state
|
||||
val previous = rememberPrevious(current = notification, shouldUpdate = { _, _ -> true })
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = notification != null,
|
||||
enter = slideInVertically(initialOffsetY = { -it }),
|
||||
exit = slideOutVertically(targetOffsetY = { -it }),
|
||||
) {
|
||||
val visibleNotification = notification ?: previous
|
||||
if (visibleNotification != null)
|
||||
Notification(
|
||||
modifier = notificationModifier,
|
||||
visibleNotification.toNotificationData(
|
||||
isPlayBuild = isPlayBuild,
|
||||
openAppListing,
|
||||
onClickShowAccount,
|
||||
onClickShowChangelog,
|
||||
onClickDismissChangelog,
|
||||
onClickDismissNewDevice,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun Notification(modifier: Modifier = Modifier, notificationBannerData: NotificationData) {
|
||||
val (title, message, statusLevel, action) = notificationBannerData
|
||||
ConstraintLayout(
|
||||
modifier =
|
||||
modifier
|
||||
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||
.padding(
|
||||
start = Dimens.notificationBannerStartPadding,
|
||||
end = Dimens.notificationBannerEndPadding,
|
||||
top = Dimens.smallPadding,
|
||||
bottom = Dimens.smallPadding,
|
||||
)
|
||||
.animateContentSize()
|
||||
.testTag(NOTIFICATION_BANNER)
|
||||
) {
|
||||
val (status, textTitle, textMessage, actionIcon) = createRefs()
|
||||
NotificationDot(
|
||||
statusLevel,
|
||||
Modifier.constrainAs(status) {
|
||||
top.linkTo(textTitle.top)
|
||||
start.linkTo(parent.start)
|
||||
bottom.linkTo(textTitle.bottom)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = title.toUpperCase(),
|
||||
modifier =
|
||||
Modifier.constrainAs(textTitle) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(status.end)
|
||||
if (message != null) {
|
||||
bottom.linkTo(textMessage.top)
|
||||
} else {
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
if (action != null) {
|
||||
end.linkTo(actionIcon.start)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.padding(start = Dimens.smallPadding),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
message?.let { message ->
|
||||
Text(
|
||||
text = message.text,
|
||||
modifier =
|
||||
Modifier.constrainAs(textMessage) {
|
||||
top.linkTo(textTitle.bottom)
|
||||
start.linkTo(textTitle.start)
|
||||
if (action != null) {
|
||||
end.linkTo(actionIcon.start)
|
||||
bottom.linkTo(parent.bottom)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
width = Dimension.fillToConstraints
|
||||
height = Dimension.wrapContent
|
||||
}
|
||||
.padding(start = Dimens.smallPadding, top = Dimens.tinyPadding)
|
||||
.wrapContentWidth(Alignment.Start)
|
||||
.let {
|
||||
if (message is NotificationMessage.ClickableText) {
|
||||
it.clickable(
|
||||
onClickLabel = message.contentDescription,
|
||||
role = Role.Button,
|
||||
) {
|
||||
message.onClick()
|
||||
}
|
||||
.testTag(NOTIFICATION_BANNER_TEXT_ACTION)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
},
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
action?.let {
|
||||
NotificationAction(
|
||||
it.icon,
|
||||
onClick = it.onClick,
|
||||
contentDescription = it.contentDescription,
|
||||
modifier =
|
||||
Modifier.constrainAs(actionIcon) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationDot(statusLevel: StatusLevel, modifier: Modifier) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.background(
|
||||
color =
|
||||
when (statusLevel) {
|
||||
StatusLevel.Error -> MaterialTheme.colorScheme.error
|
||||
StatusLevel.Warning -> MaterialTheme.colorScheme.warning
|
||||
StatusLevel.Info -> MaterialTheme.colorScheme.tertiary
|
||||
},
|
||||
shape = CircleShape,
|
||||
)
|
||||
.size(Dimens.notificationStatusIconSize)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationAction(
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
||||
IconButton(modifier = modifier.testTag(NOTIFICATION_BANNER_ACTION), onClick = onClick) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(Dimens.notificationIconPadding),
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.mullvad.mullvadvpn.compose.component.notificationbanner
|
||||
package net.mullvad.mullvadvpn.lib.ui.component
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
@ -16,15 +16,12 @@ import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.core.text.HtmlCompat
|
||||
import java.net.InetAddress
|
||||
import net.mullvad.mullvadvpn.R
|
||||
import net.mullvad.mullvadvpn.compose.extensions.getExpiryQuantityString
|
||||
import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString
|
||||
import net.mullvad.mullvadvpn.lib.model.AuthFailedError
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorState
|
||||
import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
|
||||
import net.mullvad.mullvadvpn.lib.model.InAppNotification
|
||||
import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
|
||||
import net.mullvad.mullvadvpn.repository.InAppNotification
|
||||
import net.mullvad.mullvadvpn.ui.notification.StatusLevel
|
||||
import net.mullvad.mullvadvpn.lib.model.StatusLevel
|
||||
|
||||
data class NotificationData(
|
||||
val title: AnnotatedString,
|
@ -1,4 +1,4 @@
|
||||
package net.mullvad.mullvadvpn.compose.util
|
||||
package net.mullvad.mullvadvpn.lib.ui.component
|
||||
|
||||
/*
|
||||
* Code snippet taken from:
|
@ -1,8 +1,7 @@
|
||||
package net.mullvad.mullvadvpn.compose.extensions
|
||||
package net.mullvad.mullvadvpn.lib.ui.component
|
||||
|
||||
import android.content.res.Resources
|
||||
import java.time.Duration
|
||||
import net.mullvad.mullvadvpn.R
|
||||
|
||||
private const val DAYS_IN_STANDARD_YEAR = 365
|
||||
|
@ -1,4 +1,4 @@
|
||||
package net.mullvad.mullvadvpn.compose.extensions
|
||||
package net.mullvad.mullvadvpn.lib.ui.component
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.Spanned
|
@ -0,0 +1,6 @@
|
||||
package net.mullvad.mullvadvpn.lib.ui.component.test
|
||||
|
||||
// ConnectScreen - Notification banner
|
||||
const val NOTIFICATION_BANNER = "notification_banner"
|
||||
const val NOTIFICATION_BANNER_ACTION = "notification_banner_action"
|
||||
const val NOTIFICATION_BANNER_TEXT_ACTION = "notification_banner_text_action"
|
@ -27,7 +27,9 @@ include(
|
||||
":lib:resource",
|
||||
":lib:shared",
|
||||
":lib:talpid",
|
||||
":lib:theme"
|
||||
":lib:theme",
|
||||
":lib:tv",
|
||||
":lib:ui:component",
|
||||
)
|
||||
include(
|
||||
":test",
|
||||
|
Loading…
x
Reference in New Issue
Block a user