diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-10-13 11:04:45 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-10-13 11:04:45 +0200 |
| commit | 6842d204e3585b216f7e271ecb9eaf4a158ee666 (patch) | |
| tree | d5bac59e0f8a4daf7b5db9c4d51ee9714aa54405 | |
| parent | bd2bdbedd280ca84a543472143c959d655651a73 (diff) | |
| parent | e72415c31be76c019ec135ccb231560454675e7e (diff) | |
| download | mullvadvpn-6842d204e3585b216f7e271ecb9eaf4a158ee666.tar.xz mullvadvpn-6842d204e3585b216f7e271ecb9eaf4a158ee666.zip | |
Merge branch 'add-device-name-and-time-left-to-main-view-droid-90'
42 files changed, 379 insertions, 51 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt index 02a148b22d..68cfa2b92c 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt @@ -83,6 +83,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationBlocked ), @@ -118,6 +120,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationBlocked ), @@ -151,6 +155,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() @@ -182,6 +188,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() @@ -214,6 +222,8 @@ class ConnectScreenTest { outAddress = "", showLocation = true, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() @@ -246,6 +256,8 @@ class ConnectScreenTest { outAddress = "", showLocation = true, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() @@ -280,6 +292,8 @@ class ConnectScreenTest { outAddress = "", showLocation = true, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationError( ErrorState(ErrorStateCause.StartTunnelError, true) @@ -318,6 +332,8 @@ class ConnectScreenTest { outAddress = "", showLocation = true, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationError( ErrorState(ErrorStateCause.StartTunnelError, false) @@ -353,6 +369,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationBlocked ), @@ -388,6 +406,8 @@ class ConnectScreenTest { outAddress = "", showLocation = true, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowTunnelStateNotificationBlocked ), @@ -423,6 +443,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -454,6 +476,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -485,6 +509,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -515,6 +541,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -545,6 +573,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -576,6 +606,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow(), @@ -614,6 +646,8 @@ class ConnectScreenTest { outAddress = mockOutAddress, showLocation = false, isTunnelInfoExpanded = true, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.HideNotification ), uiSideEffect = MutableSharedFlow<ConnectViewModel.UiSideEffect>().asSharedFlow() @@ -651,6 +685,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowVersionInfoNotification(versionInfo) ), @@ -687,6 +723,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowVersionInfoNotification(versionInfo) ), @@ -720,6 +758,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = null, + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowAccountExpiryNotification(expiryDate) ), @@ -758,6 +798,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowVersionInfoNotification(versionInfo) ), @@ -790,6 +832,8 @@ class ConnectScreenTest { outAddress = "", showLocation = false, isTunnelInfoExpanded = false, + deviceName = "", + daysLeftUntilExpiry = null, connectNotificationState = ConnectNotificationState.ShowAccountExpiryNotification(expiryDate) ), diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt index a177aa8ac1..95b44f8286 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt @@ -29,7 +29,7 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = false, - uiState = OutOfTimeUiState(), + uiState = OutOfTimeUiState(deviceName = ""), uiSideEffect = MutableSharedFlow(), onSitePaymentClick = {}, onRedeemVoucherClick = {}, @@ -57,7 +57,7 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(), + uiState = OutOfTimeUiState(deviceName = ""), uiSideEffect = MutableStateFlow(OutOfTimeViewModel.UiSideEffect.OpenAccountView("222")), onSitePaymentClick = {}, @@ -80,7 +80,7 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(), + uiState = OutOfTimeUiState(deviceName = ""), uiSideEffect = MutableStateFlow(OutOfTimeViewModel.UiSideEffect.OpenConnectScreen), onSitePaymentClick = {}, onRedeemVoucherClick = {}, @@ -102,7 +102,7 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(), + uiState = OutOfTimeUiState(deviceName = ""), uiSideEffect = MutableSharedFlow(), onSitePaymentClick = mockClickListener, onRedeemVoucherClick = {}, @@ -127,7 +127,7 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(), + uiState = OutOfTimeUiState(deviceName = ""), uiSideEffect = MutableSharedFlow(), onSitePaymentClick = {}, onRedeemVoucherClick = mockClickListener, @@ -152,7 +152,11 @@ class OutOfTimeScreenTest { composeTestRule.setContent { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(tunnelState = TunnelState.Connecting(null, null)), + uiState = + OutOfTimeUiState( + tunnelState = TunnelState.Connecting(null, null), + deviceName = "" + ), uiSideEffect = MutableSharedFlow(), onSitePaymentClick = {}, onRedeemVoucherClick = {}, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt index eb4d0d19a5..332c841d87 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.compose.component +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize @@ -67,6 +68,52 @@ fun ScaffoldWithTopBar( } @Composable +fun ScaffoldWithTopBarAndDeviceName( + topBarColor: Color, + statusBarColor: Color, + navigationBarColor: Color, + modifier: Modifier = Modifier, + iconTintColor: Color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaTopBar), + onSettingsClicked: (() -> Unit)?, + onAccountClicked: (() -> Unit)?, + isIconAndLogoVisible: Boolean = true, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + deviceName: String?, + timeLeft: Int?, + content: @Composable (PaddingValues) -> Unit, +) { + val systemUiController = rememberSystemUiController() + LaunchedEffect(key1 = statusBarColor, key2 = navigationBarColor) { + systemUiController.setStatusBarColor(statusBarColor) + systemUiController.setNavigationBarColor(navigationBarColor) + } + + Scaffold( + modifier = modifier, + topBar = { + Column { + MullvadTopBarWithDeviceName( + containerColor = topBarColor, + iconTintColor = iconTintColor, + onSettingsClicked = onSettingsClicked, + onAccountClicked = onAccountClicked, + isIconAndLogoVisible = isIconAndLogoVisible, + deviceName = deviceName, + daysLeftUntilExpiry = timeLeft + ) + } + }, + snackbarHost = { + SnackbarHost( + snackbarHostState, + snackbar = { snackbarData -> MullvadSnackbar(snackbarData = snackbarData) } + ) + }, + content = content + ) +} + +@Composable fun MullvadSnackbar(snackbarData: SnackbarData) { Snackbar(snackbarData = snackbarData, contentColor = MaterialTheme.colorScheme.secondary) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt index 3c5e0e1bb7..5e8fc2c78b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt @@ -2,9 +2,18 @@ package net.mullvad.mullvadvpn.compose.component +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -13,16 +22,19 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color 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 @@ -206,3 +218,107 @@ fun MullvadMediumTopBar( actions = actions ) } + +@Preview +@Composable +private fun PreviewMullvadTopBarWithLongDeviceName() { + AppTheme { + Surface { + MullvadTopBarWithDeviceName( + containerColor = MaterialTheme.colorScheme.error, + iconTintColor = MaterialTheme.colorScheme.onError, + onSettingsClicked = null, + onAccountClicked = null, + deviceName = "Superstitious Hippopotamus with extra weight", + daysLeftUntilExpiry = 1 + ) + } + } +} + +@Preview +@Composable +private fun PreviewMullvadTopBarWithShortDeviceName() { + AppTheme { + Surface { + MullvadTopBarWithDeviceName( + containerColor = MaterialTheme.colorScheme.error, + iconTintColor = MaterialTheme.colorScheme.onError, + onSettingsClicked = null, + onAccountClicked = null, + deviceName = "Fit Ant", + daysLeftUntilExpiry = 1 + ) + } + } +} + +@Composable +fun MullvadTopBarWithDeviceName( + containerColor: Color, + onSettingsClicked: (() -> Unit)?, + onAccountClicked: (() -> Unit)?, + iconTintColor: Color, + isIconAndLogoVisible: Boolean = true, + deviceName: String?, + daysLeftUntilExpiry: Int? +) { + Column { + MullvadTopBar( + containerColor, + onSettingsClicked, + onAccountClicked, + Modifier, + iconTintColor, + isIconAndLogoVisible, + ) + + // Align animation of extra row with the rest of the Topbar + val appBarContainerColor by + animateColorAsState( + targetValue = containerColor, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "ColorAnimation" + ) + Row( + modifier = + Modifier.background(appBarContainerColor) + .padding( + bottom = Dimens.smallPadding, + start = Dimens.mediumPadding, + end = Dimens.mediumPadding + ) + .fillMaxWidth() + .animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(Dimens.mediumPadding) + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = + deviceName?.let { + stringResource(id = R.string.top_bar_device_name, deviceName) + } + ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall + ) + if (daysLeftUntilExpiry != null) { + Text( + text = + stringResource( + id = R.string.top_bar_time_left, + pluralStringResource( + id = R.plurals.days, + daysLeftUntilExpiry, + daysLeftUntilExpiry + ) + ), + style = MaterialTheme.typography.bodySmall + ) + } else { + Spacer(Modifier) + } + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt index e55a549e27..1ac8873fc3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt @@ -26,7 +26,6 @@ import androidx.compose.ui.unit.sp import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.component.HtmlText import net.mullvad.mullvadvpn.compose.component.textResource -import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.model.Device @@ -58,10 +57,7 @@ fun ShowDeviceRemovalDialog(onDismiss: () -> Unit, onConfirm: () -> Unit, device }, text = { val htmlFormattedDialogText = - textResource( - id = R.string.max_devices_confirm_removal_description, - device.name.capitalizeFirstCharOfEachWord() - ) + textResource(id = R.string.max_devices_confirm_removal_description, device.name) HtmlText(htmlFormattedString = htmlFormattedDialogText, textSize = 16.sp.value) }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt index 46ee51640b..71f79e55b5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt @@ -37,7 +37,6 @@ import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar import net.mullvad.mullvadvpn.compose.dialog.DeviceNameInfoDialog import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD -import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens @@ -112,7 +111,7 @@ fun AccountScreen( Row(verticalAlignment = Alignment.CenterVertically) { InformationView( - content = uiState.deviceName?.capitalizeFirstCharOfEachWord() ?: "", + content = uiState.deviceName ?: "", whenMissing = MissingPolicy.SHOW_SPINNER ) IconButton( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt index 69d849183e..f694079ae3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt @@ -35,7 +35,7 @@ import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton import net.mullvad.mullvadvpn.compose.component.ConnectionStatusText import net.mullvad.mullvadvpn.compose.component.LocationInfo import net.mullvad.mullvadvpn.compose.component.Notification -import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR @@ -107,7 +107,7 @@ fun ConnectScreen( } } - ScaffoldWithTopBar( + ScaffoldWithTopBarAndDeviceName( topBarColor = if (uiState.tunnelUiState.isSecured()) { MaterialTheme.colorScheme.inversePrimary @@ -129,7 +129,9 @@ fun ConnectScreen( } .copy(alpha = AlphaTopBar), onSettingsClicked = onSettingsClick, - onAccountClicked = onAccountClick + onAccountClicked = onAccountClick, + deviceName = uiState.deviceName, + timeLeft = uiState.daysLeftUntilExpiry ) { Column( verticalArrangement = Arrangement.Bottom, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt index 37669a9851..4036d9547c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt @@ -30,7 +30,6 @@ import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar import net.mullvad.mullvadvpn.compose.dialog.ShowDeviceRemovalDialog import net.mullvad.mullvadvpn.compose.state.DeviceListItemUiState import net.mullvad.mullvadvpn.compose.state.DeviceListUiState -import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.lib.common.util.parseAsDateTime import net.mullvad.mullvadvpn.lib.theme.AlphaInactive import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar @@ -195,8 +194,7 @@ fun DeviceListScreen( Column { state.deviceUiItems.forEach { deviceUiState -> ListItem( - text = - deviceUiState.device.name.capitalizeFirstCharOfEachWord(), + text = deviceUiState.device.name, subText = deviceUiState.device.created.parseAsDateTime()?.let { creationDate -> diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt index 49de23228c..994e45b556 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -28,7 +28,7 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.ActionButton import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton import net.mullvad.mullvadvpn.compose.button.SitePaymentButton -import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState @@ -47,7 +47,7 @@ private fun PreviewOutOfTimeScreenDisconnected() { AppTheme { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(tunnelState = TunnelState.Disconnected), + uiState = OutOfTimeUiState(tunnelState = TunnelState.Disconnected, "Heroic Frog"), uiSideEffect = MutableSharedFlow<OutOfTimeViewModel.UiSideEffect>().asSharedFlow() ) } @@ -59,7 +59,8 @@ private fun PreviewOutOfTimeScreenConnecting() { AppTheme { OutOfTimeScreen( showSitePayment = true, - uiState = OutOfTimeUiState(tunnelState = TunnelState.Connecting(null, null)), + uiState = + OutOfTimeUiState(tunnelState = TunnelState.Connecting(null, null), "Strong Rabbit"), uiSideEffect = MutableSharedFlow<OutOfTimeViewModel.UiSideEffect>().asSharedFlow() ) } @@ -76,7 +77,8 @@ private fun PreviewOutOfTimeScreenError() { tunnelState = TunnelState.Error( ErrorState(cause = ErrorStateCause.IsOffline, isBlocking = true) - ) + ), + deviceName = "Stable Horse" ), uiSideEffect = MutableSharedFlow<OutOfTimeViewModel.UiSideEffect>().asSharedFlow() ) @@ -106,7 +108,7 @@ fun OutOfTimeScreen( } } val scrollState = rememberScrollState() - ScaffoldWithTopBar( + ScaffoldWithTopBarAndDeviceName( topBarColor = if (uiState.tunnelState.isSecured()) { MaterialTheme.colorScheme.inversePrimary @@ -128,7 +130,9 @@ fun OutOfTimeScreen( } .copy(alpha = AlphaTopBar), onSettingsClicked = onSettingsClick, - onAccountClicked = onAccountClick + onAccountClicked = onAccountClick, + deviceName = uiState.deviceName, + timeLeft = null ) { Column( verticalArrangement = Arrangement.Bottom, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt index 3c9c7352fe..93b9df5b7a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt @@ -14,7 +14,9 @@ data class ConnectUiState( val outAddress: String, val showLocation: Boolean, val connectNotificationState: ConnectNotificationState, - val isTunnelInfoExpanded: Boolean + val isTunnelInfoExpanded: Boolean, + val deviceName: String?, + val daysLeftUntilExpiry: Int? ) { companion object { val INITIAL = @@ -27,7 +29,9 @@ data class ConnectUiState( outAddress = "", showLocation = false, isTunnelInfoExpanded = false, - connectNotificationState = ConnectNotificationState.HideNotification + connectNotificationState = ConnectNotificationState.HideNotification, + deviceName = null, + daysLeftUntilExpiry = null ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt index cc19ac7ca8..f7794e5a55 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt @@ -2,4 +2,7 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.model.TunnelState -data class OutOfTimeUiState(val tunnelState: TunnelState = TunnelState.Disconnected) +data class OutOfTimeUiState( + val tunnelState: TunnelState = TunnelState.Disconnected, + val deviceName: String +) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 63fcf17ad2..7134f7b7d2 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -83,19 +83,21 @@ val uiModule = module { viewModel { ChangelogViewModel(get(), BuildConfig.VERSION_CODE, BuildConfig.ALWAYS_SHOW_CHANGELOG) } - viewModel { ConnectViewModel(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS, get()) } + viewModel { + ConnectViewModel(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS, get(), get()) + } viewModel { DeviceListViewModel(get(), get()) } viewModel { DeviceRevokedViewModel(get(), get()) } viewModel { LoginViewModel(get(), get()) } - viewModel { OutOfTimeViewModel(get(), get()) } viewModel { PrivacyDisclaimerViewModel(get()) } - viewModel { ReportProblemViewModel(get()) } viewModel { SelectLocationViewModel(get()) } viewModel { SettingsViewModel(get(), get()) } - viewModel { ViewLogsViewModel(get()) } viewModel { VoucherDialogViewModel(get(), get()) } viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get()) } + viewModel { ReportProblemViewModel(get()) } + viewModel { ViewLogsViewModel(get()) } + viewModel { OutOfTimeViewModel(get(), get(), get()) } } const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DateExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DateExtensions.kt index d3be3e09aa..e11434257a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DateExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DateExtensions.kt @@ -1,6 +1,8 @@ package net.mullvad.mullvadvpn.util import java.text.DateFormat +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.DurationUnit import org.joda.time.DateTime import org.joda.time.format.ISODateTimeFormat @@ -8,3 +10,6 @@ fun DateTime.formatDate(): String = ISODateTimeFormat.date().print(this) fun DateTime.toExpiryDateString(): String = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(this.toDate()) + +fun DateTime.daysFromNow() = + (toInstant().millis - DateTime.now().toInstant().millis).milliseconds.toInt(DurationUnit.DAYS) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt index d18e4f8fc9..e782f6f439 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt @@ -97,3 +97,30 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( ) } } + +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow<R> { + return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { + args: Array<*> -> + @Suppress("UNCHECKED_CAST") + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8 + ) + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt index 01a1c84896..01ba71ff86 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -24,6 +25,7 @@ import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.model.AccountExpiry import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy import net.mullvad.mullvadvpn.ui.serviceconnection.LocationInfoCache @@ -36,6 +38,7 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy import net.mullvad.mullvadvpn.util.appVersionCallbackFlow import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier import net.mullvad.mullvadvpn.util.combine +import net.mullvad.mullvadvpn.util.daysFromNow import net.mullvad.mullvadvpn.util.toInAddress import net.mullvad.mullvadvpn.util.toOutAddress import net.mullvad.talpid.tunnel.ActionAfterDisconnect @@ -47,6 +50,7 @@ class ConnectViewModel( private val serviceConnectionManager: ServiceConnectionManager, private val isVersionInfoNotificationEnabled: Boolean, accountRepository: AccountRepository, + private val deviceRepository: DeviceRepository, ) : ViewModel() { private val _uiSideEffect = MutableSharedFlow<UiSideEffect>(extraBufferCapacity = 1) val uiSideEffect = _uiSideEffect.asSharedFlow() @@ -74,7 +78,8 @@ class ConnectViewModel( serviceConnection.connectionProxy.tunnelUiStateFlow(), serviceConnection.connectionProxy.tunnelRealStateFlow(), accountRepository.accountExpiryState, - _isTunnelInfoExpanded + _isTunnelInfoExpanded, + deviceRepository.deviceState.map { it.deviceName() } ) { location, relayLocation, @@ -82,7 +87,8 @@ class ConnectViewModel( tunnelUiState, tunnelRealState, accountExpiry, - isTunnelInfoExpanded -> + isTunnelInfoExpanded, + deviceName -> if (tunnelRealState.isTunnelErrorStateDueToExpiredAccount()) { _uiSideEffect.tryEmit(UiSideEffect.OpenOutOfTimeView) } @@ -124,7 +130,9 @@ class ConnectViewModel( tunnelUiState = tunnelUiState, versionInfo = versionInfo, accountExpiry = accountExpiry - ) + ), + deviceName = deviceName, + daysLeftUntilExpiry = accountExpiry.date()?.daysFromNow() ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt index 8a789f62fd..b1df2d2225 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState @@ -31,7 +31,8 @@ import org.joda.time.DateTime class OutOfTimeViewModel( private val accountRepository: AccountRepository, private val serviceConnectionManager: ServiceConnectionManager, - private val pollAccountExpiry: Boolean = true + private val deviceRepository: DeviceRepository, + private val pollAccountExpiry: Boolean = true, ) : ViewModel() { private val _uiSideEffect = MutableSharedFlow<UiSideEffect>(extraBufferCapacity = 1) @@ -47,10 +48,21 @@ class OutOfTimeViewModel( } } .flatMapLatest { serviceConnection -> - serviceConnection.connectionProxy.tunnelStateFlow() + kotlinx.coroutines.flow.combine( + serviceConnection.connectionProxy.tunnelStateFlow(), + deviceRepository.deviceState + ) { tunnelState, deviceState -> + OutOfTimeUiState( + tunnelState = tunnelState, + deviceName = deviceState.deviceName() ?: "", + ) + } } - .map { tunnelState -> OutOfTimeUiState(tunnelState = tunnelState) } - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState()) + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(), + OutOfTimeUiState(deviceName = "") + ) init { viewModelScope.launch { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt index fe2ddcb66a..6c9b2ea75d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt @@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.WelcomeUiState import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL -import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository @@ -61,7 +60,7 @@ class WelcomeViewModel( WelcomeUiState( tunnelState = tunnelState, accountNumber = deviceState.token(), - deviceName = deviceState.deviceName()?.capitalizeFirstCharOfEachWord() + deviceName = deviceState.deviceName() ) } } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt index 18f8447f44..bddaee353e 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt @@ -20,11 +20,13 @@ import net.mullvad.mullvadvpn.compose.state.ConnectNotificationState import net.mullvad.mullvadvpn.compose.state.ConnectUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.relaylist.RelayCountry import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.VersionInfo import net.mullvad.mullvadvpn.ui.serviceconnection.AppVersionInfoCache import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache @@ -65,6 +67,7 @@ class ConnectViewModelTest { ) ) private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing) + private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial) // Service connections private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() @@ -77,6 +80,9 @@ class ConnectViewModelTest { // Account Repository private val mockAccountRepository: AccountRepository = mockk() + // Device Repository + private val mockDeviceRepository: DeviceRepository = mockk() + // Captures private val locationSlot = slot<((GeoIpLocation?) -> Unit)>() private val relaySlot = slot<(List<RelayCountry>, RelayItem?) -> Unit>() @@ -103,6 +109,8 @@ class ConnectViewModelTest { every { mockAccountRepository.accountExpiryState } returns accountExpiryState + every { mockDeviceRepository.deviceState } returns deviceState + every { mockConnectionProxy.onUiStateChange } returns eventNotifierTunnelUiState every { mockConnectionProxy.onStateChange } returns eventNotifierTunnelRealState @@ -117,6 +125,7 @@ class ConnectViewModelTest { ConnectViewModel( serviceConnectionManager = mockServiceConnectionManager, accountRepository = mockAccountRepository, + deviceRepository = mockDeviceRepository, isVersionInfoNotificationEnabled = true ) } @@ -351,6 +360,7 @@ class ConnectViewModelTest { val expectedConnectNotificationState = ConnectNotificationState.ShowAccountExpiryNotification(mockDateTime) every { mockDateTime.isBefore(any<ReadableInstant>()) } returns true + every { mockDateTime.toInstant().millis } returns 0 // Act, Assert viewModel.uiState.test { @@ -360,6 +370,7 @@ class ConnectViewModelTest { locationSlot.captured.invoke(mockLocation) relaySlot.captured.invoke(mockk(), mockk()) accountExpiryState.value = AccountExpiry.Available(mockDateTime) + val result = awaitItem() assertEquals(expectedConnectNotificationState, result.connectNotificationState) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt index 5f81032938..8c1ec10f5a 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt @@ -16,8 +16,10 @@ import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.model.AccountExpiry +import net.mullvad.mullvadvpn.model.DeviceState import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.repository.AccountRepository +import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.AuthTokenCache import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer @@ -39,6 +41,7 @@ class OutOfTimeViewModelTest { private val serviceConnectionState = MutableStateFlow<ServiceConnectionState>(ServiceConnectionState.Disconnected) private val accountExpiryState = MutableStateFlow<AccountExpiry>(AccountExpiry.Missing) + private val deviceState = MutableStateFlow<DeviceState>(DeviceState.Initial) // Service connections private val mockServiceConnectionContainer: ServiceConnectionContainer = mockk() @@ -48,6 +51,7 @@ class OutOfTimeViewModelTest { private val eventNotifierTunnelRealState = EventNotifier<TunnelState>(TunnelState.Disconnected) private val mockAccountRepository: AccountRepository = mockk() + private val mockDeviceRepository: DeviceRepository = mockk() private val mockServiceConnectionManager: ServiceConnectionManager = mockk() private lateinit var viewModel: OutOfTimeViewModel @@ -64,10 +68,13 @@ class OutOfTimeViewModelTest { every { mockAccountRepository.accountExpiryState } returns accountExpiryState + every { mockDeviceRepository.deviceState } returns deviceState + viewModel = OutOfTimeViewModel( accountRepository = mockAccountRepository, serviceConnectionManager = mockServiceConnectionManager, + deviceRepository = mockDeviceRepository, pollAccountExpiry = false ) } @@ -104,7 +111,7 @@ class OutOfTimeViewModelTest { // Act, Assert viewModel.uiState.test { - assertEquals(OutOfTimeUiState(), awaitItem()) + assertEquals(OutOfTimeUiState(deviceName = ""), awaitItem()) serviceConnectionState.value = ServiceConnectionState.ConnectedReady(mockServiceConnectionContainer) eventNotifierTunnelRealState.notify(tunnelRealStateTestItem) diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonStringExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonStringExtensions.kt index f46664e929..06a2de9148 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonStringExtensions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonStringExtensions.kt @@ -7,12 +7,6 @@ private const val EXPIRY_FORMAT = "YYYY-MM-dd HH:mm:ss z" private const val BIG_DOT_CHAR = "●" private const val SPACE_CHAR = ' ' -fun String.capitalizeFirstCharOfEachWord(): String { - return split(" ") - .joinToString(" ") { word -> word.replaceFirstChar { firstChar -> firstChar.uppercase() } } - .trimEnd() -} - fun String.parseAsDateTime(): DateTime? { return try { DateTime.parse(this, DateTimeFormat.forPattern(EXPIRY_FORMAT)) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt index a91ce46148..f856ef8c89 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt @@ -7,7 +7,7 @@ import org.joda.time.DateTime sealed class AccountExpiry : Parcelable { @Parcelize data class Available(val expiryDateTime: DateTime) : AccountExpiry() - @Parcelize object Missing : AccountExpiry() + @Parcelize data object Missing : AccountExpiry() fun date(): DateTime? { return (this as? Available)?.expiryDateTime diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt index 440d03de55..2af9b01362 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt @@ -19,10 +19,16 @@ sealed class DeviceState : Parcelable { } fun deviceName(): String? { - return (this as? LoggedIn)?.accountAndDevice?.device?.name + return (this as? LoggedIn)?.accountAndDevice?.device?.name?.capitalizeFirstCharOfEachWord() } fun token(): String? { return (this as? LoggedIn)?.accountAndDevice?.account_token } } + +private fun String.capitalizeFirstCharOfEachWord(): String { + return split(" ") + .joinToString(" ") { word -> word.replaceFirstChar { firstChar -> firstChar.uppercase() } } + .trimEnd() +} diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml index bb26081112..b98455f7d9 100644 --- a/android/lib/resource/src/main/res/values-da/strings.xml +++ b/android/lib/resource/src/main/res/values-da/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Skift placering</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Slå VPN til/fra</string> + <string name="top_bar_device_name">Enhedsnavn: %1$s</string> + <string name="top_bar_time_left">Resterende tid: %1$s</string> <string name="try_again">Prøv igen</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Hvilken TCP-port UDP-over-TCP tilsløringsprotokollen skal forbinde til på VPN-serveren.</string> diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml index 357e4209e3..19120efa7b 100644 --- a/android/lib/resource/src/main/res/values-de/strings.xml +++ b/android/lib/resource/src/main/res/values-de/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Ort wechseln</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPN umschalten</string> + <string name="top_bar_device_name">Gerätename: %1$s</string> + <string name="top_bar_time_left">Verbleibende Zeit: %1$s</string> <string name="try_again">Erneut versuchen</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Mit welchem TCP-Port sich das UDP-über-TCP-Verschleierungsprotokoll auf dem VPN-Server verbinden soll.</string> diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml index 1ca4ad0fa5..e5c5f7d657 100644 --- a/android/lib/resource/src/main/res/values-es/strings.xml +++ b/android/lib/resource/src/main/res/values-es/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Cambiar ubicación</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Alternar VPN</string> + <string name="top_bar_device_name">Nombre del dispositivo: %1$s</string> + <string name="top_bar_time_left">Tiempo restante: %1$s</string> <string name="try_again">Volver a intentarlo</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">El puerto TCP al que se conectará el protocolo de ofuscación de UDP sobre TCP en el servidor VPN.</string> diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml index eb02e63756..379d8dd4bb 100644 --- a/android/lib/resource/src/main/res/values-fi/strings.xml +++ b/android/lib/resource/src/main/res/values-fi/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Vaihda sijaintia</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Vaihda VPN:ää</string> + <string name="top_bar_device_name">Laitteen nimi: %1$s</string> + <string name="top_bar_time_left">Aikaa jäljellä: %1$s</string> <string name="try_again">Yritä uudelleen</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Määrittää, mihin VPN-palvelimen TCP-porttiin \"UDP TCP:n kautta\" -hämäysteknologia-protokollan tulee muodostaa yhteys.</string> diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml index da970c6d89..9da5482c92 100644 --- a/android/lib/resource/src/main/res/values-fr/strings.xml +++ b/android/lib/resource/src/main/res/values-fr/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Changer de localisation</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Activer/désactiver le VPN</string> + <string name="top_bar_device_name">Nom de l\'appareil : %1$s</string> + <string name="top_bar_time_left">Temps restant : %1$s</string> <string name="try_again">Réessayer</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Le port TCP auquel le protocole de dissimulation UDP sur TCP doit se connecter sur le serveur VPN.</string> diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml index c988e760cf..e91aaecdb9 100644 --- a/android/lib/resource/src/main/res/values-it/strings.xml +++ b/android/lib/resource/src/main/res/values-it/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Cambia posizione</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Attiva/disattiva VPN</string> + <string name="top_bar_device_name">Nome del dispositivo: %1$s</string> + <string name="top_bar_time_left">Tempo rimasto: %1$s</string> <string name="try_again">Riprova</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">A quale porta TCP deve connettersi il protocollo di offuscamento UDP-over-TCP sul server VPN.</string> diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml index 8c9ef84739..3112ec2b1c 100644 --- a/android/lib/resource/src/main/res/values-ja/strings.xml +++ b/android/lib/resource/src/main/res/values-ja/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">場所を切り替える</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPNの切り替え</string> + <string name="top_bar_device_name">デバイス名: %1$s</string> + <string name="top_bar_time_left">残り時間: %1$s</string> <string name="try_again">再試行</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">UDP-over-TCP難読化プロトコルで接続する必要のあるVPNサーバーのTCPポートです。</string> diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml index 209023a64e..b535966911 100644 --- a/android/lib/resource/src/main/res/values-ko/strings.xml +++ b/android/lib/resource/src/main/res/values-ko/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">위치 전환</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPN 전환</string> + <string name="top_bar_device_name">장치 이름: %1$s</string> + <string name="top_bar_time_left">남은 시간: %1$s</string> <string name="try_again">다시 시도</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">UDP-over-TCP 난독 처리 프로토콜이 VPN 서버에서 연결해야 하는 TCP 포트입니다.</string> diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml index 0aef2a2c2e..6a0f2ba377 100644 --- a/android/lib/resource/src/main/res/values-my/strings.xml +++ b/android/lib/resource/src/main/res/values-my/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">တည်နေရာ ပြောင်းရန်</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPN ရွေးသုံးရန်</string> + <string name="top_bar_device_name">စက်အမည်- %1$s</string> + <string name="top_bar_time_left">ကျန်သည့် အချိန်- %1$s</string> <string name="try_again">ထပ်ကြိုးစားရန်</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">VPN ဆာဗာကို ဖွင့်ရန် ၎င်း TCP ပေါ့တ် UDP-over-TCP Obfuscation ပရိုတိုကောလ်နှင့် ချိတ်ဆက်ထားသင့်ပါသည်။</string> diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml index 2b0e370bb3..6872608306 100644 --- a/android/lib/resource/src/main/res/values-nb/strings.xml +++ b/android/lib/resource/src/main/res/values-nb/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Bytt plassering</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Velg VPN</string> + <string name="top_bar_device_name">Enhetsnavn: %1$s</string> + <string name="top_bar_time_left">Tid igjen: %1$s</string> <string name="try_again">Prøv på nytt</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">TCP-porten som UDP-over-TCP-tilsløringen skal koble til på VPN-serveren.</string> diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml index cdbaa554c3..005f1c6907 100644 --- a/android/lib/resource/src/main/res/values-nl/strings.xml +++ b/android/lib/resource/src/main/res/values-nl/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Locatie wijzigen</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPN in-/uitschakelen</string> + <string name="top_bar_device_name">Apparaatnaam: %1$s</string> + <string name="top_bar_time_left">Resterende tijd: %1$s</string> <string name="try_again">Probeer het opnieuw</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Met welke TCP-poort moet het UDP-over-TCP-obfuscatieprotocol verbinding maken op de VPN-server.</string> diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml index 2e2e6ee267..98b69a66a8 100644 --- a/android/lib/resource/src/main/res/values-pl/strings.xml +++ b/android/lib/resource/src/main/res/values-pl/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Zmień lokalizację</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Przełącz VPN</string> + <string name="top_bar_device_name">Nazwa urządzenia: %1$s</string> + <string name="top_bar_time_left">Pozostało: %1$s</string> <string name="try_again">Spróbuj ponownie</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Port TCP, z którym powinien łączyć się protokół zaciemniania UDP-przez-TCP na serwerze VPN.</string> diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml index 2fee06cab6..5dd4fd61ea 100644 --- a/android/lib/resource/src/main/res/values-pt/strings.xml +++ b/android/lib/resource/src/main/res/values-pt/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Alterar local</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Alternar VPN</string> + <string name="top_bar_device_name">Nome do dispositivo: %1$s</string> + <string name="top_bar_time_left">Tempo restante: %1$s</string> <string name="try_again">Tentar novamente</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">A que porta TCP o protocolo de ofuscação UDP sobre TCP deve ligar-se no servidor VPN.</string> diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml index 0fb01c88ad..7b9acc9195 100644 --- a/android/lib/resource/src/main/res/values-ru/strings.xml +++ b/android/lib/resource/src/main/res/values-ru/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Сменить местоположение</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Включение VPN</string> + <string name="top_bar_device_name">Имя устройства: %1$s</string> + <string name="top_bar_time_left">Осталось времени: %1$s</string> <string name="try_again">Повторить попытку</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">TCP-порт, к которому должен подключаться протокол обфускации UDP через TCP на VPN-сервере.</string> diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml index c65809dc5d..d8183b2435 100644 --- a/android/lib/resource/src/main/res/values-sv/strings.xml +++ b/android/lib/resource/src/main/res/values-sv/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Växla plats</string> <string name="tcp">TCP</string> <string name="toggle_vpn">Växla VPN</string> + <string name="top_bar_device_name">Enhetsnamn: %1$s</string> + <string name="top_bar_time_left">Tid kvar: %1$s</string> <string name="try_again">Försök igen</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">Vilken TCP-port som UDP-över-TCP-obfuskeringsprotokoll bör ansluta till på VPN-servern.</string> diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml index 3f01840e0a..7afc8a7b44 100644 --- a/android/lib/resource/src/main/res/values-th/strings.xml +++ b/android/lib/resource/src/main/res/values-th/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">สลับตำแหน่ง</string> <string name="tcp">TCP</string> <string name="toggle_vpn">เปิด/ปิด VPN</string> + <string name="top_bar_device_name">ชื่ออุปกรณ์: %1$s</string> + <string name="top_bar_time_left">เหลือเวลา: %1$s</string> <string name="try_again">ลองอีกครั้ง</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">พอร์ต TCP ใดที่โพรโทคอลการทำให้ข้อมูลยุ่งเหยิง UDP-ผ่าน-TCP ควรเชื่อมต่อบนเซิร์ฟเวอร์ VPN</string> diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml index 08ff5f47e6..908f9ce2d9 100644 --- a/android/lib/resource/src/main/res/values-tr/strings.xml +++ b/android/lib/resource/src/main/res/values-tr/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">Konum değiştir</string> <string name="tcp">TCP</string> <string name="toggle_vpn">VPN\'i aç/kapat</string> + <string name="top_bar_device_name">Cihaz adı: %1$s</string> + <string name="top_bar_time_left">Kalan süre: %1$s</string> <string name="try_again">Tekrar dene</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">TCP üzerinden UDP gizleme protokolünün VPN sunucusunda hangi TCP portuna bağlanması gerekiyor.</string> diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml index 174262c638..667ffbcde9 100644 --- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">切换位置</string> <string name="tcp">TCP</string> <string name="toggle_vpn">切换 VPN</string> + <string name="top_bar_device_name">设备名称:%1$s</string> + <string name="top_bar_time_left">剩余时间:%1$s</string> <string name="try_again">重试</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">UDP-over-TCP 混淆协议应连接到 VPN 服务器上的哪个 TCP 端口。</string> diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml index 70b0d42c55..5378b92550 100644 --- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml +++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml @@ -175,6 +175,8 @@ <string name="switch_location">切換位置</string> <string name="tcp">TCP</string> <string name="toggle_vpn">切換 VPN</string> + <string name="top_bar_device_name">裝置名稱:%1$s</string> + <string name="top_bar_time_left">剩餘時間:%1$s</string> <string name="try_again">再試一次</string> <string name="udp">UDP</string> <string name="udp_over_tcp_port_info">UDP-over-TCP 混淆通訊協定應連線到 VPN 伺服器上的哪個 TCP 連接埠。</string> diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index bc9630e974..c9c837d38d 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -224,4 +224,6 @@ <string name="verifying_voucher">Verifying voucher…</string> <string name="added_to_your_account">%s was added to your account.</string> <string name="less_than_one_day">less than one day</string> + <string name="top_bar_time_left">Time left: %s</string> + <string name="top_bar_device_name">Device name: %s</string> </resources> |
