summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-10-13 11:04:45 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-10-13 11:04:45 +0200
commit6842d204e3585b216f7e271ecb9eaf4a158ee666 (patch)
treed5bac59e0f8a4daf7b5db9c4d51ee9714aa54405
parentbd2bdbedd280ca84a543472143c959d655651a73 (diff)
parente72415c31be76c019ec135ccb231560454675e7e (diff)
downloadmullvadvpn-6842d204e3585b216f7e271ecb9eaf4a158ee666.tar.xz
mullvadvpn-6842d204e3585b216f7e271ecb9eaf4a158ee666.zip
Merge branch 'add-device-name-and-time-left-to-main-view-droid-90'
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt44
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreenTest.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt47
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/TopBar.kt116
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeviceRemovalDialog.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ConnectUiState.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/DateExtensions.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/FlowUtils.kt27
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModel.kt14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt3
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ConnectViewModelTest.kt11
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModelTest.kt9
-rw-r--r--android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonStringExtensions.kt6
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt2
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt8
-rw-r--r--android/lib/resource/src/main/res/values-da/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-de/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-es/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fi/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-fr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-it/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ja/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ko/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-my/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nb/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-nl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pl/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-pt/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-ru/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-sv/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-th/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-tr/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rCN/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values-zh-rTW/strings.xml2
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml2
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>